MotionLayoutのサンプルコードから学ぶMotion Editor

この記事は[コネヒト Advent Calendar 2019](https://qiita.com/advent-calendar/2019/connehito) 3日目の記事になります。

## はじめに
[Android Studio 4.0 Canary 4](https://androidstudio.googleblog.com/2019/11/android-studio-40-canary-4-available.html)に新機能として導入されたMotion Editorについて調べてみたので、公開されているMotionLayoutのサンプルコードをベースに、どのように利用されているのか紹介します。

## MotionLayoutとは？
- ConstraintLayout 2.0に含まれている機能
- 始まりと終わりのレイアウトをXMLで定義することによって簡単にアニメーションを実装することができる

## Motion Editorとは？
- Android Studio 4.0 CanaryからMotion Editor導入された
- IDE内でViewの関係性やアニメーションを確認、設定できる

## サンプルコードの紹介
今回利用したサンプルコードはPiotrPrusさんの[MotionLayoutPlayground](https://github.com/PiotrPrus/MotionLayoutPlayground)リポジトリです。こちらのリポジトリでは、いくつかMotionLayoutのサンプルがありますが、「scene13(RV with ML)」のTinder風アニメーションのMotionLayoutについて紹介します。まずは以下のキャプチャを見て挙動を確認しましょう。

![motionlayout.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/3004a36a-50e6-4f65-c1ac-ab0aa6c140ba.gif)

グリッドリスト上に複数のCardViewがあり、そのCardViewを左にスワイプする(like)と、リストから削除されます。反対に右にスワイプする(dislike)と先ほどと同様にリストから削除されるようにMotionLayoutで実装されています。

## サンプルコードの解説
対象画面のコード一式は下記の通りです。

- [Scene13Fragment.kt](https://github.com/PiotrPrus/MotionLayoutPlayground/blob/master/app/src/main/java/com/piotrprus/motionlayoutplayground/fragments/Scene13Fragment.kt) Like/DisLikeできるグリッドリスト画面
- [MySceneGridItemRecyclerViewAdapter.kt](https://github.com/PiotrPrus/MotionLayoutPlayground/blob/master/app/src/main/java/com/piotrprus/motionlayoutplayground/helper/MySceneGridItemRecyclerViewAdapter.kt) グリッドリスト用のアダプター
- [fragment_scene13.xml](https://github.com/PiotrPrus/MotionLayoutPlayground/blob/master/app/src/main/res/layout/fragment_scene13.xml) Fragmentのレイアウト
- [item_scene_13_grid.xml](https://github.com/PiotrPrus/MotionLayoutPlayground/blob/master/app/src/main/res/layout/item_scene_13_grid.xml) グリッドリストのレイアウト
- [ml_scene_13.xml](https://github.com/PiotrPrus/MotionLayoutPlayground/blob/master/app/src/main/res/xml/ml_scene_13.xml) MotionSceneのレイアウト

item_scene_13_grid.xmlの中身を見てみると、Viewの要素にCardViewとそれに対応したlike、dislike用のオーバーレイが配置されています。

```xml:item_scene_13_grid.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"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:clipChildren="false"
    android:clipToPadding="false"
    app:layoutDescription="@xml/ml_scene_13">

    <!-- CardView -->
    <com.google.android.material.card.MaterialCardView
        android:id="@+id/cardViewScene13"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorGrey"
        app:cardBackgroundColor="@color/colorBlue"
        app:cardCornerRadius="10dp"
        app:cardElevation="4dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/item_number"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:textAppearance="?attr/textAppearanceListItem"
                tools:text="1" />

            <TextView
                android:id="@+id/content"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:textAppearance="?attr/textAppearanceListItem"
                tools:text="test" />
        </LinearLayout>
    </com.google.android.material.card.MaterialCardView>

    <!-- Like用のオーバーレイ -->
    <View
        android:id="@+id/cardScene13LikeOverlay"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/frame_overlay_like"
        app:layout_constraintBottom_toBottomOf="@id/cardViewScene13"
        app:layout_constraintEnd_toEndOf="@id/cardViewScene13"
        app:layout_constraintStart_toStartOf="@id/cardViewScene13"
        app:layout_constraintTop_toTopOf="@id/cardViewScene13" />

    <!-- Dislike用のオーバーレイ -->
    <View
        android:id="@+id/cardScene13DislikeOverlay"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/frame_overlay_dislike"
        app:layout_constraintBottom_toBottomOf="@id/cardViewScene13"
        app:layout_constraintEnd_toEndOf="@id/cardViewScene13"
        app:layout_constraintStart_toStartOf="@id/cardViewScene13"
        app:layout_constraintTop_toTopOf="@id/cardViewScene13" />
</androidx.constraintlayout.motion.widget.MotionLayout>
```

### Motion Editorで処理の流れを見る
では、MotionLayoutの構成を見てみましょう。

<img width="1526" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f32333339312f37323063313430372d636338312d336136322d363431312d3437613231666330613363662e706e67.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/aa2a021f-4517-ac72-ea32-2c15d0191d47.png">

グリッドリスト部分に当たる、item_scene_13_grid.xmlをMotion Editorを開いたキャプチャです。これまではXMLで書いていたものが、GUIで表示、設定できるようになりました！Motion Editorを見てみると、5つのConstraintSetで構成されています。

- rest : 初期状態
- like : いいね
- goneRight : 右に消える
- dislike : 好まない
- goneLeft : 左に消える

ConstraintSetとは、Viewの状態を定義でき、例えば初めのViewの状態と終わりのViewの状態やアニメーションを定義することができます。これら複数のConstraintSetをどのように組み合わせてlike、dislikeの処理を行なっているのでしょうか？それぞれ確認してみたい思います。

### like処理
#### ConstraintSet rest
CardViewを表示する部分に当たるConstraintSet restを選択すると、ConstraintSetの中に3つのConstraintが定義されていて、cardViewScene13、cardScene13LikeOverlay、cardScene13DislikeOverlayのConstraintがあります。さらにConstraint cardViewScene13を選択すると、それぞれプロパティが設定されています。

<img width="1124" alt="スクリーンショット 2019-12-01 20.47.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/179776c0-08fc-6f25-1fc3-fe6a2be53132.png">

ConstraintSet restを矢印がlikeとdislikeのConstraintSetに設定されています。まずはlikeの矢印を見てみますが、矢印はTransitionを表します。Transitionを再生すると右に回転していくことがわかります。

<img width="1103" alt="スクリーンショット 2019-12-01 19.30.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/626fd26d-bc3d-42f4-5ce3-24a447cbb413.png">

ちなみにrestからlikeのTransition上のマークはスワイプやクリックイベントを表すマークとなっています。
![スクリーンショット 2019-12-03 8.38.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/1514ac3f-7869-18b8-3fde-5a0885801c70.png)

#### ConstraintSet like
次にいいねの処理に当たる、ConstraintSet likeについて見てみます。CardViewが回転されている状態であることがわかります。

<img width="1104" alt="スクリーンショット 2019-12-01 19.43.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/5bcfda1f-1439-8e87-0a36-eab39605c7e2.png">

Constraint cardViewScene13で右にローテーションしていることがわかります。
<img width="1128" alt="スクリーンショット 2019-12-01 21.28.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/e1328a78-0f88-e9cc-531f-a59cda587c8a.png">

次にTransitionがlikeからgoneRightへと定義されており、Transitionを再生してみるとViewが画面外に移動し見えなくなることがわかります。autoTransitionプロパティにanimateToEndが設定されていますが、これによってlikeからgoneRight状態に自動的にアニメーション化されます。
<img width="1105" alt="スクリーンショット 2019-12-01 19.47.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/81cc6ca4-dcae-6306-e043-bf8f9f169e12.png">
 
#### ConstraintSet goneRight
ここまでrestからlike、goneRightまでConstraintSetを組み合わせてアニメーションを実装していることがわかりました。ConstraintSet goneRightの中にはConstraintとして、cardViewScene13などが定義されており、左のプレビューからcardViewScene13が画面でかつalpha0になっていることがわかります。このようにして右スワイプしてCardViewが消えるアニメーションを実装されています。

<img width="1124" alt="スクリーンショット 2019-12-01 21.33.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/c8d96bad-f5bf-907e-b95f-5bc1e2f7c58c.png">

以上がlike処理になります。restなどのConstraintSet以下に破線がありますが、これはConstraintSetのderiveConstraintsFromプロパティを利用すると表示されます。likeの場合はrestから派生しているという意味になります。リストから削除されている部分については、コード側で実装されているため別途後述します。次にdislike処理についても確認します。

### dislike処理
#### ConstraintSet dislike
likeと同様にConstraintSetでrestからdislikeが定義されています。左スワイプをされるとローテートされた状態がConstraintSet dislikeです。さらにConstraintSet dislikeのcardViewScene13を見てみると左向きにローテーションされているのがわかります。

<img width="1122" alt="スクリーンショット 2019-12-01 22.59.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/68cecd6c-2458-6200-14c2-5f696aa32a0a.png">

次にTransitionを見てみるとConstraintSet dislikeからConstraintSet goneLeftに設定されているのがわかります。
<img width="1124" alt="スクリーンショット 2019-12-01 23.04.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/3f8d4cce-685b-a29a-3988-36ace46239d7.png">

#### ConstraintSet goneLeft
rest -> dislike -> goneLeft ConstraintSetとTransitionが定義されており、goneLeftでは、alpha:0で透過し、rotation:-70、layout_marginEnd:600dpで画面外にViewが移動していることがわかります。ここまでがdislikeのアニメーションになります。

<img width="1124" alt="スクリーンショット 2019-12-01 23.06.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23391/44c55bb2-52ef-f885-9e3c-c59246fcbbe4.png">

### like、dislike後のリスト削除
likeとdislike後にリストから削除されるようになっていて、ここの部分はコード側で実装されているため解説します。
[Scene13Fragment](https://github.com/PiotrPrus/MotionLayoutPlayground/blob/master/app/src/main/java/com/piotrprus/motionlayoutplayground/fragments/Scene13Fragment)では、[MySceneGridItemRecyclerViewAdapter](https://github.com/PiotrPrus/MotionLayoutPlayground/blob/master/app/src/main/java/com/piotrprus/motionlayoutplayground/helper/MySceneGridItemRecyclerViewAdapter.kt)を使っています。リスト削除についてですが、MotionLayout.TransitionListenerインターフェースが提供されていて、goneRight、goneLeftのトランジッションが完了したタイミングでリストから削除しています。

```kotlin:MySceneGridItemRecyclerViewAdapter.kt
class MySceneGridItemRecyclerViewAdapter(
    private val mValues: MutableList<DummyItem>,
    private val animationListener: (state: AnimationState) -> Unit
) : RecyclerView.Adapter<MySceneGridItemRecyclerViewAdapter.ViewHolder>() {
    // 〜省略〜
    
    private val removeItemListener: (Int) -> Unit = {
        Handler().postDelayed({
            mValues.removeAt(it)
            notifyDataSetChanged()
        }, 100)
    }

    inner class ViewHolder(val mView: View, removeItemListener: (Int) -> Unit) :
        RecyclerView.ViewHolder(mView) {
        val mIdView: TextView = mView.item_number
        val mContentView: TextView = mView.content
        internal var position = 0

        override fun toString(): String {
            return super.toString() + " '" + mContentView.text + "'"
        }

        init {
            (mView as MotionLayout).setTransitionListener(object : MotionLayout.TransitionListener {
                override fun onTransitionTrigger(
                    p0: MotionLayout?,
                    p1: Int,
                    p2: Boolean,
                    p3: Float
                ) {
                }

                override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
                    when {
                        p1 == R.id.rest && p3 > 0f -> animationListener(AnimationState.STARTED)
                    }
                }

                override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
                    animationListener(AnimationState.COMPLETED)
                    if (p1 == R.id.goneRight || p1 == R.id.goneLeft) {
                        Log.d("Adapter", "Remove item at position: $position")
                        removeItemListener(position)
                    }
                }

                override fun onTransitionStarted(p0: MotionLayout?, startId: Int, endId: Int) {

                }
            })
        }
    }
}
```

## おわりに
簡単にでしたが、MotionLayoutのサンプルを用いてMotion Editorについて触れてみました。GUIによってConstraintSetとTransitionの関係性がわかりやすくなり、Transitionも手軽に再生できるのでとても便利です。Motion EditorはAndroid Studio 4.0 Canaryの新機能でもっと触っておかしいところがあればIssueを投げていけるように頑張ります。

## PR
コネヒトではエンジニアを募集しています！家族向けサービスをつくりたいエンジニアの皆さん、お待ちしています。<a href="https://www.wantedly.com/projects/290082">
  <img src="https://d2v9k5u4v94ulw.cloudfront.net/small_light(dw=1956,dh=796,da=s,ds=s,cw=1956,ch=796,cc=FFFFFF,of=webp)/assets/images/3591158/original/57a7b345-04dc-492c-8166-b68717e17079?1552905339" alt="Connehito Image">
  <span>家族の課題を解決するサービスのMAUを増やすAndroidエンジニア募集！</span>
</a>