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

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()