MotionLayoutでアニメーションさせるイメージを掴む

[Life is Tech ! #2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/life-is-tech-2) 1日目の記事です🎉

## はじめに
- ゴディバです！
- 2020 LiT!#2アドベントカレンダー1日目はAndroidの記事になります！
- これを読めば誰でもMotionLayoutを使えるようになることを目指しました！
- いまさらMotionLayout・・・？
    - **はい！！！**

## なにやんの
- ようやく`ConstraintLayout 2.0`が安定版になったこともあり、改めてMotionLayoutを使ったアニメーションの簡単なAndroidサンプルアプリを作ります
- 「**え、これでアニメーションできるの。ちょっとMotionLayout使ってやろうかな**」って思ってくれたら僕の勝ちです

## MotionLayoutとは
- `ConstraintLayout 2.0.0`から使えるようになったレイアウト
- 得意技はアニメーションとそのモーションの管理
- `ConstraintLayout`のサブクラスなので基本的に`ConstraintLayout`と同じように使えます
- 詳しくは[こちら](https://developer.android.com/training/constraint-layout/motionlayout?hl=ja)

## 超ざっくりしたイメージ
- アニメーション前のレイアウトとアニメーション後のレイアウトを作ると、その間のアニメーションを勝手にやってくれる凄いやつです
- なのでやることとしては**レイアウトを２つ作る**イメージです

## 実装！
### 依存追加
- `2.0.1`でも使えますが、諸々のバグが修正された`2.0.4`でいきます

```build.gradle
dependencies {
...
    androidx.constraintlayout:constraintlayout:2.0.4
}
```

### 該当レイアウトのGroupを変更
- アニメーションさせたいレイアウトのroot-viewを`MotionLayoutに変えます`
- とりあえずView２つくらい置いときます
- エラー出ると思いますが、後ほど作るモーションファイルを指定してないだけなので慌てなくて大丈夫です

```activity_main.xml
<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:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/black"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#444444"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/red" />

    <View
        android:id="@+id/red"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#cc4444"
        app:layout_constraintBottom_toTopOf="@id/black"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

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

### MotionSceneを作成
- `/res/xml/scene_main.xml`みたいなレイアウトファイルを作成します (名前は何でもいいです)
- このレイアウトファイルでアニメーション前とアニメーション後のレイアウトをそれぞれ作るイメージです

```scene_main.xml
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="1000">
        <OnSwipe
            app:dragDirection="dragUp"
            app:touchAnchorId="@+id/black"
            app:touchAnchorSide="top" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/black"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/red" />

        <Constraint
            android:id="@+id/red"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toTopOf="@id/black"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/black"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toTopOf="@id/red"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@+id/red"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/black" />
    </ConstraintSet>

</MotionScene>
```

- こんなレイアウトを作ってます
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126115/de877963-813f-3faf-fba9-4ff2f3ffb128.png" />

#### 解説
- `<Transition>`
    - レイアウトの動きを指定します
    - 後述するレイアウトの状態のidを開始と終了でそれぞれ指定してます
    - `duration`は言わずもがな、アニメーション時間を指定してます(ms)
- `<OnSwipe>`
    - ユーザの動きによってアニメーション開始させたい時に使います
    - どのViewをどの方向にスワイプした時にアニメーションさせるかを指定しています
    - もちろんコードからも動かせるので、**無くても大丈夫です**

```scene_main.xml
...
<Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="1000">
        <OnSwipe
            app:dragDirection="dragUp"
            app:touchAnchorId="@+id/black"
            app:touchAnchorSide="top" />
    </Transition>
...
```

- `<ConstraintSet>`
    - アニメーションでのレイアウトの状態をまとめます
    - ここで指定したidを`<Transition>`の`constraintSetStart`/`constraintSetEnd`で指定します
    - 各`ConstraintSet`タグで囲まれてる部分でアニメーション開始時/終了時それぞれのレイアウトを組むイメージです

```scene_main.xml
...
<ConstraintSet android:id="@+id/start">
...
```

- `<Constraint>`
    - レイアウトの状態を指定します
    - ここで指定するidでレイアウト上の要素が紐付きます
    - id以外の要素を指定することでstart/endでアニメーションを補完してくれます
        - width, height
        - margins
        - ConstraintLayoutの各種制約 (`layout_constraintBottom_toBottomOf`とか)
        - alpha
        - visibility
        - elevation
        - rotation, rotationX, rotationY
        - translationX, translationY, translationZ
        - scaleX, scaleY

```scene_main.xml
...
<Constraint
            android:id="@+id/black"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/red" />
...
```

### 作成したMotionSceneをレイアウトに設定
- `MotionLayout`の`layoutDescription`で作成したレイアウトファイルを指定します

```activity_main.xml
<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:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/scene_main">
...
```

### 実行
- 黒のViewをスワイプで赤のViewと入れ替わるようになったかと思います
- `scene_main`でそれぞれの`constraintBottom_toTopOf`と`constraintTop_toBottomOf`の制約を入れ替えたのでその間が補完されてアニメーションするようになってます

## 発展！
### 赤Viewもスワイプできるようにしたい
- モーションのトリガーが増えるので、赤Viewスワイプ用の`<Transition>`を増やします
- 今回はアニメーション開始時に赤Viewが上にあるので下スワイプ時にアニメーションするように`dragDirection`と`touchAnchorSide`を設定します

```scene_main.xml
...
<Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="1000">
        <OnSwipe
            app:dragDirection="dragDown"
            app:touchAnchorId="@+id/red"
            app:touchAnchorSide="bottom" />
</Transition>
...
```

### コードから動かしたい
- ここまでレイアウトファイル完結でアニメーションさせましたが、実際のところコードで別の処理を走らせつつアニメーションさせることがほとんどかと思います
- `MotionLayout.transitionToEnd()`で start -> end のアニメーション、`MotionLayout.transitionToStart()`でその逆を行うことができます
- ほかにもいろいろ知りたいよって人は[ここ](https://developer.android.com/reference/androidx/constraintlayout/motion/widget/MotionLayout)見るか自分でやってみて、良さげなのあったら記事にでもしてください

```MainActivity.kt
class MainActivity : AppCompatActivity() {
    private var flg = true

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        root.setOnClickListener {
            if (flg) {
                root.transitionToEnd()
            } else {
                root.transitionToStart()
            }
            flg = !flg
        }
    }
}
```

- `OnClickListener`も設定する場合は`<Transition>`に`<OnSwipe>`を設定してるとタッチイベントを取られてしまうので取り除いてください
- ちなみにConstraintLayoutの[バグレポートの回答](https://issuetracker.google.com/issues/163573420)ではプログラムでいい感じにやってねと言われてるので当たり前ですがこれらのタッチイベントが手を取り合うことはないと思います

```scene_main.xml
...
<Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="1000" />
...
```

## 余談
- どのアニメーションにも言えることですが、細かいパーツに分けて考えるとアニメーションさせやすいですね
- 例えば某ロゴはこんな感じにパーツ分けすると作りやすいかと思います ~~(これFigmaでつくってる時が一番楽しかった)~~
![part_logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126115/ddb01792-d842-98f9-e228-dfde0efe6e14.png)

- これも２つのレイアウトを作るだけでいいので、MotionLayoutで簡単にアニメーションさせられますね
- ロゴのレイアウトが出来ていれば、アニメーション開始時のレイアウトを組み替えるだけでロゴのレイアウトに組み変わるアニメーションを自動でやってくれます

| 正方形 | 複数ブロック | 外枠 |
|:-----------:|:------------:|:------------:|
|<img width=200 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126115/683c3951-ed38-ca7e-21b0-6e6346d66249.gif"/>|<img width=200 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126115/578056f6-ef60-cff1-cfc4-2290770d83b3.gif"/>|<img width=200 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126115/3708bdc4-1a2d-6b20-311e-3f84d7c2a464.gif"/>|


## おわりに
- レイアウトを２つ作ればアニメーションさせられると考えると、いろいろなことが簡単に出来そうですね
- ConstraintLayout 2.0が安定版になり、バグも修正されてきているのでこれを期に是非やってみてください！