【Android】MotionLayoutでMaterialDesignのTextFieldもどきを実装する

# はじめに

昨年のGoogleI/O 2018でConstraintLayout2.0が紹介されましたが、その中でMotionLayoutという新たなAndroidのアニメーションの仕組みが公開されました。
今回は、そのMotionLayoutを使用して、MaterialDesignのTextFieldっぽいものを実装してみたので、それについて実装方法などを書き連ねたいと思います。
※またMotionLayoutはまだalpha版なので、今回は、`constraint-layout:2.0.0-alpha4`での実装を紹介しますが、新たなバージョンがリリースされた場合、実装が変わる可能性もあるので、ご了承ください。

# 作ったもの

こちらのような項目がリストのように表示されていて、画面遷移させることなく閲覧モードから入力モードにすることができます。それぞれのモード間の遷移時のアニメーションでMotionLayoutを使って実装しています。

挙動としては、
1. 右のテキスト部分をクリックした場合、入力モードになる
2. フォーカスが外れたら、閲覧モードに戻る
（フォーカスは別のテキストFieldをクリックするか、下のボタンを押下すると外れる）


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147934/76b450d6-a187-8b25-d9a8-05133ba30ca5.gif" width=50%>

ソースコードはこちらに上げています。参考にされたい方はこちらをご覧ください。
https://github.com/youmitsu/MotionLayoutMaterialTextField

# MotionLayoutの概要

MotionLayoutについては、多くの方が仕組みについての記事を公開しているので、ここでは省略します。

基本的なステップとしては、以下となります。

1. アニメーションさせる前のレイアウトをMotionLayoutを親として実装
2. Sceneのxmlを作成
3. 1のattributeに2のSceneファイルを指定

# 実装方法

## 1. ConstraintLayout2.0のインストール

まずは、ConstraintLayout2.0をインストールします。
AndroidXと、SupportLibraryの2つのArtifactsがあるので、自身のプロジェクトに応じて、使い分けましょう。

リリースノートはこちら
https://androidstudio.googleblog.com/2019/04/constraintlayout-200-alpha-4.html

AndroidXの場合

```gradle
dependencies {
    ...
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4'
}
```

SupportLibraryの場合

```gradle
dependencies {
    ...
    implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha4'
}
```

## 2. 初期状態のviewを作成

まずは、こちらのクリックされる前のレイアウトを作っていきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147934/b9f802b0-8ec0-d227-bf78-cdf85ba457c9.png" width=50%>

xmlは以下になります。MotionLayoutはConstraintLayoutの子クラスなので、基本的にはConstraintLayoutでレイアウトを作るのと同じ要領で実装します。

※サンプルリポジトリでは、Databindingを使って入力データをバインディングしているので、`<layout>`と`<data>`タグが入っていますが、無くても動きます。

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

    <data>
        <variable name="data" type="jp.co.youmitsu.myapplication.CustomMotionEditTextLayout"/>
    </data>

    <androidx.constraintlayout.motion.widget.MotionLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/motion_layout"
    >

        <TextView
                android:id="@+id/nickname_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:gravity="start|center_vertical"
                android:layout_marginStart="10dp"
                android:textColor="#808080"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toStartOf="@id/nickname_value"
                android:text="@{data.title}"
        />

        <TextView
                android:id="@+id/nickname_value"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:gravity="end|center_vertical"
                android:textSize="15sp"
                android:layout_marginEnd="10dp"
                android:ellipsize="end"
                android:singleLine="true"
                android:maxEms="15"
                android:text="@{data.value}"
                android:clickable="true"
                android:focusable="true"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/nickname_title"/>

        <androidx.appcompat.widget.AppCompatEditText
                android:id="@+id/edit_text"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:alpha="0"
                android:visibility="invisible"
                android:textSize="15sp"
                android:inputType="text"
                android:text="@{data.value}"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"/>

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

## 3. アニメーション前後のConstraintの状態とTransitionを表すMotionSceneファイルを作成

次に、タップしてアニメーションし始める時と終わった時のレイアウトのConstraintSetとTransitionをMotionSceneという形で定義します。
MotionSceneファイルはxmlで記述するので、res/xml配下に新たなファイルを作成します。

xmlファイルは以下です。
@id/startがアニメーションする前のConstraintSet。@id/endがアニメーションした後のConstraintSetを表しています。
今回の場合、start時のConstraintと上記2で定義したMotionLayout内の要素のConstraintは一致することになります。


`@id/start`のConstraintSetでのレイアウト:
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147934/b9f802b0-8ec0-d227-bf78-cdf85ba457c9.png" width=50%>

`@id/end`のConstraintSetでのレイアウト:
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147934/6edc2f30-4115-c36e-4397-261dc272dc26.png" width=50%>



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

    <Transition
            motion:constraintSetStart="@id/start"
            motion:constraintSetEnd="@id/end"
            motion:duration="200">

        <OnClick motion:targetId="@id/nickname_value"
                 motion:clickAction="transitionToStart|transitionToEnd"/>

    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
                android:id="@+id/nickname_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:textSize="@dimen/text_size_normal"
                android:layout_marginStart="5dp"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toStartOf="@id/nickname_value">
            <CustomAttribute
                    motion:attributeName="textSize"
                    motion:customFloatValue="15"
            />
            <CustomAttribute
                    motion:attributeName="textColor"
                    motion:customColorValue="#808080"
            />
        </Constraint>

        <Constraint
                android:id="@+id/nickname_value"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:textSize="15sp"
                android:layout_marginEnd="10dp"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toEndOf="@id/nickname_title"/>

        <Constraint
                android:id="@+id/edit_text"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:alpha="0"
                android:visibility="invisible"
                android:textSize="15sp"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"/>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">

        <Constraint
                android:id="@id/nickname_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="5dp"
                motion:layout_constraintBottom_toTopOf="@id/edit_text"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent">
            <CustomAttribute
                    motion:attributeName="textSize"
                    motion:customFloatValue="12"
            />
            <CustomAttribute
                    motion:attributeName="textColor"
                    motion:customColorValue="@color/colorAccent"
            />
        </Constraint>

        <Constraint
                android:id="@+id/nickname_value"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                android:textSize="15sp"
                android:alpha="0"
                android:visibility="invisible"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"/>

        <Constraint
                android:id="@id/edit_text"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:alpha="1"
                android:visibility="visible"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"/>
    </ConstraintSet>

</MotionScene>
```

## 4. MotionLayoutのattributeにlayoutDescriptionとして3のSceneファイルを設定

アニメーションする対象のview(MotionLayout)とMotionSceneファイルを作成できたら、それぞれを結びつけるために、MotionLayoutのlayoutDescription属性を設定します。こうすることで、画像のようなアニメーションが実現できます。

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

    <data>
        <variable name="data" type="jp.co.youmitsu.myapplication.CustomMotionEditTextLayout"/>
    </data>

    <androidx.constraintlayout.motion.widget.MotionLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/motion_layout"
            app:layoutDescription="@xml/layout_custom_motion_edittext_scene"  // これを追加
    >
    ...
    </androidx.constraintlayout.motion.widget.MotionLayout>
</layout>
```

# まとめ

今回は、MotionLayoutを使って、MaterialDesignのTextFieldの拡張っぽいものを実装してみました。アニメーションに関するコードをほとんどxmlだけで完結させることができるので、とても使いやすそうだなという印象です。
これからも色々機能追加があるみたいなので、楽しみです。
最後まで読んでいただきありがとうございました。

# 参考にさせていただいたもの

- [Introduction to MotionLayout (part I) | Medium](https://medium.com/google-developers/introduction-to-motionlayout-part-i-29208674b10d)
- [t-kurimura/MotionLayoutSpeedDial](https://github.com/t-kurimura/MotionLayoutSpeedDial)
- [[Android] ConstraintLayout レイアウト逆引きまとめ | Qiita](https://qiita.com/tktktks10/items/62d85dabac4bdb8c1f94)