ConstraintLayoutをもう少し深く知ってみる

shibuya.apk #18で話すネタです。
https://shibuya-apk.connpass.com/event/64610/
# きっかけ
Google I/OのこれがConstraintLayoutでできているという噂を聞いて、これは！！と思ってConstraintLayuotを始めてみました。
![output_sicle.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/9aebeb3d-3010-1b15-006d-35b8157e1a7c.gif)
この記事はConstraintLayoutを使って、ちょっとした画面は作ったことはある人向けです。
このアニメーションについて知りたい人は最後だけ読んで下さい。
それにより以下のようなアニメーションを簡単に作ることができました。
![output.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/9116baee-9fbe-73e0-09df-abfbf1a12d7a.gif)

Google I/Oの動画とかで完璧に理解している人にはそんなに新しい知識はないかもです。。

サンプルアプリも用意しました。
https://github.com/takahirom/constraint-layout-samples
<img src="https://qiita-image-store.s3.amazonaws.com/0/27388/900d9393-3ce6-5d71-41c3-f9fc72014140.png" width="300px" />


## 基礎的なところ

大きさの指定について、てきとうになってしまっていたのでメモしておきます。

| | 名前 | 説明 |
|---|---|---|
| <img src="https://codelabs.developers.google.com/codelabs/constraint-layout/img/7fb5ded1aedd7246.png" width="150px"/> | Fixed | 設定されている大きさ。width="16dp"など | 
| <img src="https://codelabs.developers.google.com/codelabs/constraint-layout/img/44fff184b08bc2e4.png" width="150px"/ > | Match Constraints | Constraintを満たす最大の大きさ。**XMLファイルでは0dpがこれの意味**で、width="0dp"となっていたらMatch Constraintだと思うといいと思います |
| <img src="https://codelabs.developers.google.com/codelabs/constraint-layout/img/d066cfdb50a039ba.png" width="150px"/ > | Wrap Content | コンテンツの大きさに依存する。(画像サイズや文字数など) |
| | Match Parent | ConstraintLayoutでは**利用されるべきではない** |

<!--

二種類の自動的なConstraintsを入れる方法

Infer Constraints ![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/c09dbb8b-3117-1309-16fc-03b4ae2d91f3.png)
全てのView同士の位置を見てConstraintを入れる

Autoconnect ![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/c8121df1-2e2e-bee5-4699-fd9e68ff7da1.png)
親のViewの要素と選択したViewとの関係のみ見てConstraintを入れる

アスペクト比の指定できる。
画像の大きさによって可変にする場合はlayout_widthをwrap_contentにして、縦をmatch_constraintにする。
-->

## Bias
大きさがwrap_contentまたはfixedになっていて、左右または上下に制約がついている時、biasを設定することができます。
この上と下にでている50というものです。
動かすと位置が移動します。
![bias.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/3a073d5b-a30b-5032-69c8-67466ba7a35d.gif)


ちなみに**コードからも設定することができます**。
基本的に`ConstraintSet`を使うことで、プロパティをいじることができます

```kotlin
val constraintSet = ConstraintSet()
constraintSet.clone(constraintLayout)
val bias = 0.5F
constraintSet.setVerticalBias(R.id.button, bias)
constraintSet.applyTo(constraintLayout)
```

ConstraintLayoutを利用して、左寄せにしておいて、**狭くなったときのマージンを確保しておくということもできたり**して、より柔軟にレイアウトできます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/4bd9ddbf-fb14-2af5-28da-36c7d63b42c6.png)


## Chain
他のViewと同じ大きさなど大きさを整えるような形でのレイアウトが出来ます。
例えば**均等配置とか、LinearLayoutのweight=1みたいなのが楽にできます**。
また間にあるViewが消えたときもちゃんとLinearLayoutのような動きをするようです。

均等配置
(ちなみにChain Styleというものがあり、CHAIN_SPREAD -> CHAIN_SPREAD_INSIDE -> CHAIN_PACKEDの順番で動きます。見ればわかると思います。)
![chain.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/4c5e1aab-04ad-64df-046e-ac83dbab966f.gif)

xml上では以下のようになっていて指定されているのがわかると思います。

```
app:layout_constraintHorizontal_chainStyle="spread_inside"
```


一つのボタンをwidthを0dpにすることで、match constraintになって、こんな感じになります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/8b186520-2d4e-16f2-811a-6ee2c1842dee.png)

以下のように**weightを指定することも出来る**ので、比率での指定も可能となります。

```xml
app:layout_constraintHorizontal_weight="1"
```

## GuideLine
デザインツールでよく利用されるらしいガイドラインを引けます。
マテリアルデザインでは[KeyLine](https://material.io/guidelines/layout/metrics-keylines.html#metrics-keylines-baseline-grids)という名前で紹介されています。

右クリック→Helpers→Add GlideLineで追加できます。

上や横からの**DP指定で線を引けるだけでなく、％指定することができます**。
以下の例では左から20dp、上から水平に50%で指定して、それを元にボタンを置いています。
<img src="https://qiita-image-store.s3.amazonaws.com/0/27388/1c969b6c-7ccd-c42d-ccbf-ec2814bb59bd.png" width="280px" />
%で指定するのは現状UIで指定できないようで、
xmlを開いて以下のようになっているところを

```
app:layout_constraintGuide_begin="20dp"
```

以下のように`app:layout_constraintGuide_percent`を使うように修正することがで利用することができます。割合は0〜1で指定します。

```
app:layout_constraintGuide_percent="0.5"
```

## ConstraintLayoutはViewがgoneのときの挙動もサポートしている
ConstraintLayoutの中のViewがGONEになったときはそのViewが0x0でMarginも0になるような動きになります。
![gone_2.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/7b5768d3-5f77-e06f-466a-c65d7cbee190.gif)


その時にGONEになったViewを参照していたViewで**goneになったときのマージンの大きさを設定することができます**。地味にうれしいですね。

```xml
app:layout_goneMarginLeft="200dp"
```
![gone.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/bb314fe4-eeaf-9f92-b55d-6afb6ff47a74.gif)


# 1.1.0 Beta-1
新しく追加された機能について紹介していきます。

## Barrier
### なぜ使うか？
以下のようなレイアウトの時に"Settings"という文字列がめっちゃ長くなる可能性がある時、どうやって実装しますか？？

| 通常時 | 長くなった時 |
|---|---|
![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/31381d10-6332-5a14-51fb-e031d4881d31.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/6dff8041-1a53-5ef8-d6b2-1f6d23602ffd.png)|
Settingsの部分の文字数が増えるとかぶっちゃいました。これをBarrierで改善しましょう。

### 使い方

こういうときCameraとSettingsのTextViewをレイアウトで囲むのではなくBarrierを使います。
右クリックからHelpers -> Add Vertical barrierで追加できます。(どうでもいいんですが、BarrierのBが大文字と小文字で違いますね)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/5e9a3177-1693-768a-dfe2-2f1bc3d4c56c.png)
そしてBarrierの下に避けさせたいものを持ってきます。仮想的にレイアウトを作っちゃうイメージです。
![add.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/c83b9cc9-ea51-eaa2-b969-7335f59089e5.gif)
Barrierを作る場所を指定します。CameraとSettingsのTextViewの右側なのでrightまたはendを指定します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/af574c8b-92e2-3af1-6c59-40b661464195.png)

最後に右のEditTextからのConstraintを消して、BarrierにつけてあげればOK。


![constraint.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/822f1ec8-4c92-56d3-bac2-c00a9007152c.gif)


これで**伸びても大丈夫に**なりました！！

![testbarrier.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/f237dee7-8b80-ec97-771c-308de618bbf1.gif)

## Group
### なぜ使うか？
例えば固定のヘッダーがあったとして、それがいくつものViewで構成されているとき、表示非表示を一気に切り替えるようなときに使います。
(コードをなんとなく見たのですが、今のところvisibilityとelevationのみしか変更できないようです。)

### 使い方
まずConstraintLayoutで右クリックしてAdd Groupします

![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/a1927e2a-72fe-567b-3ce6-4f12803b8932.png)
Groupの中にViewを入れます。
![group.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/fd451929-b046-6d07-fdd3-81b189f0df33.gif)

後はgroupにidをつけられるので、以下のような形でコードから変えることで**一気に関連付けられているViewの可視性を変更できます**。

```java
findViewById<Group>(R.id.group).visibility = View.GONE
```

## PlaceHolder
PlaceHolderは用意しておくことで、簡単にウィジェットの内容を置き換えることができます。

![placeholder.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/58cec47c-66d7-9a3d-0ce8-20ee67d7e207.gif)

たったこれだけ。最初はなぜこんな動きができるのか意味不明でした。

```java
val onClickListener: (View) -> Unit = { view ->
     TransitionManager.beginDelayedTransition(root as ViewGroup)
     placeholder.setContentId(view.id)
}
imageA.setOnClickListener(onClickListener)
imageB.setOnClickListener(onClickListener)
imageC.setOnClickListener(onClickListener)
imageD.setOnClickListener(onClickListener)
```

### 使い方
今回のような使い方をするにはまず表示したいものを**Chainでつなげます**。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/0be74922-67e0-9e88-a606-9a12aabba6b3.png)

そしてxmlでPlaceHolderを置いてから、UI上で大きさなどの制約をつけます(現状、AndroidStudioのUI上で設置できない？)
ここでapp:content="@+id/image_d"のように指定することで、PlaceHolderの方にViewがもってこられるような形になります。

```xml
        <android.support.constraint.Placeholder
            android:id="@+id/placeholder"
            ...
            app:content="@+id/image_d"
            ...  />
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/8c094fde-0946-da10-f506-60610ddfa02f.png)

これで以下のようなコードだけで実行できます。
なぜこれでできるかというと、**contentIdを指定することでViewの入れ替わりが発生して、またChainを利用していることで、Viewが消えた時にちゃんと他のViewで保管して入れてくれる**ためです。
そしてTransitionManager.beginDelayedTransitionを使うことでアニメーションしています。[Transitionについてはこちらで前に説明しましたのでよろしければ、、](https://speakerdeck.com/takahirom/android-transition)

```java
val onClickListener: (View) -> Unit = { view ->
     TransitionManager.beginDelayedTransition(placeHolderBinding.root as ViewGroup)
     placeholder.setContentId(view.id)
}
imageA.setOnClickListener(onClickListener)
imageB.setOnClickListener(onClickListener)
imageC.setOnClickListener(onClickListener)
imageD.setOnClickListener(onClickListener)
```


## Percent Dimension
以下で％(割合)で大きさを指定できます。(現状、AndroidStudioのUI上で設置できない？)
**PercentRelativeLayoutの置き換えが快適になりそう**です。

```xml
android:layout_width="0dp"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.5"
```


# Google I/Oのアニメーションについて
このアニメーションについてです。
![output_sicle.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/9aebeb3d-3010-1b15-006d-35b8157e1a7c.gif)
コードはここにあります。
https://github.com/google/iosched/blob/master/lib/src/main/java/com/google/samples/apps/iosched/feed/FeedViewHolder.java#L51

すごく簡単に書くと以下のような形です。

```java
    private final ConstraintSet expandedConstraints = new ConstraintSet();
    private ConstraintLayout mainLayout;


// 前もってやっておく レイアウトのConstraintSetの内容をcloneする。
expandedConstraints.clone(mainLayout.getContext(), R.layout.feed_message_card_expanded);

// アニメーション + レイアウト変更
TransitionManager.beginDelayedTransition(parent);
expandedConstraints.applyTo(mainLayout);
```

beginDelayedTransitionでアニメーションを、applyToでConstraintSetの反映を行っているようです。
`constraints.clone(context, R.layout.expanded);`で既存のレイアウトから制約だけ取ってきて、それを`constrains.applyTo(constraintLayout)`で反映させるということを行っています。

本当はTransitionの動きをCustom Transitionを作ってカスタマイズして、画像と文字が重ならないようにしたりなどを行っていますが、Transitionの話になってしまうので、ここでは省略します。

最初の方に出していたサンプルのアプリのコードは以下になります。**ConstraintLayoutを使うことで簡単にアニメーションを実装することができます**。
https://github.com/takahirom/constraint-layout-samples/blob/master/app/src/main/java/com/github/takahirom/constraint_layout_samples/AnimationActivity.kt

# ConstraintLayoutの拡張性
**BarrierやGroupは`ConstraintHelper`クラスを継承して実現されています**。

Viewのライフサイクルのタイミングなどで、 指定されたViewの操作が出来きます
カスタムしたのを簡単に作ってみました。ちなみに**[勝手に作ることも公式にサポートされています](https://academy.realm.io/posts/360-andev-2017-nicolas-roard-advanced-constraintlayout/)**。ちゃんとした使い方はちゃんとしたドキュメントが出てからになると思います。
![blink.gif](https://qiita-image-store.s3.amazonaws.com/0/27388/d87db1cf-6847-2a3e-3acf-77bf8d073253.gif)

レイアウトでの指定

```xml
    <com.github.takahirom.constraint_layout_samples.Blink
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="textView,imageView" />
```

ConstraintHelperを継承したBlinkクラス

```kotlin
class Blink : ConstraintHelper {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun updatePreLayout(container: ConstraintLayout) {
        super.updatePreLayout(container)

        for (i in 0 until this.mCount) {
            val id = this.mIds[i]
            val view = container.getViewById(id)
            if (view != null) {
                ObjectAnimator.ofFloat(view, "alpha", 1F, 0F).apply {
                    repeatCount = ValueAnimator.INFINITE
                    repeatMode = ValueAnimator.REVERSE
                }.start()
            }
        }
    }
}
```


# まとめ
* いろんなレイアウトの機能が入っていて、 結構いろんなパターンに適応できそう
* ある程度拡張できるように作ってある
* ConstraintLayout 1.1が待ち遠しい
* アニメーションを簡単にできる


---
参考資料
https://codelabs.developers.google.com/codelabs/constraint-layout/index.html
https://androidstudio.googleblog.com/2017/05/constraintlayout-110-beta-1-release.html
https://medium.com/google-developers/building-interfaces-with-constraintlayout-3958fa38a9f7
https://blog.stylingandroid.com/constraintlayout-chains-spread-chains/
https://www.youtube.com/watch?v=nYb4FUdlLZE
https://academy.realm.io/posts/360-andev-2017-nicolas-roard-advanced-constraintlayout/
https://github.com/googlesamples/android-ConstraintLayoutExamples
https://constraintlayout.com/