【Android】独自のConstraintHelperを作成し、MotionSceneをちょっとスッキリさせてみる

この記事は、[and factory Advent Calendar 2020](https://qiita.com/advent-calendar/2020/andfactory)の**20日目**の記事です。
昨日は[@kawamoto_kazuya](https://qiita.com/kawamoto_kazuya)さんの「[GoでgRPCの構築](https://qiita.com/kawamoto_kazuya/items/630f65dc2674cd88b8b5)」でした！

# はじめに
MotionLayoutいいですよね！
アニメーションをxmlで宣言的に定義できたり、Android StudioのMotion Editorもなかなかに便利です。

そんなMotionLayoutですが、いつの間にかMotionSceneのxmlファイルの記述が多くなってしまうこともあるかと思います。

そこで本記事では、独自のConstraintHelperを作成することで、MotionSceneファイルをちょっとスッキリさせてみる方法をご紹介します。

※ 本記事の内容は、ConstraintLayout ver.2.0.4で確認しています。

# サンプル
矢印をドラッグすることで、ドロイド君の透明度を徐々に1→0にするアニメーションを作ってみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995/7c9230a0-5762-9bac-aaf2-f432fb936811.gif" width=200 />

MotionSceneファイルは下記のような感じです。

```xml:scene.xml
<?xml version="1.0" encoding="utf-8"?>
<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">
        <OnSwipe
            app:dragDirection="dragRight"
            app:touchAnchorId="@id/arrow"
            app:touchAnchorSide="right" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@id/arrow">
            <Layout
                android:layout_width="60dp"
                android:layout_height="60dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@id/arrow">
            <Layout
                android:layout_width="60dp"
                android:layout_height="60dp"
                app:layout_constraintEnd_toEndOf="parent" />
        </Constraint>

        <Constraint android:id="@id/droid">
            <PropertySet android:alpha="0" />
        </Constraint>
    </ConstraintSet>
</MotionScene>
```
1. ドラッグできるように、矢印（ `@id/arrow` ）に対して `OnSwipe` の指定をする。

2. 矢印は、開始時点（ `@id/start` ）では画面左になるように制約を付け、終了時点（ `@id/end` ）で画面右になるように制約を付ける。

3. ドロイド君（ `@id/droid` ）は、終了時点で透明度（ `alpha` ）が0になるように制約を付ける。

これだけで、矢印をドラッグすることでドロイド君を徐々に透明にするアニメーションを実現できます。（MotionLayout便利:grinning:）

# ドロイド君が増殖した場合
ただ、上記の例ではドロイド君が1体だったのでスッキリと書けていますが、ドロイド君が10体に増殖すると…
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995/0f4f0683-9731-b5fa-98ef-2bcbb41eca63.gif" width=200 />

[Group](https://developer.android.com/reference/androidx/constraintlayout/widget/Group)や[Layer](https://developer.android.com/reference/androidx/constraintlayout/helper/widget/Layer)では、透明度（ `alpha` ）に関してはサポートしていないため使うことができず、ドロイド君10体に対し、それぞれ愚直に `Constraint` の制約を付ける必要が出てきてしまいます。

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

    <!-- 省略 -->

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@id/arrow">
            <Layout
                android:layout_width="60dp"
                android:layout_height="60dp"
                app:layout_constraintEnd_toEndOf="parent" />
        </Constraint>

        <Constraint android:id="@id/droid_1">
            <PropertySet android:alpha="0" />
        </Constraint>

        <Constraint android:id="@id/droid_2">
            <PropertySet android:alpha="0" />
        </Constraint>

        <Constraint android:id="@id/droid_3">
            <PropertySet android:alpha="0" />
        </Constraint>

        <Constraint android:id="@id/droid_4">
            <PropertySet android:alpha="0" />
        </Constraint>

        <Constraint android:id="@id/droid_5">
            <PropertySet android:alpha="0" />
        </Constraint>

        <Constraint android:id="@id/droid_6">
            <PropertySet android:alpha="0" />
        </Constraint>

        <Constraint android:id="@id/droid_7">
            <PropertySet android:alpha="0" />
        </Constraint>

        <Constraint android:id="@id/droid_8">
            <PropertySet android:alpha="0" />
        </Constraint>

        <Constraint android:id="@id/droid_9">
            <PropertySet android:alpha="0" />
        </Constraint>

        <Constraint android:id="@id/droid_10">
            <PropertySet android:alpha="0" />
        </Constraint>
    </ConstraintSet>
</MotionScene>
```

# 独自のConstraintHelperを作成してみる
そこで、**透明度を一括で適用できる独自のConstraintHelper**を作成し、MotionSceneファイルをちょっとスッキリさせてみたいと思います。

ConstraintHelperを継承して作られている[Group.java](https://github.com/androidx/constraintlayout/blob/3271f47c1f231fef1911382593d502548bc0f84e/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/Group.java) を参考にしつつ、自作したクラスが下記となります。（`Opacity.kt`）

```kotlin:Opacity.kt
package app.example.sample

import android.content.Context
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintHelper
import androidx.constraintlayout.widget.ConstraintLayout

class Opacity @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
) : ConstraintHelper(context, attrs, defStyleAttr) {

    override fun init(attrs: AttributeSet?) {
        super.init(attrs)
        this.mUseViewMeasure = false // ①
    }

    // ②
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        updateReferencedViewsAlpha()
    }

    // ③
    override fun setAlpha(alpha: Float) {
        super.setAlpha(alpha)
        updateReferencedViewsAlpha()
    }

    // ④
    private fun updateReferencedViewsAlpha() {
        val parent = parent
        if (parent is ConstraintLayout) {
            getViews(parent).forEach { view ->
                view?.alpha = alpha
            }
        }
    }
}
```

1. 参考にした`Group.java`でも[呼んでいた](https://github.com/androidx/constraintlayout/blob/3271f47c1f231fef1911382593d502548bc0f84e/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/Group.java#L57)ため同様に呼んでいますが、内部的には `onMeasure()` 時に無駄な計算をしないためのフラグのようでした。

2. ViewがWindowにアタッチされたタイミングで処理を行いたいので定義。

3. 透明度が更新されたタイミングでも処理を行いたいので定義。

4. このクラスの肝となるメソッドです。 `getViews(parent)` は `ConstraintHelper` に定義されているメソッドで、 `constraint_referenced_ids` で指定されたidのViewたちを取得するメソッドです。`Opacity.kt`ではViewたちを取得し、それぞれで `alpha` を指定してあげている感じです。

# 使ってみる
レイアウトファイルでは、 `Opacity` 追加し、 `constraint_referenced_ids` でドロイド君のImageViewのidをセットします。

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

    <!-- 省略（ドロイド君のImageViewがdroid_1, droid_2,...と10個並びます） -->

    <!-- 矢印 -->
    <ImageView
        android:id="@+id/arrow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_baseline_forward_24" />

    <app.example.sample.Opacity
        android:id="@+id/opacity"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="droid_1,droid_2,droid_3,droid_4,droid_5,droid_6,droid_7,droid_8,droid_9,droid_10" />
</androidx.constraintlayout.motion.widget.MotionLayout>
```

MotionSceneファイルでは、愚直に書いていた制約を削除し、 `@id/opacity` への制約だけにすることができました…！（ｽｯｷﾘ）

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

    <!-- 省略 -->

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@id/arrow">
            <Layout
                android:layout_width="60dp"
                android:layout_height="60dp"
                app:layout_constraintEnd_toEndOf="parent" />
        </Constraint>

        <!-- @id/opacityへの指定だけになった！ -->
        <Constraint android:id="@id/opacity">
            <PropertySet android:alpha="0" />
        </Constraint>
    </ConstraintSet>
</MotionScene>
```

差分は下記のような感じに。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995/b88e220e-d8ff-09db-db9a-bf75b6eb051e.png" width=300 />

ちょっとスッキリさせることができたのではないでしょうか。

# 終わりに
独自のConstraintHelperを作成することで、MotionSceneファイルをちょっとスッキリさせてみる方法をご紹介しました。

今回は透明度を変更するという簡単な機能でしたが、うまく使うとさらに効率よくMotionLayout・ConstraintLayoutを実装することができそうなので、便利だなと感じました。

ConstraintLayout 2.1.0では、[Carousel](https://github.com/androidx/constraintlayout/wiki/Carousel)というHelperクラスも出てくるようなので、今後もぞくぞくと便利なHelperクラスが誕生してくるのかもしれません。

# 参考 :pray:
- [【Android】ConstraintHelperの使い方](https://qiita.com/emusute1212/items/7b34ec4562309d183458)