MotionLayoutでFAB Speed dialを実現する
こんにちはtkyです。
前回 、MotionLayoutでアニメーションさせる(Android) - ticktakclockの日記にてMotionLayoutについて学習しました。
今回はこのMotionLayoutを利用してFABメニューにおけるスピードダイアル(Speed dial)を実現してみたいと思います。
これですね。
実際のコードは前回のGitHubと同じです。 Sample9
をご確認ください。
レイアウト
まずはlayout.xmlにパーツを定義していきます。ツリー的にはこのようにになります。
1つViewを設置(@+id/viewのこと)していますが、これは背景をつけるために配置しています。Gif上で赤色の背景にあたる部分です。
layout/motion_09.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.motion.widget.MotionLayout 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" app:layoutDescription="@xml/scene_09"> <View android:id="@+id/view" android:layout_width="wrap_content" android:layout_height="wrap_content" tools:layout_editor_absoluteX="0dp" tools:layout_editor_absoluteY="0dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@android:drawable/ic_menu_add" app:fabCustomSize="48dp" tools:layout_editor_absoluteX="339dp" tools:layout_editor_absoluteY="659dp" /> <TextView android:id="@+id/sub_text1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="camera" tools:layout_editor_absoluteX="200dp" tools:layout_editor_absoluteY="553dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/sub_btn1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="#3F51B5" android:clickable="true" android:src="@android:drawable/ic_menu_camera" app:fabCustomSize="32dp" tools:layout_editor_absoluteX="332dp" tools:layout_editor_absoluteY="560dp" /> <TextView android:id="@+id/sub_text2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="call phone" tools:layout_editor_absoluteX="297dp" tools:layout_editor_absoluteY="714dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/sub_btn2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="#03A9F4" android:clickable="true" android:src="@android:drawable/ic_menu_call" app:fabCustomSize="32dp" tools:layout_editor_absoluteX="363dp" tools:layout_editor_absoluteY="723dp" /> <TextView android:id="@+id/sub_text3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="search" tools:layout_editor_absoluteX="297dp" tools:layout_editor_absoluteY="698dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/sub_btn3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="#FF9800" android:clickable="true" android:src="@android:drawable/ic_menu_search" app:fabCustomSize="32dp" tools:layout_editor_absoluteX="368dp" tools:layout_editor_absoluteY="691dp" /> <TextView android:id="@+id/sub_text4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="share social" tools:layout_editor_absoluteX="297dp" tools:layout_editor_absoluteY="698dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/sub_btn4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="#4CAF50" android:clickable="true" android:src="@android:drawable/ic_menu_share" app:fabCustomSize="32dp" tools:layout_editor_absoluteX="368dp" tools:layout_editor_absoluteY="691dp" /> <TextView android:id="@+id/sub_text5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="setting" tools:layout_editor_absoluteX="297dp" tools:layout_editor_absoluteY="698dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/sub_btn5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="#9C27B0" android:clickable="true" android:src="@android:drawable/ic_menu_manage" app:fabCustomSize="32dp" tools:layout_editor_absoluteX="368dp" tools:layout_editor_absoluteY="691dp" /> </androidx.constraintlayout.motion.widget.MotionLayout>
量は多いですが、1つ1つはただのButtonとTextViewです。パーツを定義したので xml/scene_09.xml
に開始/終了の成約を設定してどのようにアニメーションするかを定義していきましょう。
トリガー、開始成約、終了成約の順にポイントを解説していきます。
今回すべて1つのxml内に記述していますが、開始と終了成約については別xmlで定義したほうが可読性が上がるので本番投入する際は分けると良いのではないかと思われます。
トリガー
やりたいことは「FABボタンが押された時にメニューを開く/閉じる」です。 Transitionにはfabがクリックされた時に成約をトグルできるようにOnClickを配置します。
<Transition motion:constraintSetStart="@id/start" motion:constraintSetEnd="@id/end" motion:duration="200"> <OnClick motion:targetId="@id/fab" motion:clickAction="toggle" /> </Transition>
開始
@startにはメニューが閉じている状態の成約を指定していきます。
- 背景の@viewは特にすることはないので透過させておきます
- メニューの各ボタンも表示する必要はないので
android:visibility="invisible"
を指定して非表示状態とします - アニメーション自体は上下のみで、メニューのFAB(@sub_btn1~@sub_btn5)の下の成約を
motion:layout_constraintBottom_toBottomOf="parent"
で親につけておきます
<ConstraintSet android:id="@+id/start"> <Constraint android:id="@id/view"> <Layout android:layout_width="0dp" android:layout_height="0dp" motion:layout_constraintTop_toTopOf="parent" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintEnd_toEndOf="parent" /> <CustomAttribute motion:attributeName="BackgroundColor" motion:customColorValue="#00FFFFFF" /> </Constraint> <Constraint android:id="@id/fab"> <Layout android:layout_width="48dp" android:layout_height="48dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintBottom_toBottomOf="parent" /> <CustomAttribute motion:attributeName="Rotation" motion:customFloatValue="0" /> </Constraint> <Constraint android:id="@id/sub_btn1" android:visibility="invisible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/fab" motion:layout_constraintEnd_toEndOf="@id/fab" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/sub_text1" android:visibility="invisible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn1" motion:layout_constraintBottom_toBottomOf="@id/sub_btn1" motion:layout_constraintTop_toTopOf="@id/sub_btn1" /> </Constraint> <Constraint android:id="@id/sub_btn2" android:visibility="invisible"> <Layout android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn1" motion:layout_constraintEnd_toEndOf="@id/sub_btn1" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/sub_text2" android:visibility="invisible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn2" motion:layout_constraintBottom_toBottomOf="@id/sub_btn2" motion:layout_constraintTop_toTopOf="@id/sub_btn2" /> </Constraint> <Constraint android:id="@id/sub_btn3" android:visibility="invisible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn2" motion:layout_constraintEnd_toEndOf="@id/sub_btn2" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/sub_text3" android:visibility="invisible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn3" motion:layout_constraintBottom_toBottomOf="@id/sub_btn3" motion:layout_constraintTop_toTopOf="@id/sub_btn3" /> </Constraint> <Constraint android:id="@id/sub_btn4" android:visibility="invisible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn3" motion:layout_constraintEnd_toEndOf="@id/sub_btn3" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/sub_text4" android:visibility="invisible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn4" motion:layout_constraintBottom_toBottomOf="@id/sub_btn4" motion:layout_constraintTop_toTopOf="@id/sub_btn4" /> </Constraint> <Constraint android:id="@id/sub_btn5" android:visibility="invisible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn4" motion:layout_constraintEnd_toEndOf="@id/sub_btn4" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/sub_text5" android:visibility="invisible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn5" motion:layout_constraintBottom_toBottomOf="@id/sub_btn5" motion:layout_constraintTop_toTopOf="@id/sub_btn5" /> </Constraint> </ConstraintSet>
終了
@endにはメニューが開いている状態の成約を指定していきます。
- 背景はViewのBackgroundColor属性を変更するためにCustomAttributeで色を指定します
- メニューのボタンたちは今度は表示する必要があるので
android:visibility="visible"
を指定して表示状態とします - メニューの各FABは例えば@sub_btn5(一番上のFAB)では
motion:layout_constraintBottom_toTopOf="@id/sub_btn4"
というように1つ下のFABの上に配置するように指定します
<ConstraintSet android:id="@+id/end"> <Constraint android:id="@id/view"> <Layout android:layout_width="0dp" android:layout_height="0dp" motion:layout_constraintTop_toTopOf="parent" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintEnd_toEndOf="parent" /> <CustomAttribute motion:attributeName="BackgroundColor" motion:customColorValue="#5CEC9D9D" /> </Constraint> <Constraint android:id="@id/fab"> <Layout android:layout_width="48dp" android:layout_height="48dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintBottom_toBottomOf="parent" /> <CustomAttribute motion:attributeName="Rotation" motion:customFloatValue="45" /> </Constraint> <Constraint android:id="@id/sub_btn1" android:visibility="visible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/fab" motion:layout_constraintEnd_toEndOf="@id/fab" motion:layout_constraintBottom_toTopOf="@id/fab" /> </Constraint> <Constraint android:id="@id/sub_text1" android:visibility="visible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn1" motion:layout_constraintBottom_toBottomOf="@id/sub_btn1" motion:layout_constraintTop_toTopOf="@id/sub_btn1" /> </Constraint> <Constraint android:id="@id/sub_btn2" android:visibility="visible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn1" motion:layout_constraintEnd_toEndOf="@id/sub_btn1" motion:layout_constraintBottom_toTopOf="@id/sub_btn1" /> </Constraint> <Constraint android:id="@id/sub_text2" android:visibility="visible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn2" motion:layout_constraintBottom_toBottomOf="@id/sub_btn2" motion:layout_constraintTop_toTopOf="@id/sub_btn2" /> </Constraint> <Constraint android:id="@id/sub_btn3" android:visibility="visible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn2" motion:layout_constraintEnd_toEndOf="@id/sub_btn2" motion:layout_constraintBottom_toTopOf="@id/sub_btn2" /> </Constraint> <Constraint android:id="@id/sub_text3" android:visibility="visible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn3" motion:layout_constraintBottom_toBottomOf="@id/sub_btn3" motion:layout_constraintTop_toTopOf="@id/sub_btn3" /> </Constraint> <Constraint android:id="@id/sub_btn4" android:visibility="visible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn3" motion:layout_constraintEnd_toEndOf="@id/sub_btn3" motion:layout_constraintBottom_toTopOf="@id/sub_btn3" /> </Constraint> <Constraint android:id="@id/sub_text4" android:visibility="visible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn4" motion:layout_constraintBottom_toBottomOf="@id/sub_btn4" motion:layout_constraintTop_toTopOf="@id/sub_btn4" /> </Constraint> <Constraint android:id="@id/sub_btn5" android:visibility="visible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn4" motion:layout_constraintEnd_toEndOf="@id/sub_btn4" motion:layout_constraintBottom_toTopOf="@id/sub_btn4" /> </Constraint> <Constraint android:id="@id/sub_text5" android:visibility="visible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn5" motion:layout_constraintBottom_toBottomOf="@id/sub_btn5" motion:layout_constraintTop_toTopOf="@id/sub_btn5" /> </Constraint> </ConstraintSet>
全体
長いのでここを見てください。
https://github.com/ticktakclock/MotionLayoutSamaple/blob/master/app/src/main/res/xml/scene_09.xml
まとめ
MotionLayoutを使ってxmlのみでアニメーションを実現しました。
MotionLayoutのポイントはボタンクリック時のアニメーションも含めてxmlで定義して実現できるのが良いかな、と思います。
このようにすることでボタンクリック時に本当にやりたい処理のみktファイルに記述することができて全体的に見通しの良いコードになりそうです。
次はCoordinatorLayoutと組み合わせてMotionLayoutアニメーションを実装してみようと思います。
次回
参考:ありがとうございます!