言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

Shared element transitionでリストの要素を並び替える

やりたいこと

Fragmentを切り替えてReordering(要素を並び替え)する shared element transition の記事を見て楽しそうだったからやってみた。 medium.com

全く同じことをやっても楽しくないので、Fragment <-> Activityでやってみる。

できた

いきなりだけど成果物

f:id:muumuumuumuu:20180909122945g:plain

コードはこの辺

github.com

やったこと

Activityで shared element transition するには startActivity() するときに第2引数にtransition animationのbundleを渡してやる必要がある。 transition animationを作るのに必要なのは下記のPair.

  • 移動元になるview
  • 上記viewに対して一意になるID

今回はリストの並び替えなので上記のpairを並び替えたいViewの数だけ持つ必要がある。 RecyclerView でリストを表示している場合、Adapterの onBindViewHolder で各Viewにアクセスできるのでこれを保持しておく。

class ReorderingAdapter(private val activity: Activity)
    : RecyclerView.Adapter<ReorderingViewHolder>() {

    val items = mutableListOf<ImageView>() // adapterのpropertyとしてmutableListを持つ

    override fun onBindViewHolder(holder: ReorderingViewHolder, position: Int) {
        holder.bind(position)
        if (!items.contains(holder.image)) {
            items.add(holder.image) // 画面に表示されたタイミングでlistに追加
        }
    }

}

実際に並び替えを実行するタイミングで保持していたviewのリストからtransition animation用のbundleを生成する。

fab.setOnClickListener {
    context?.let {
        val itemList = mutableListOf<Pair<View, String>>()
        (recycler.adapter as? ReorderingAdapter)?.items?.forEachIndexed { index, view ->
            itemList.add(Pair(view, IMAGE_TRANSITION_NAME + index))
        }

        val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
            activity as Activity, *itemList.toTypedArray()
        ).toBundle()
        ReorderingActivity.start(it, options)
    }
}

並び替え後のviewの方でも同じIDを設定する。

itemView.setTag(R.id.position, position)
ViewCompat.setTransitionName(image, IMAGE_TRANSITION_NAME + position)

あとは普通の Shared element transition のように postponeEnterTransition() したり見た目の微調整して終わり。

注意点として、画面に表示されていないものはanimationの対象にならない。 そもそも onBindViewHolder() を通らないとitemsのlistにaddできないが、仮にlistに存在していたとしても画面外だったらanimationされない。RecyclerViewを使っていたのですでにrecycleされてしまっている場合はviewがなくなっちゃってるからanimationできないのかな。

おまけ

transition animation用のbundleを生成する時に ActivityOptionsCompat#makeSceneTransitionAnimation() を使うが、このメソッドの第2引数が可変長引数になっている。

    @NonNull
    @SuppressWarnings("unchecked")
    public static ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity,
            Pair<View, String>... sharedElements) {
        if (Build.VERSION.SDK_INT >= 21) {
            android.util.Pair<View, String>[] pairs = null;
            if (sharedElements != null) {
                pairs = new android.util.Pair[sharedElements.length];
                for (int i = 0; i < sharedElements.length; i++) {
                    pairs[i] = android.util.Pair.create(
                            sharedElements[i].first, sharedElements[i].second);
                }
            }
            return createImpl(ActivityOptions.makeSceneTransitionAnimation(activity, pairs));
        }
        return new ActivityOptionsCompat();
    }

可変長引数にListを渡すときは一度Arrayに型変換してからspread operator(*)を使う。

*itemList.toTypedArray()

2018年前半を振り返る

はじめに

2018年前半に自分が何をしていたか後から振り返られるようにメモを残しておく。2017年のまとめと同じく自分のtweetを振り返ってペタペタしていく。本当は年末にまとめて一年分やりたかったがtweet数が多すぎて一気にやると辛いという前回の反省を生かして2018年は半分に区切っていく。

前回のまとめはこちら

muumuutech.hatenablog.com

1月

この時点で割と仕事に余裕があるときは隙あらば有休消化に励んでいた気がする。

普段アニメをあまりみないけどずっと気になっていた廻るピングドラムを見始めた。最初はサイコホラーなのかサスペンスなのかシュールコメディなのかわからなかったけど後半一気に止まらない感じがすごかった。
社のslackに「生存戦略」のスタンプがあってなんだろうと思っていたけど元ネタがわかってスッキリ。

長らく続けていたAndroidもくもく会、自分の退職に伴い最終回を迎えた。運営している自分もとっても楽しかったし本当にありがとうございました!

今年は(まだ半分しか終わってないけど)これと同じ内容5回くらいつぶやいている気がする。今でも絶対にフォローしているつもりだけど実はフォローしていない人いっぱいいそうだ。

DroidKaigi appにコントリビュートする実績解除した。

DroidKaigi preludeでgfxさんと二人でセッション解説をやった。会場にいた誰よりも楽しんでいた自信があるw

最終出社日、最後のPRで自分で自分を消すやつ、一度やってみたかったので記念スクショ撮ってた。

1月は送別会とかでたくさん美味しいもの食べたり、寄せ書きが嬉しすぎて号泣したり忙しかった。幸せなことだな〜〜〜

2月

2月から丸一ヶ月有休消化で寒すぎて引きこもりまくってた。

DroidKaigi、今年はオーディエンス参加だった。濃い二日間でした!

今年は初めて確定申告した。一度フローを理解するとそんなでもないはずなんだけど、初回はなかなかつらみがあった。

2月にこんなことをつぶやいているけど、2018年後半始まっても未だに毎月スターエンジニアが誰かしら転職している気がする。

Paymoの残高まだまだ余ってます。Paymo支払いの飲み会とかやりたい。ただしPaymoで支払う側として。

有休消化でのんびり南の島にバカンスに行った。一人で。

主にビーチサイドで本を読んでいたんだけど、そのとき読んだ本の記録はこちら。 muumuutech.hatenablog.com

日本に戻って来たら花粉が飛び始めていた。

3月

新しい職場。外国人の同僚も多いのでここから英語も併記したりし始めている。

花粉がマジで辛い

新しい職場でももくもく会をやっていくぞ!

次回の開催はこちら

mercari-android-mokumoku.connpass.com

Flutter盛り上がって来た

shibuya.apkがライブ配信されるようになった。DeployGateさんのオフィスで配信見ながら女子エンジニア会。美味しいお料理とお酒で最高だった

友人の家でバーフバリ鑑賞会をやった。ボリウッド映画多分初めてだったけど楽しかった!

4月

Android Dagashiデビューしてテンションが上がっていた

IWDのトークセッションに参加した。 kinukoさんkeynoteが素晴らしすぎて、この資料は定期的に見返したい



Flutter勉強会やった

勉強会の前に自分でも触ってみるか〜と思って試したらこのザマ

築地市場が移転する前に早朝のツアーに行ってみた。

ツアーは基本的に英語だし参加者もほとんど外国人でここどこだっけ?ってなった

この日は築地だけじゃなく色々日本っぽいところを巡るデートをしていた

5月

出張で福岡に来た

移動中の飛行機のお供で読んだOKR本よかった!

OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法

OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法

Kotlin愛好会で登壇した

6月

出張でアメリカのオフィスに一週間ほど行った。

海外オフィスでの雑な感想が今までで一番伸びた

UberiPhone置き忘れて大変だった

muumuutech.hatenablog.com

久しぶりに新卒で入った会社の人たちと飲んでいた。普段エンジニアの中でもweb系の本当に限られた人としかあってなかったんだなーと実感した

おまけ

夫シリーズ

Android Frameworkのコードにbreakpointを止めるメモ

Android開発をしていると、Frameworkがどういう挙動をしているか調べたくなる時がある。そういう時はFrameworkのコードにbreakpointを置くんだけど、止まってくれたり止まらなかったりすることがあるので困っていた。

このTweetに対して神リプライがついたので流れないようにメモしておく。

実際試したところ、Compile SDK versionと合わせたEmulatorを作ってそこで動かすというのが良さそう。

StateListAnimatorを使ってXMLだけでAnimationをつける

こちらの記事を読んで「<selector> の中にobject animator埋め込めるの知らなかった!!すげー!!!!」となったので遊んでみたメモ。

android.jlelse.eu

StateListAnimator

AndroidにはStateListAnimatorというクラスがあって、Viewのdrawable stateによってAnimationを書き分けることができる。何が最高かってこのAnimationはXMLでお手軽にかけるってところだ。1

ドキュメントはこちら

遊んでみた

アイコンに触っている間だけ大きくなるアニメーションを書いてみた。 xmlはこんな感じ

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <set>
            <objectAnimator
                android:duration="200"
                android:propertyName="scaleX"
                android:valueTo="1.5" />
            <objectAnimator
                android:duration="200"
                android:propertyName="scaleY"
                android:valueTo="1.5" />
            <objectAnimator
                android:duration="200"
                android:propertyName="transitionZ"
                android:valueTo="10dp" />
        </set>
    </item>
    <item>
        <set>
            <objectAnimator
                android:duration="200"
                android:propertyName="scaleX"
                android:valueTo="1" />
            <objectAnimator
                android:duration="200"
                android:propertyName="scaleY"
                android:valueTo="1" />
            <objectAnimator
                android:duration="200"
                android:propertyName="transitionZ"
                android:valueTo="0dp" />
        </set>
    </item>
</selector>

android:state_pressed="true" の時とそうでない場合でObjectAnimatorの振る舞いを変えることができる。上記公式ドキュメントのリンク先を参照するとpress以外のstateもたくさんある。

xmlobjectAnimatorタグで使えるattribute一覧はこちら

Animation resources  |  Android Developers

    <objectAnimator
        android:propertyName="string"
        android:duration="int"
        android:valueFrom="float | int | color"
        android:valueTo="float | int | color"
        android:startOffset="int"
        android:repeatCount="int"
        android:repeatMode=["repeat" | "reverse"]
        android:valueType=["intType" | "floatType"]/>

このanimationとviewを紐づける時はこんな感じでandroid:stateListAnimatoを使う。

android:stateListAnimator="@animator/fav_animator"

こんな感じでハートがドキドキする。

f:id:muumuumuumuu:20180718234247g:plain

サンプルコードはこちら。

github.com


  1. 個人的には最高だと思うけど、Viewの振る舞いをXMLに書くことに対して反対派の人はいると思うので、そこはもうお好みで :pray:

KotlinのRegexとDestructuredで文字列からdata classに変換する

Androidでアプリを書いていて、正規表現を扱うときにjava.util.regex.Matcherを使うこと多いが、あれは個人的には好きではない。もっとスッキリかけるんじゃないかなぁといつも思ってしまう。

例えば、URLからprotocolとdomainを正規表現を使って取り出すコードを書こうと思うとこんな感じになるかと思う。

    companion object {
        private const val REGEX: String = "(.*)://(.*)"
    }

    fun hoge() {
        val urlString = "http://www.com"
        val url = generateUrlFromString(urlString)
    }

    private fun generateUrlFromString(url: String): Url? {
        val matcher = Pattern.compile(REGEX).matcher(url)
        return if (matcher.find()) {
            val protocol = matcher.group(1)
            val domain = matcher.group(2)
            return Url(protocol, domain)
        } else {
            null
        }
    }

data class Url(val protocol: String, val domain: String)

再帰の場合に注意が必要で、上記の場合だとmatcher.group()が0ではなく1と2だったり何かとやらかしてしまったりする。(matcher.group(0)にはhttp://www.comが入ってくる)

そんなときに、この記事見て最高では????と思ったので自分でも試してみることにした。一行でまとめると Destructuredクラスが最高では?という話です。やっぱりKotlinは可愛い。

medium.com

上記のgenerateUrlFromString()Java正規表現からKotlinの正規表現に書き換えたコードがこちら。

    private fun generateUrlFromString(url: String): Url? =
        REGEX.toRegex().matchEntire(url)
            ?.destructured
            ?.let { (protocol, domain) ->
                Url(protocol, domain)
            }

これだけで十分可愛さが伝わる気がするが、蛇足ながら可愛いポイントを書いていく。

  1. 文字列REGEXtoRegex()Regexクラスに変換
  2. matchEntire()で引数url stringを渡すことによりMatchResultを取得
  3. 2で正規表現にマッチしなかった場合はnullになるので、?.destructuredを取得。これは正規表現にマッチしたgroupをDestructuredクラスで返してくれる
  4. 3ですでにnullの可能性があるので引き続き?.でletを呼ぶ。ポイントはここでラベルをつけることができるという点。サンプルコードだとprotocoldomainをすぐに使っているが、let blockのなかで複雑なことをしている場合にわざわざ名前をつけるために変数に代入しなくてもよくて可読性がグッと上がる。

これらをワインラインでスッキリかけるところがまた可愛い!

当然ながらKotlinのRegexJavaのMatcherとかをwrapしているので、めんどくさいところは全てやってくれていて最高。

言いたいことは以上です。

kotlin.concurrent.threadは新規threadが作成されるけどcurrent threadのgroupで動く

[2018.7.15 ご指摘いただき記事の最後に追記しました]

あらまし

先日おもしろいTweetを見つけたのでちょっと調べてみたメモ。

残念ながら手元で上記の現象は再現しなくて、MashmallowでもCrashが発生しなかったんだけど、

Kotlinのthreadってmain threadじゃない別threadで動くんじゃなかったっけ…?🤔

と思ったので調査。
ちなみにMashmallowの端末が手元になかったのでEmulator環境でやってます。

まずは調査用の簡単なコードを準備。Activityの onResume() でUI操作を行う。(textという名前のTextViewに"hogehoge"というtextをセットしています。)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_thread)
        Timber.plant(Timber.DebugTree())
        // main threadのログ出力結果を確認するためにログを入れる
        Timber.d("main thread: ${Looper.getMainLooper().thread}")
    }

    override fun onResume() {
        super.onResume()
        thread {
            Timber.d("current thread: ${Thread.currentThread()}")
            text.text = "hogehoge"
        }
    }

ThreadのtoString()で表示しているのは下記の三つ。 thread nameprioritygroup name

    public String toString() {
        ThreadGroup group = getThreadGroup();
        if (group != null) {
            return "Thread[" + getName() + "," + getPriority() + "," +
                           group.getName() + "]";
        } else {
            return "Thread[" + getName() + "," + getPriority() + "," +
                            "" + "]";
        }
    }

ログとるときの操作は、単純に画面を表示させるだけ。結果はこんな感じ。

07-14 08:31:01.772 5497-5497/? D/ThreadActivity: main thread: Thread[main,5,main]
07-14 08:31:01.777 5497-5513/? D/ThreadActivity$onResume: current thread: Thread[Thread-266,5,main]

thread blockの中でログを出力した方は確かにthread nameを見ると別のthreadになっている。しかし3つ目のgroup nameはmainになっている。

kotlin.concurrent.threadを読んでみよう

Kotlinのthreadは気軽にサンプルコードのように書いている人が多いと思うが、じつはたくさんのdefault引数が用意されている

public fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit): Thread {
    val thread = object : Thread() {
        public override fun run() {
            block()
        }
    }
    if (isDaemon)
        thread.isDaemon = true
    if (priority > 0)
        thread.priority = priority
    if (name != null)
        thread.name = name
    if (contextClassLoader != null)
        thread.contextClassLoader = contextClassLoader
    if (start)
        thread.start()
    return thread
}

で、そんなdefault引数を置いておいて、メソッドの頭でJavajava.lang.Threadをnewしているんだけど、このときcurrent threadをparentにとるようになっているので、main threadからthread()を呼び出すとmain threadのgroupで新規threadが作られるのだ。

    private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        Thread parent = currentThread();
        if (g == null) {
            g = parent.getThreadGroup();
        }

        g.addUnstarted();
        this.group = g;

UI操作できるのは一つのthreadからだけ

ちなみに上記のサンプルコードで、一度backgroundに行って再びonResume()に帰ってくるとアプリがcrashする。

    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

backgroundから復帰した時にonResume()でもう一度threadから新規threadを作成しているので最初にさわったthreadではない別のthreadからUIを触ろうとしてcrashする。 ログを見てもthread nameが変わっていることがわかる

// Activity起動
07-14 08:31:01.772 5497-5497/? D/ThreadActivity: main thread: Thread[main,5,main]
07-14 08:31:01.777 5497-5513/? D/ThreadActivity$onResume: current thread: Thread[Thread-266,5,main]

// Backgroundから復帰(2回目のonResume)
07-14 08:31:06.983 5497-5518/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread: Thread[Thread-269,5,main]

ここでthreadを新規作成するのではなくmain threadに固定することでこのcrashは避けられる。

    override fun onResume() {
        super.onResume()
// この部分を
//        thread {
//            Timber.d("current thread: ${Thread.currentThread()}")
//            text.text = "hogehoge"
//        }

// こう変える
        runOnUiThread {
            Timber.d("current thread: ${Thread.currentThread()}")
            text.text = "hogehoge"
        }
    }

ログを見たら全てmain threadで固定されていることがわかる。

// Activity起動
07-14 08:07:18.328 5242-5242/com.example.muumuu.playgroundapp D/ThreadActivity: main thread: Thread[main,5,main]
07-14 08:07:18.329 5242-5242/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread: Thread[main,5,main]

// Backgroundから復帰(2回目のonResume)
07-14 08:07:27.811 5242-5242/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread: Thread[main,5,main]

ちなみにもう一つおもしろい挙動を見つけたのでメモ。 こんな感じでthreadとrunOnUiThread両方書いてやるとCrashは発生しない。

    override fun onResume() {
        super.onResume()
        thread {
            Timber.d("current thread from thread: ${Thread.currentThread()}")
            text.text = "hogehoge"
        }

        runOnUiThread {
            Timber.d("current thread from ui thread: ${Thread.currentThread()}")
            text.text = "hogehoge"
        }
    }

ログを見ると別threadよりも先に runOnUiThread の中が実行されているのがわかる。先にmain threadでさわっているので以降main threadのgroupで動いている別threadでさわってもcrashしないということなのかな。 // この部分間違ってそうなので追記で捕捉しました(2018.7.15)

// 1回目のonResume()
07-14 08:40:11.749 5693-5693/? D/ThreadActivity$onResume: current thread from ui thread: Thread[main,5,main]
07-14 08:40:11.749 5693-5709/? D/ThreadActivity$onResume: current threa from thread: Thread[Thread-278,5,main]

// 2回目のonResume()
07-14 08:40:49.488 5693-5693/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread from ui thread: Thread[main,5,main]
07-14 08:40:49.489 5693-5717/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current threa from thread: Thread[Thread-280,5,main]

[2018.7.15ここから追記]

ブログ公開ごにhydrakecatさんにご指摘いただいたのでさらに追加調査。

確かに thread { } の中で Thread.sleep(1000)を入れるとcrashするようになった。 (emulator環境Mashmallow / 実機環境Oreo共にcrashすることを確認 )

    override fun onResume() {
        super.onResume()
        thread {
            text.text = "hogehoge" // sleep以前にtextをさわってもcrashしないが、
            Thread.sleep(1000) // 1000msecの遅延後
            text.text = "hogehoge" // textをさわるとcrashする
        }

例外を投げているのはViewRootImpl.javaのこの部分。

Cross Reference: /frameworks/base/core/java/android/view/ViewRootImpl.java

   7311     void checkThread() {
   7312         if (mThread != Thread.currentThread()) {
   7313             throw new CalledFromWrongThreadException(
   7314                     "Only the original thread that created a view hierarchy can touch its views.");
   7315         }
   7316     }

mThreadに何が入るのかとういうと、constructorでThread.currentThread()を代入している。mThreadはfinalなのでsleep前後でも変わらない。

    235     final Thread mThread;

    478     public ViewRootImpl(Context context, Display display) {
    479         mContext = context;
    480         mWindowSession = WindowManagerGlobal.getWindowSession();
    481         mDisplay = display;
    482         mBasePackageName = context.getBasePackageName();
    483         mThread = Thread.currentThread();

mThreadThread.currentThread() もsleep前後で変化しないはずなのでおかしいなぁと思っていたら

というわけでcheckThread()が呼ばれない可能性を考える。

ViewRootImplmHandlingLayoutInLayoutRequestというフラグを持っていて、Layout実行時にこのフラグがtrueになる。

   1158     @Override
   1159     public void requestLayout() {
   1160         if (!mHandlingLayoutInLayoutRequest) {
   1161             checkThread();
   1162             mLayoutRequested = true;
   1163             scheduleTraversals();
   1164         }
   1165     }

で、requestLayout()にこのフラグが立っていたら checkThread() が実行されない(というか何もしない)ので、threadのcheckも行われず例外も投げられないということのようだ。sleep後はlayoutが完了してフラグがfalseになっているのでthreadのcheckが行われて例外が発生している。

runOnUiThread()thread 両方書いたケースでも、runOnUiThread()の方でlayout要求が出ているのでmHandlingLayoutInLayoutRequestがtrueになり、そのタイミングでthreadでUIをさわったのでcheckThread()が呼ばれず例外が発生しなかっただけだと思われる。

ViewRootImplAndroid FrameworkのクラスなのでDebuggerが効かず本当にそうか?というのが確認できないので歯がゆいが、多分あってるんじゃ無いかなぁ。多分。

[2018.7.15 追記ここまで]

RxTextView#textChanges()でdistinctUntilChangedする時にeventが流れない時がある

みんな大好きRxBinding1 ですが、ちょっとハマることがあったのでメモ。

RxTextView#textChanges()EditText の入力イベントを監視する。その時に重複した入力をdistinctUntilChanged()で削ろうとしたら以降何もeventが流れない現象に遭遇した。

RxTextView.textChanges(editText)
        .distinctUntilChanged() //  ここでevnetが止められるから
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe {
            Log.d("SampleApp", it.toString()) // ここは実行されない
        }

textChanges()は内部ではTextWatcherを使っていて、これがTextの変更を検出するとCharSequenceを返してくれるのだが、こいつはmutableで毎度同じinstanceを返すのだ。同じinstanceなのでchangeしたと見なされずいつまでたっても次のeventが流れてこない。

なので、CharSequenceからStringを取り出してやれば想定していた挙動になる。

RxTextView.textChanges(editText)
        .map { it.toString() } //  ここでStringに変換すると
        .distinctUntilChanged() 
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe {
            Log.d("SampleApp", it.toString()) // ここが実行される
        }

そもそもなんで RxTextView#textChanges() で入力の重複を防ぎたかったかというと、機種依存でSoftware Keyboardのsearch buttonをtapした時にtextChangesのeventがemitされちゃう場合があって、これを防ぎたかったから。これはRxTextViewというよりは、内部で使われているTextWatcherでeventが発火しているからなんだけど、keyboardの機種依存周りつらすぎてなんか仕様とか作って統一して欲しい…


  1. AndroidでReactive Programmingする時に、Viewのイベントをstreamで扱えるのが最大の利点だと思ってしまいそうになるくらいに大好き