0
1

Compose での ConstraintLayout に関するまとめ

Last updated at Posted at 2024-08-05

はじめに

  • Jetpack Compose で ConstraintLayout を使うときの参考に作成しました
  • 記事を作成した時点での Android Studio のバージョンは「Koala | 2024.1.1」です
    • Build は「#AI-241.15989.150.2411.11948838, built on June 11, 2024」でした
  • 不備や認識違いがありましたら、指摘頂けると大変助かります

ダウンロード

Compose の設定に加え、以下の依存関係を設定する

  • バージョンカタログ
androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintLayoutCompose" }
  • build.gradle.kts
implementation(libs.androidx.constraintlayout.compose)

ConstraintLayout コンポーザブル

APIリファレンス

@Composable
inline fun ConstraintLayout(
    constraintSet: ConstraintSet,
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    animateChanges: Boolean = false,
    animationSpec: AnimationSpec<Float> = tween<Float>(),
    noinline finishedAnimationListener: (() -> Unit)? = null,
    crossinline content: @Composable () -> Unit
): Unit

レイアウトの流れ(ConstraintSet 不使用)

  1. ConstraintLayout コンポーザブルを呼び出す
  2. 1の contentcreateRef() または createRefs() を使って制約の参照を生成
  3. 制約によって配置される側の modifier に、2で生成した参照を利用して制約を設定

参照の生成

参照は createRef() で1つ1つ作るか、もしくは createRefs() でまとめて作る

createRef()

APIリファレンス

fun createRef(): ConstrainedLayoutReference
  • val textRef = createRef() という具合に使う

createRefs()

APIリファレンス

fun createRefs(): ConstraintLayoutScope.ConstrainedLayoutReferences
  • val (textRef, imageRef) = createRefs() という具合に使う
  • ここでは参照を2つまとめて作っているが、3つ以上まとめて作ることも可能

制約の設定

APIリファレンス

fun Modifier.constrainAs(
    ref: ConstrainedLayoutReference,
    constrainBlock: ConstrainScope.() -> Unit
): Modifier
  • Modifier.constrainAs() で、制約を設定するための Modifier を生成する
  • ref には createRef や createRefs で生成した参照をセット
  • constrainBlock 内で、自身から相手方への参照に対して制約を設定する
    • constrainBlock の中で ConstrainedLayoutReference のプロパティである top や start などをレシーバーに、 linkTo() や centerTo() や centerVerticallyTo() を呼び出す
    • linkTo の引数には、制約の相手型に対応する参照をセットする。 app:layout_ConstraintTop_toBottomOf などを使うときと同じイメージ
    • linkTo の第2引数 margin にはマージンも設定できる

センタリング

  • linkTo を複数組み合わせることでもできるが、 centerTo や centerHorizontally などを使えば1行で設定できる
// 自身を親の中央に配置する場合
centerTo(parent)

// 自身を親の左右中央に配置する場合
centerHorizontally(parent)

サンプルコード

ConstraintLayout {
    val (imageRef, boxRef) = createRefs()

    Image(
        modifier = Modifier.constrainAs(imageRef) {
            width = Dimension.matchParent
            height = Dimension.fillToConstraints
            top.linkTo(parent.top)
        
            // Boxとの間に12dpのマージンを設ける
            bottom.linkTo(boxRef.top, margin = 12.dp)
        },
        painter = painterResource(id = R.drawable.picture_11),
        contentDescription = null,
    )

    // Boxの幅をImageの幅に合わせる
    Box(
        modifier = Modifier.constrainAs(boxRef) {
            width = Dimension.fillToConstraints
            height = Dimension.value(96.dp)
            start.linkTo(imageRef.start)
            end.linkTo(imageRef.end)
            bottom.linkTo(parent.bottom)
        }
        contentAlignment = Alignment.Center,
    ) {
        Text(text = ...)
    }
}

レイアウトの流れ(ConstraintSet 使用)

制約の定義とデザインの定義を分離してレイアウトする方法

  1. ConstraintSet コンポーザブルで ConstraintSet を生成
  2. 1の description 引数に与えた関数内で createRefFor() または createRefsFor() で参照を生成
  3. 2で生成した参照を引数に constrain 関数を呼び出して linkTo などで制約を設定
  4. 1で生成した ConstraintSetConstraintLayoutconstraintSet パラメータに与える
  5. 4の ConstraintLayout()content パラメータでデザインを定義する際、2の createRefFor() または createRefsFor() で生成した参照に付与したIDを、各 Composable の⁠ Modifier.layoutId() で指定

ConstraintSet の生成

APIリファレンス (オーバーロードあり)

fun ConstraintSet(description: ConstraintSetScope.() -> Unit): ConstraintSet

参照の生成

参照は createRefFor() で1つ1つ作るか、もしくは createRefsFor() でまとめて作る

createRefFor()

APIリファレンス

fun createRefFor(id: Any): ConstrainedLayoutReference

createRefsFor()

ConstraintLayout-compose バージョン 1.1.0-alpha13 以降でないと使えないことに注意

APIリファレンス

fun createRefsFor(vararg ids: Any): ConstraintSetScope.ConstrainedLayoutReferences

サンプルコード

// 制約を定義した ConstraintSet
val constraintSet = ConstraintSet {
    /*
     * IDを付与した参照を生成。ちなみにIDは Any なので、文字列でなくてもOK
     * 
     * また createRefsFor() が使えるなら以下でもよい
     * val (imageRef, buttonRef) = createRefsFor("image", "button")
     */
    val imageRef = createRefFor("image")
    val boxRef = createRefFor("box")

    // imageRef についての制約
    constrain(imageRef) {
        width = Dimension.matchParent
        height = Dimension.fillToConstraints
        top.linkTo(parent.top)
        bottom.linkTo(boxRef.top, margin = 12.dp)
    }

    // boxRef についての制約
    constrain(boxRef) {
        width = Dimension.fillToConstraints
        height = Dimension.value(96.dp)
        start.linkTo(imageRef.start)
        end.linkTo(imageRef.end)
        bottom.linkTo(parent.bottom)
    }
}

// ConstraintLayout のパラメータに ConstraintSet をセットする
ConstraintLayout(constraintSet = constraintSet) {
    Image(
        // Modifier.layoutId に createRefFor で生成した参照のIDをセット
        modifier = Modifier.layoutId("image"),
        painter = painterResource(id = R.drawable.picture_11),
        contentDescription = null,
    )

    Box(
        modifier = Modifier.layoutId("box"),
        contentAlignment = Alignment.Center,
    ) {
        Text(text = ... )
    }
}

UI以外に対する制約

Guideline

  1. createGuidelineFromXXXXX で Guideline を生成
    • ConstraintLayoutBaseScope に Guideline を生成するメソッドがいくつも用意されている
    • createGuidelineFromStartcreateGuidelineFromTop など
    • 便宜上 “Guideline” と呼ぶが、これらの関数で生成されるのは ConstraintLayoutBaseScope.VerticalAnchorConstraintLayoutBaseScope.HorizontalAnchor などであり “Guideline” という型のオブジェクトではない点に注意
  2. 生成した Guideline に対して制約を設定する
    • UIや parent と異なり、生成された Guideline の参照に対して制約を張ればよく、 topstart を意識する必要はない

サンプルコード

val ref = createRef()

// parent の上部から30%の位置に Guideline を生成
val guideline = createGuidelineFromTop(0.3f)

// Text の上部を parent に、下部を Guideline に合わせる
Text(
    text = ...,
    modifier = Modifier.constrainAs(ref) {
        top.linkTo(parent.top)
        bottom.linkTo(guideline)
        ...
    }
)

Barrier

  1. createXXXXXBarrier で Barrier を生成
    • ConstraintLayoutBaseScope には createBottomBarriercreateStartBarrier など、 Barrier を生成するメソッドがいくつも用意されている
    • 便宜上 “Barrier” と呼ぶが、こちらも実際に生成されるのは ConstraintLayoutBaseScope.VerticalAnchorConstraintLayoutBaseScope.HorizontalAnchor などで、 “Barrier” という型のオブジェクトでない点に注意
  2. 生成した Barrier に対して制約を設定する
    • これも Guideline と同じく、生成された Barrier の参照に対して制約を張ればよい( topstart を意識する必要はない)

サンプルコード

val (ref1, ref2, ref3) = createRefs()

// UIの下(Bottom)をベースにする Barrier を生成。この場合は ref1 と ref2 をつけたUIがバリアの対象になる
val barrier = createBottomBarrier(ref1, ref2)

Text(text = "1", modifier = Modifier..constrainAs(ref1) { ... })
Text(text = "2", modifier = Modifier.constrainAs(ref2) { start.linkTo(ref1.end) })

// 1 と 2 の下部に Barrier があるので、 1 と 2 のうち高さがある方の下部に配置される
Text(text = "3", modifier = constrainAs(ref3) { top.linkTo(barrier) })

Chain

  1. createXXXXXChain で Chain を生成。 Chain を設定するUIへの参照を引数に与える
    • Chain を設定する参照は vararg となっており可変で、特に上限数は無い模様
    • 引数 chainStyleChainStyle クラスの定数 Packed, Spread, SpreadInside を指定する
    • Guideline や Barrier と違い createXXXXXChain を呼び出すだけで成立するので、関数の戻り値を意識する必要はない
  2. ConstraintLayoutBaseScope に Chain を生成するメソッドが用意されている

Chain を成立させる関数

Guideline や Barrier と違い、 Chain 関連の関数は2つだけ
戻り値の型もわりとそのまんまの名前

createHorizontalChain

APIリファレンス

fun createHorizontalChain(
    vararg elements: LayoutReference,
    chainStyle: ChainStyle = ChainStyle.Spread
): HorizontalChainReference
createVerticalChain

APIリファレンス

fun createVerticalChain(
    vararg elements: LayoutReference,
    chainStyle: ChainStyle = ChainStyle.Spread
): VerticalChainReference

サンプルコード

val (ref1, ref2, ref3) = createRefs()

// Chain をかける参照と chainStyle をセット
// この時点で ref1, ref2, ref3 の間に Chain が成立する( linkTo で制約を設ける必要はある)
createHorizontalChain(ref1, ref2, ref3, chainStyle = ChainStyle.Packed)

Text(
    text = "1",
    modifier = Modifier.background(color = Color.Red).constrainAs(ref1) {
        start.linkTo(parent.start)
        end.linkTo(ref2.start)
    }
)

Text(
    text = "2",
    modifier = Modifier.background(color = Color.Blue).constrainAs(ref2) {
        start.linkTo(ref1.end)
        end.linkTo(ref3.start)
    }
)

Text(
    text = "3",
    modifier = Modifier.background(color = Color.LightGray).constrainAs(ref3) {
        start.linkTo(ref2.end)
        end.linkTo(parent.end)
    }
)

サイズについて

Dimension

APIリファレンス

  • 固定値は以下のどちらか。なお preferredValue だと、空いている領域によって実際のサイズが変動する
    • Dimension.value
    • Dimension.preferredValue
  • 制約上限いっぱいまで広げたい( match_parent 相当)場合は Dimension.fillToConstraints
  • コンテンツの表示に適当なサイズ( wrap_content 相当)とするなら以下のどちらか
    • Dimension.wrapContent
    • Dimension.preferredWrapContent 
  • 制約上限に対して%を指定する場合は Dimension.percent(Float)

サンプルコード

width = Dimension.value(60.dp)      // 固定値
width = Dimension.fillToConstraints // 制約上限いっぱい
width = Dimension.wrapContent       // コンテンツの表示に必要なサイズ (wrap_content)
width = Dimension.matchParent       // 親と同じサイズ (match_parent)
width = Dimension.percent(0.5f)     // %指定

width / height

  • constrainAs() の constrainBlock で width や height パラメータに設定する
    • Dimension インターフェイスに定義されている定数を使う(上述)
    • デフォルト値は Dimension.wrapContent 
  • 制約に応じたサイズを設定する場合でも、 widthheight には適切な値をセットした方がよい
    • 期待する結果ではないレイアウトがされてしまう場合がある。具体的には以下

(参考)LazyColumn と Button を縦に並べた場合

前提条件
  • Button の制約は bottom.linkTo(parent.bottom)
  • LazyColumn の制約は top.linkTo(parent.top) かつ bottom.linkTo(button.top) 
  • ButtonLazyColumn の間には 16.dp のマージンを設定
  • 左図は heightDimension.fillToConstraint を指定し、右図では height を指定していない

 

結果について

  • 右図は height にデフォルト値である Dimension.wrapContent が採用された結果、 LazyColumn の一部がボタンにオーバーレイしてしまっている
  • LazyColumnButton の間にマージンもセットしている。左図では有効だが、右図では無視されてしまっている
  • 制約を設定した上で各 Composable に適切なサイズを指定することで、「自身の領域は top と bottom で linkTo した部分のすべて」であると明示する。それによって、予想外のレイアウトがされてしまう危険性を減らす

関連リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1