ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

MotionLayoutでFAB Speed dialを実現する

こんにちはtkyです。

前回 、MotionLayoutでアニメーションさせる(Android) - ticktakclockの日記にてMotionLayoutについて学習しました。

今回はこのMotionLayoutを利用してFABメニューにおけるスピードダイアル(Speed dial)を実現してみたいと思います。

これですね。

f:id:ticktakclock:20191231003938g:plain:w200
デモ

実際のコードは前回のGitHubと同じです。 Sample9 をご確認ください。

github.com

レイアウト

まずはlayout.xmlにパーツを定義していきます。ツリー的にはこのようにになります。

f:id:ticktakclock:20191231000743p:plain
layoutツリー

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の上に配置するように指定します

f:id:ticktakclock:20191231001309p:plain:w200
サンプル

    <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アニメーションを実装してみようと思います。

次回

ticktakclock.hatenablog.com

参考:ありがとうございます!

Android MotionLayout概論 - Eureka Engineering - Medium