【Android】MotionLayoutを触ってみる

# MotionLayoutとは

Viewのアニメーションを管理できるViewGroupです。 

ConstraintLayoutのサブクラスで、ConstraintLayoutを利用しているレイアウトなら、まるっとMotionLayoutに置き換えることができます。

※MotionLayoutはAPIレベル14との下位互換性があります。

# 導入

gradleに依存関係を追加します。
MotionLayoutは `ConstraintLayoutのライブラリの2.0.0から導入` されたものですので、ConstraintLayoutのバージョンを上げる必要があります。
※2020/3/5の時点での最新バージョンは2.0.0-beta4でした。

```build.gradle(app)
// AndroidXを導入している場合はこちら
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'

// それ以外はこちら
implementation 'com.android.support.constraint：constraint-layout：2.0.0-beta4'
```

MotionLayoutで利用するアニメーションファイル※1を入れるためのXMLフォルダ※2を作成します。

※1 厳密にはアニメーション開始時と終了時のViewの状態を記述したファイルです。
※2 AndroidStudioのメニューから、File > New > Folder > XML Resource Folderで作成。

# 触ってみる

凝ったレイアウトを作る時間がないので、デフォルトの `activity_main.xml` にMotionLayoutを適応してみます。

```activity_main.xml
<!-- ConstraintLayoutからMotionLayoutに置き換えました。 -->
<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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    >

    <!-- アニメーションさせたいViewにidをつけます。 -->
    <TextView
        android:id="@+id/main_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />

</androidx.constraintlayout.motion.widget.MotionLayout>
```

次に `res/xml/` にアニメーションファイルを作成します。

```res/xml/motion_scene_main.xml
<!-- MotionScene※3タグで大枠を作ります。xmlns:motion="~"を忘れずに。 -->
<MotionScene
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto"
    >

    <!-- 開始、終了アニメーションの指定と実行時間を記述します。 -->
    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start"
        motion:duration="1000"
        >
        <!-- トリガーとなるアクションとViewのidを紐付けます。 -->
        <OnClick motion:targetId="@id/main_text_view"/>
    </Transition>

    <!-- アニメーション開始時のViewの状態を記述します。 -->
    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/main_text_view"
            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"
            motion:layout_constraintTop_toTopOf="parent"
            />
    </ConstraintSet>

    <!-- アニメーション終了時のViewの状態を記述します。 -->
    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/main_text_view"
            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"
            />
    </ConstraintSet>

</MotionScene>
```

※3 xmlns:motion="~"を含めれば、MotionSceneという命名でなく、どのような名前を付けても認識してくれます。

最後に、MotionLayoutとアニメーションファイルを紐づけてやります。

```activity_main.xml
<androidx.constraintlayout.motion.widget.MotionLayout

    〜 省略 〜

    app:layoutDescription="@xml/motion_scene_main"
    >
```


端末で確認するとしてみます。
テキストをタップするとしっかりアニメーションしていますね。
![20200305.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252756/aa7b7183-5798-6b72-2e07-2cd0515bc385.gif)

MotionLayoutはView自体をアニメーションさせるだけでなく、Viewのパラメータを動的に変更することもできます。
アニメーションファイルに少し手を加えてみます。

```res/xml/motion_scene_main.xml
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto"
    >

    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start"
        motion:duration="1000"
        >
        <OnClick motion:targetId="@id/main_text_view" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/main_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent"
            >
            <!-- アニメーション開始時のテキストの色を指定します。 -->
            <CustomAttribute
                motion:attributeName="textColor"
                motion:customColorValue="#ff0000"
                />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/main_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            >
            <!-- アニメーション終了時のテキストの色を指定します。 -->
            <CustomAttribute
                motion:attributeName="textColor"
                motion:customColorValue="#0000ff"
                />
        </Constraint>
    </ConstraintSet>

</MotionScene>
```

![20200305_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252756/c8035c8d-573c-bd81-fd5c-49c1b03b5176.gif)

`CustomAttribute` というタグは、attributeNameに指定したViewのパラメータをアニメーションに合わせて変更してくれます。
色を変える他に、アニメーションに合わせてViewを変形させたり、画像をクロスフェードさせたりできます。
CustomAttributeを複数指定することももちろん可能です。

ここでは特に触れないので気になった方は調べてみてください。

### 追記１
上記ではアニメーションファイルにトリガーとなるViewを紐づけていましたが、実際のプロジェクトでは、コードで指定する方が一般的かと思います。

```res/xml/motion_scene_main.xml
<!-- ここは削除して構いません。 -->
<OnClick motion:targetId="@id/main_text_view" />
```

```MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val motionLayout = findViewById<MotionLayout>(R.id.main_motion_layout)
        // トリガーとなるViewのクリック時に、指定したid※4のアニメーションを開始します。
        findViewById<TextView>(R.id.main_text_view).setOnClickListener {
            // 現在のアニメーションの状態を判定して、開始するアニメーションを決めます。
            if (motionLayout.currentState == R.id.start) {
                motionLayout.transitionToEnd()
            } else {
                motionLayout.transitionToStart()
            }
        }
    }
}
```

※4 アニメーションファイルで定義したConstraintSetのidです。

### 追記２
要件によってはアニメーションの進行状況を検知したい場合があると思います。
そんな時は、 `MotionLayout.TransitionListener` をセットすればコールバックが得られます。

```MainActivity.kt
motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
    override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {
        // トリガーした時に呼ばれます。
    }

    override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {
        // アニメーション開始時に呼ばれます。
    }

    override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {
        // アニメーション中に呼ばれます。
    }

    override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
        // アニメーション終了時に呼ばれます。
    }
})
```

# 感想

MotionLayoutの魅力は、ほぼ既存の実装に影響せず、線形補完の滑らかなアニメーションで手軽にリッチな表現ができるところにあると思いました。また、学習コストが比較的低いのも◎。


# 参考
https://developer.android.com/training/constraint-layout/motionlayout