CoordinatorLayoutでMotionLayoutアニメーションを実現する(Android)
こんにちは、tkyです。
前回はMotionLayoutを使って実践で使えそうなアニメーションを実装しました。
今回も実践で使えそうなアニメーションを実装してみようと思います。
こちら
画面スクロール時にタイトルのテキストがアニメーションするようなMotionLayoutです。
前回同様のGitHubリポジトリとなります。 Sample10
をご確認ください。
レイアウト
CoordinatorLayoutなのでここは特に変わらずです。AppBarのレイアウト(layout/motion_10_header)が今回MotionLayoutに対応させることになります。
body側はただのTextViewなので特に触れません。GitHub参照ということで。。。
layout/motion_10.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="false"> <com.google.android.material.appbar.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="180dp" android:theme="@style/AppTheme.AppBarOverlay"> <include layout="@layout/motion_10_header" /> </com.google.android.material.appbar.AppBarLayout> <include layout="@layout/motion_10_body" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>
AppBar
これがMotionLayoutのレイアウトになりますが、ポイントは Sample10MotionLayout
というMotionLayoutを継承したカスタムMotionLayoutクラスを使っているというところです。
Sample10MotionLayout
については後述します。
あとは表示したいUIを配置していきます。今回の場合、
- 背景に使うImageView(写真は適当に準備しています)
- タイトルTextView
- サブタイトルTextView
<?xml version="1.0" encoding="utf-8"?> <com.github.ticktakclock.motionlayoutsample.Sample10MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:showPaths="true" android:id="@+id/motionLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:minHeight="50dp" app:layoutDescription="@xml/scene_10" app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"> <ImageView android:id="@+id/background" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" app:srcCompat="@drawable/public_domain_photo_2" tools:layout_editor_absoluteX="0dp" tools:layout_editor_absoluteY="0dp" /> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/sample_10_title" android:textColor="@android:color/white" android:textSize="24sp" tools:layout_editor_absoluteX="173dp" tools:layout_editor_absoluteY="263dp" /> <TextView android:id="@+id/title_sub" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/sample_10_sub_title" android:textColor="@android:color/white" tools:layout_editor_absoluteX="184dp" tools:layout_editor_absoluteY="348dp" /> </com.github.ticktakclock.motionlayoutsample.Sample10MotionLayout>
トリガー
やりたいことは「スクロールしたときにTextViewの位置を変更する」です。
スクロールは”縦のスワイプ”になるので motion:dragDirection="dragUp"
でスクロールを実装します。ターゲットは背景画像にしておきます。
<Transition motion:constraintSetEnd="@id/end" motion:constraintSetStart="@id/start" motion:duration="200" motion:motionInterpolator="linear"> <OnSwipe motion:dragDirection="dragUp" motion:targetId="@id/background" motion:touchAnchorSide="bottom" /> </Transition>
開始
@startにはAppBarが開いている状態の成約を指定していきます。
背景画像は見せておきたいので android:alpha="1"
で表示状態にしておきます。
あとは通常通りタイトルTextViewは中央に来るように、サブタイトルTextViewはタイトルの下に来るように成約を設定します。
<ConstraintSet android:id="@+id/start"> <Constraint android:id="@id/background" android:alpha="1"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintStart_toStartOf="parent" /> </Constraint> <Constraint android:id="@id/title"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" motion:layout_constraintBottom_toBottomOf="@id/background" motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toTopOf="@id/background" /> </Constraint> <Constraint android:id="@id/title_sub"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toBottomOf="@id/title" /> </Constraint> </ConstraintSet>
終了
@endにはAppBarが閉じている状態の成約を指定していきます。
背景画像は見せなくて良いので android:alpha="0"
で表示状態にしておきます。
スクロールした時にタイトルTextViewは左下、サブタイトルTextViewはタイトルの右横に配置します。
<ConstraintSet android:id="@+id/end"> <Constraint android:id="@id/background" android:alpha="0"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/title"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginBottom="8dp" motion:layout_constraintBottom_toBottomOf="@id/background" motion:layout_constraintStart_toStartOf="parent" /> </Constraint> <Constraint android:id="@id/title_sub"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" motion:layout_constraintBottom_toBottomOf="@id/title" motion:layout_constraintStart_toEndOf="@id/title" motion:layout_constraintTop_toTopOf="@id/title" /> </Constraint> </ConstraintSet>
カスタムMotionLayoutについて
ここまでやれば普通であればスクロールでアニメーションできるのですが、残念ながらMotionLayoutクラスではアニメーションできませんでした。
アニメーションする際、MotionLayoutの progress
値が変化することでTransition発生することになりますが、AppBarのレイアウトの縦移動だけではMotionLayoutのこの値が変化することはないようです。
ということでMotionLayoutに AppBarLayout.OnOffsetChangedListener
を実装してAppbarLayout で発生するoffsetをMotionLayoutのprogressに適用させることでTransitionの発生を促すようにチャレンジします。
class Sample10MotionLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : MotionLayout(context, attrs, defStyleAttr), AppBarLayout.OnOffsetChangedListener { override fun onOffsetChanged(appBarLayout: AppBarLayout?, offset: Int) { val scrolled = appBarLayout?.totalScrollRange?.toFloat() progress = -offset / (appBarLayout?.totalScrollRange?.toFloat() ?: 0f) } override fun onAttachedToWindow() { super.onAttachedToWindow() (parent as? AppBarLayout)?.addOnOffsetChangedListener(this) } }
これでスクロールでMotionLayoutのTransitionを効かせる事ができたのですが、もっと良い方法がないか模索したいところです。
まとめ
CoordinatorLayoutとMotionLayoutを組み合わせたアニメーションを実装してみました。
MotionLayoutを拡張してAppBarのoffsetをMotionLayoutのprogressに適用させることでアニメーションを実現しました。
どうしてもカスタムMotionLayoutが必要なのかはわかりませんが、もっと良い方法があれば是非教えていただきたいです。
発展
カスタムMotionLayoutのprogressをうまく使えば様々なケースでMotionLayoutを使えそうなことがわかったのでDrawerLayoutでも適用可能かと思われます。
詳しくはGitHubの Sample11
をご確認ください。
class Sample11MotionLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : MotionLayout(context, attrs, defStyleAttr), DrawerLayout.DrawerListener { override fun onDrawerSlide(drawerView: View, slideOffset: Float) { progress = slideOffset } override fun onDrawerClosed(drawerView: View) { } override fun onDrawerOpened(drawerView: View) { } override fun onDrawerStateChanged(newState: Int) { } override fun onAttachedToWindow() { super.onAttachedToWindow() (parent as? DrawerLayout)?.addDrawerListener(this) } }