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

  • 111
    いいね
  • 0
    コメント

shibuya.apk #18で話すネタです。
https://shibuya-apk.connpass.com/event/64610/

きっかけ

Google I/OのこれがConstraintLayoutでできているという噂を聞いて、これは!!と思ってConstraintLayuotを始めてみました。
output_sicle.gif
この記事はConstraintLayoutを使って、ちょっとした画面は作ったことはある人向けです。
このアニメーションについて知りたい人は最後だけ読んで下さい。
それにより以下のようなアニメーションを簡単に作ることができました。
output.gif

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

サンプルアプリも用意しました。
https://github.com/takahirom/constraint-layout-samples

基礎的なところ

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

名前 説明
Fixed 設定されている大きさ。width="16dp"など
Match Constraints Constraintを満たす最大の大きさ。XMLファイルでは0dpがこれの意味で、width="0dp"となっていたらMatch Constraintだと思うといいと思います
Wrap Content コンテンツの大きさに依存する。(画像サイズや文字数など)
Match Parent ConstraintLayoutでは利用されるべきではない

Bias

大きさがwrap_contentまたはfixedになっていて、左右または上下に制約がついている時、biasを設定することができます。
この上と下にでている50というものです。
動かすと位置が移動します。
bias.gif

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

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

ConstraintLayoutを利用して、左寄せにしておいて、狭くなったときのマージンを確保しておくということもできたりして、より柔軟にレイアウトできます。
image.png

Chain

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

均等配置
(ちなみにChain Styleというものがあり、CHAIN_SPREAD -> CHAIN_SPREAD_INSIDE -> CHAIN_PACKEDの順番で動きます。見ればわかると思います。)
chain.gif

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

app:layout_constraintHorizontal_chainStyle="spread_inside"

一つのボタンをwidthを0dpにすることで、match constraintになって、こんな感じになります。
image.png

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

app:layout_constraintHorizontal_weight="1"

GuideLine

デザインツールでよく利用されるらしいガイドラインを引けます。
マテリアルデザインではKeyLineという名前で紹介されています。

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

上や横からのDP指定で線を引けるだけでなく、%指定することができます
以下の例では左から20dp、上から水平に50%で指定して、それを元にボタンを置いています。

%で指定するのは現状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

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

app:layout_goneMarginLeft="200dp"

gone.gif

1.1.0 Beta-1

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

Barrier

なぜ使うか?

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

通常時 長くなった時
image.png image.png

Settingsの部分の文字数が増えるとかぶっちゃいました。これをBarrierで改善しましょう。

使い方

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

image.png
そしてBarrierの下に避けさせたいものを持ってきます。仮想的にレイアウトを作っちゃうイメージです。
add.gif
Barrierを作る場所を指定します。CameraとSettingsのTextViewの右側なのでrightまたはendを指定します。
image.png

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

constraint.gif

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

testbarrier.gif

Group

なぜ使うか?

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

使い方

まずConstraintLayoutで右クリックしてAdd Groupします

image.png
Groupの中にViewを入れます。
group.gif

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

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

PlaceHolder

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

placeholder.gif

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

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

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

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

image.png

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

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の置き換えが快適になりそうです。

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

Google I/Oのアニメーションについて

このアニメーションについてです。
output_sicle.gif
コードはここにあります。
https://github.com/google/iosched/blob/master/lib/src/main/java/com/google/samples/apps/iosched/feed/FeedViewHolder.java#L51

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

    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の操作が出来きます
カスタムしたのを簡単に作ってみました。ちなみに勝手に作ることも公式にサポートされています。ちゃんとした使い方はちゃんとしたドキュメントが出てからになると思います。
blink.gif

レイアウトでの指定

    <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クラス

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/