Material Design motion system (Beta)とAndroidの実装 (Android Material Component 1.2.0-alpha05時点)


きっかけ

Android Material Component 1.2.0-alpha05がリリースされ、これが入ったので見てみました。

概要

このドキュメントによると、Material Design’s motion systemのUIのコンポーネントや画面の遷移のパターンは4つから構成されます。
https://material.io/design/motion/the-motion-system.html#transition-patterns

  • Container transform
  • Shared axis
  • Fade through
  • Fade

それぞれの説明と実装方法

基本的にはここにあるとおりですが、ちょっとかいつまんで説明します。
https://material.io/develop/android/theming/motion/#material-container-transform

Container transform

共通の要素がある場合の遷移、リストの一行と詳細画面が関連あるなどの場合。
例:
- DroidKaigiのアプリでいうとセッション一覧とセッション詳細。
- セッション詳細にあるスピーカーの情報とスピーカー詳細。

どんなアニメーション?
AndroidでいうとViewGroupなどコンテナになっている部分をSharedElementTransionするイメージ

実装方法

MaterialContainerTransformを利用します。MaterialContainerTransformはAndroidXではなくAndroidのプラットフォームにあるandroid.transition.Transitionを継承しています。
これまでは画像など単体のViewをShared Element Transitionとして共有していましたが、この方法ではViewGroupなどを共有します。

MaterialContainerTransformは具体的にどうやってレイアウトを描画するか?

MaterialContainerTransform.TransitionDrawableというのがMaterialContainerTransformの中にあり、そのcanvasに始まりと終わりのViewをdrawすることによって実現しているようです。結構コード読みやすいので、ハマったらコード読むと解決できます。

※ MaterialContainerTransform.TransitionDrawable内

Fragment AからFragment Bに遷移するときの基本的な実装方法

  • 遷移元フラグメントFragment A transitionNameを遷移元レイアウトにつけ、FragmentNavigatorExtrasで渡して(Navigationを使わないでやる場合は .addSharedElement(view, “shared_element_container”) といった感じでやるみたいです。)、Navigationのextraで渡してあげればOKです。
// 今まではImageViewなどにつけていたが、レイアウトにtransitionNameをつける
binding.root.transitionName =
    "${speaker.id}-${SessionDetailFragment.TRANSITION_NAME_SUFFIX}"
binding.root.setOnClickListener {
    // ViewとTransitionNameの関連付けをextraに入れる。
    val extras = FragmentNavigatorExtras(
        binding.root to binding.root.transitionName
    )
    binding.root.findNavController()
        .navigate(
            SessionDetailFragmentDirections.actionSessionToSpeaker(
                speaker.id,
                SessionDetailFragment.TRANSITION_NAME_SUFFIX,
                null
            ),
            // extraをNavigationで渡す
            extras
        )
}
  • Fragment BのenterTransitionにMaterialContainerTransform()を入れる。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    sharedElementEnterTransition = MaterialContainerTransform(requireContext())
  • 行き先のルートViewなどに同じTransitionNameをつけてあげる。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    val binding = FragmentSpeakerBinding.bind(view)
    binding.speakerRoot.transitionName = "${navArgs.speakerId}-${navArgs.transitionNameSuffix}"

基本的にはMaterialContainerTransformを使うこと以外はレイアウトを指定したShared Element Transitionを実装すればOKです

プルリクで実装を見たければこちらで見られます。
https://github.com/DroidKaigi/conference-app-2020/pull/810/files

ハマりどころ(今回は詳しく書かないです)

  • exit transitionがデフォルトでAndroidXのtransitionになっていて、enter transitionがプラットフォームのtransitionになるのでクラッシュする。両方プラットフォームかAndroidXのtransitionでなくてはならない。
  • アニメーションする先が画面全体ではない場合(DroidKaigiアプリではToolbarがActivityにあった)、drawingViewIdを使って画面の裏がフェードする範囲を変える必要がある。
  • Transition対象のViewにはbackgroundをつけておかないとうまく動かなかったりする。

MaterialContainerTransform.setDrawDebugEnabledを使うことでアニメーションのデバッグができます。

Shared axis

空間やナビゲーションなどで位置関係があるUI要素での切り替え。X Y Z軸でスライドさせるような形で利用でき、要素間の関係を強化できる。
例:
- チュートリアルで下にカルーセルがあって、押したときに横にスクロールされて切り替えるなど。並列の要素で位置関係があるもの。

MaterialSharedAxisというTransitionを利用します。

詳しくはドキュメントやこのあたりの実装を見ていくことで実装できそうです。
https://material.io/develop/android/theming/motion/#shared-axis
https://github.com/material-components/material-components-android/blob/8e8d20c94f3573f7f7418f3971fbbc41a4fa83bd/catalog/java/io/material/catalog/transition/TransitionSharedAxisDemoFragment.java#L101

Fade through

お互いに関係がないUI要素間での切り替えに利用します。BottomNavigationでの切り替えなどに利用します。

MaterialFadeThroughというTransitionを利用します。

詳しくはドキュメントやこのあたりの実装を見ていくことで実装できそうです。
https://material.io/develop/android/theming/motion/#shared-axis
https://github.com/material-components/material-components-android/blob/8e8d20c94f3573f7f7418f3971fbbc41a4fa83bd/catalog/java/io/material/catalog/transition/TransitionSharedAxisDemoFragment.java#L101

Fade

例えばFabが表示されたり、Dialogが表示されたり消えたりと言った。UI要素が表示されるときに利用するパターンのようです。

MaterialFadeというTransitionを利用します。
https://material.io/develop/android/theming/motion/#fade
https://github.com/material-components/material-components-android/blob/8e8d20c94f3573f7f7418f3971fbbc41a4fa83bd/catalog/java/io/material/catalog/transition/TransitionFadeDemoFragment.java#L62

まとめ

新しいMaterial Design motion systemではアニメーションの種類がかなり絞られて、また実装的にも簡単にできるように工夫されており、Transitionでハマって時間をめちゃくちゃ吸われるようなことが少なくなりました。
Jetpack Composeにも将来的には同じような仕組みが入るのかなと予想できますが待ってみましょう。