【Android】ConstraintHelperの使い方

本記事は[Android #2 Advent Calendar](https://qiita.com/advent-calendar/2019/android-2)の22日目の記事になります。 :christmas_tree: :snowman2: 
初の参加で若干緊張しています。。。。 :innocent: 

# はじめに

現在、AndroidのUI実装では`ConstraintLayout`を利用することがほとんどだと思います。
今回は`ConstraintLayout`の一つの、`ConstraintHelper`について紹介したいと思います。

なお、今回使用するバージョンは、ベータ版の[`ConstraintLayout 2.0.0-beta2`](https://androidstudio.googleblog.com/2019/10/constraintlayout-200-beta-3.html)を使用しているので、後ほど内容が変更になる可能性がありますので、その点はご了承ください。
また、変更がかかった際は、再度こちらの記事にも更新をかけようと考えています。 :bow:

# `ConstraintHelper`とは

複数のViewに対して同じ操作を行うためのものと認識しています。
`ConstraintHelper`自体の実装は`View`を拡張した抽象クラスであり、このクラスを独自に拡張することもサポートされているようですので、非常に便利です。

`ConstraintHelper`へ反映させたいViewを指定するときは、`app:constraint_referenced_ids`にidをコンマ区切りで格納することで指定できます。これは基本的にどの`ConstraintHelper`でもこの方式を用いており、共通仕様となっております。

独自で拡張するまでもなく、すでに`ConstraintHelper`を拡張したクラスとして、以下が存在します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/6ce74eec-a6c0-2039-319d-6ff29178f7a7.png)

すでにご存知のものも多いと思いますが、一つずつ軽く説明します。（`VirtualLayout`は抽象クラスですので飛ばします。）

## `Barrier`

下のように、二つの並んだViewに基準線をもうけます。これがバリアーと呼ばれる`ConstraintHelper`であり、そこに Constraint をつけることで、片方の大きさが変わっても、基準線は変動してViewが重なるということは無くなります。これは結構使ったことのある人が多いのではないでしょうか。

![barrier.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/0aa8b114-6df4-dc90-6dcb-c955dbe8e475.gif)

## `Flow`

特定のViewを指定した方向に並べます。方向としては、**Horizontal**と**Vertical**が存在し、それぞれの並び方を以下に示します。こちらは最近リリースされた機能になるので、初めての方も多いと思います。

**Horizontal**

<img width="357" alt="スクリーンショット 2019-12-11 1.20.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/117afdca-e3f8-026d-277d-32b905c0f6d7.png">

**Vertical**

<img width="188" alt="スクリーンショット 2019-12-11 1.29.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/737ba4a8-7ec9-889c-30e1-3b370a9951bc.png">

## `Group`

まとめてViewのVisibilityを変更したい時に使用します。
GroupのVisibilityを変更すると、それに応じて、中身のViewのVisibilityも一括で変更されます。
こちらはかなり有名なので、愛用している人も多いと思います。

![group.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/b08f32fa-d8ef-4a07-45f8-a8eb021bd3dd.gif)

## `Layer`

Viewをまとめて操作したい時に使用します。

今回はLayerを使用して特別なことはしていませんが、`app:constraint_referenced_ids`にViewを追加してみると、下のようにまとめてくれましたので、今までLayoutをネストしてUI実装してた箇所が不要になると思います。

<img width="176" alt="スクリーンショット 2019-12-12 1.17.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/f725556a-4c90-0d85-2fcd-f3adf11b0f4b.png">

## `MotionHelper`

主に`MotionLayout`で使用する`ConstraintHelper`のようです。
今回の記事では`ConstraintHelper`がメインですので、割愛します。

# `ConstraintHelper`の拡張

上でも書いた通り、`ConstraintHelper`は拡張することを公式にサポートしています！
ここでは、実際に拡張しながら使い方を見ていきたいと思います。

## 独自実装時に継承するメソッド（変更される可能性あり）

独自実装をする時によく継承するメソッドを洗い出しておきますが、全て継承する必要はありません。

- `init(attrs: AttributeSet)`
    - その名の通り、初期化メソッドです。
    - attrsで属性の内容を取ることができます。
- `updatePreLayout(container: ConstraintLayout)`
    - **初め**の`onMeasure`が呼び出される時に実行される。
- `updatePostLayout(container: ConstraintLayout)`
    - `onLayout`が呼び出される時に実行される
- `updatePostMeasure(container: ConstraintLayout)`
    - `onMeasure`が呼び出されるたびに実行される。
- `updatePostConstraints(container: ConstraintLayout)`
    - constraintsに変更があるたびに呼び出されます。

## 実装例

今回は指定したViewの背景色を一括で変更するための`ConstraintHelper`を作りましょう。
名前は`BackgroundHelper.kt`とします。

`attr.xml`はトピックから外れるので隠しておきます。

<details>
<summary>`attr.xml`</summary>
<div>

```xml:attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="BackgroundHelper">
        <attr name="backgroundColor" format="color" />
    </declare-styleable>
</resources>
```
</div>
</details>

```kotlin:BackgroundHelper.kt
class BackgroundHelper @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {
    private lateinit var backgroundColor: Color

    override fun init(attrs: AttributeSet?) {
        super.init(attrs)
        // 設定する背景色を取得する
        val array = context.obtainStyledAttributes(
            attrs,
            R.styleable.BackgroundHelper
        )

        backgroundColor = array.getColor(
            R.styleable.BackgroundHelper_backgroundColor,
            -1
        ).let {
            Color.valueOf(it)
        }
        
        array.recycle()
    }

    override fun updatePreLayout(container: ConstraintLayout?) {
        super.updatePreLayout(container)
        // はじめに`app:constraint_referenced_ids`に詰められた値をsetIdsでセットする （2.0.0-beta3からは不要）
        if (mReferenceIds != null) {
            setIds(mReferenceIds)
        }
    }

    override fun updatePostLayout(container: ConstraintLayout?) {
        // `app:constraint_referenced_ids`の個数をmCountで取得できる。
        // mIdsには`app:constraint_referenced_ids`で指定したIDが配列でつめられる
        mIds.take(mCount).mapNotNull { id ->
            container?.getViewById(id)
        }.forEach { view ->
            // それぞれのViewの背景色を指定した色へ変更する
            view.background = backgroundColor.toDrawable()
        }
    }
}
```

`mIds.take(mCount)`の部分は`take`せずに単純に`mIds`の`forEach`だけでも良いのでは？ :thinking: と思っているのですが、他の`Group`や`Barrier`でも同じ手法で実装されているので、毎回これを使っています。 :thinking: 

## 実行

この`BakgroundHelper`を先ほどのTextView二つに適応させると以下のようになります。（色は`#D81B60`で指定しています。）

<img width="198" alt="スクリーンショット 2019-12-14 0.18.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/16880d35-fe50-e9d7-6cc7-2b85cb3c9424.png">

`ConstraintHelper`で個人的に一番感動したポイントなのですが、このように`backgroundColor`を変更すると、リアルタイムでその変更が適応されます。（画面左側）

![background_helper.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/4bb66ad2-79c3-7022-e544-a34495324dfd.gif)

# 最後に

今回は`ConstraintHelper`という`ConstraintLayout`の一部について紹介しました。
`ConstraintLayout`だけでは表現しきれないUIも、これを使えば実装ができるかもしれませんし、非常に将来性のある機能だと思っています。

2.0.0がstableになるのが待ち遠しいですね！ :upside_down: 

# 余談

ConstraintLayoutの最新版として、[`ConstraintLayout 2.0.0-beta3`](https://androidstudio.googleblog.com/2019/10/constraintlayout-200-beta-3.html)があるのですが、ConstraintHelperがうまく動いてくれませんでしたので、しぶしぶ今回は`ConstraintLayout 2.0.0-beta2`を使用しています。。。
やはりベータ版だと、ところどころバグがあるので、stableになるまで待ったほうがよさそうですね :sweat:

# 参考
- [ConstraintLayoutをもう少し深く知ってみる - Qiita](https://qiita.com/takahirom/items/97818748e2c2059a2536)
- [Android ConstraintLayout—The Guide to ConstraintHelpers \| Polidea](https://www.polidea.com/blog/android-constraintlayout-the-guide-to-constrainthelpers/)
- [Constraint Helper 활용하기 - Yeongpyo - Medium](https://medium.com/@yeongpal/constraint-helper-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0-995a868130fb)
- [ConstraintLayout Flowの紹介 - stsnブログ](https://satoshun.github.io/2019/04/constraintlayout-flow/)