はじめに
- 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 コンポーザブル
@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 不使用)
-
ConstraintLayout
コンポーザブルを呼び出す - 1の
content
でcreateRef()
またはcreateRefs()
を使って制約の参照を生成 - 制約によって配置される側の
modifier
に、2で生成した参照を利用して制約を設定
参照の生成
参照は createRef()
で1つ1つ作るか、もしくは createRefs()
でまとめて作る
createRef()
fun createRef(): ConstrainedLayoutReference
-
val textRef = createRef()
という具合に使う
createRefs()
fun createRefs(): ConstraintLayoutScope.ConstrainedLayoutReferences
-
val (textRef, imageRef) = createRefs()
という具合に使う - ここでは参照を2つまとめて作っているが、3つ以上まとめて作ることも可能
制約の設定
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 使用)
制約の定義とデザインの定義を分離してレイアウトする方法
-
ConstraintSet
コンポーザブルでConstraintSet
を生成 - 1の
description
引数に与えた関数内で createRefFor() または createRefsFor() で参照を生成 - 2で生成した参照を引数に
constrain
関数を呼び出してlinkTo
などで制約を設定 - 1で生成した
ConstraintSet
をConstraintLayout
のconstraintSet
パラメータに与える - 4の
ConstraintLayout()
のcontent
パラメータでデザインを定義する際、2のcreateRefFor()
またはcreateRefsFor()
で生成した参照に付与したIDを、各 Composable のModifier.layoutId()
で指定
ConstraintSet の生成
APIリファレンス (オーバーロードあり)
fun ConstraintSet(description: ConstraintSetScope.() -> Unit): ConstraintSet
参照の生成
参照は createRefFor()
で1つ1つ作るか、もしくは createRefsFor()
でまとめて作る
createRefFor()
fun createRefFor(id: Any): ConstrainedLayoutReference
createRefsFor()
ConstraintLayout-compose バージョン 1.1.0-alpha13 以降でないと使えないことに注意
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
-
createGuidelineFromXXXXX
で Guideline を生成- ConstraintLayoutBaseScope に Guideline を生成するメソッドがいくつも用意されている
- createGuidelineFromStart や createGuidelineFromTop など
- 便宜上 “Guideline” と呼ぶが、これらの関数で生成されるのは
ConstraintLayoutBaseScope.VerticalAnchor
やConstraintLayoutBaseScope.HorizontalAnchor
などであり “Guideline” という型のオブジェクトではない点に注意
- 生成した Guideline に対して制約を設定する
- UIや
parent
と異なり、生成された Guideline の参照に対して制約を張ればよく、top
やstart
を意識する必要はない
- UIや
サンプルコード
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
-
createXXXXXBarrier
で Barrier を生成- ConstraintLayoutBaseScope には createBottomBarrier や createStartBarrier など、 Barrier を生成するメソッドがいくつも用意されている
- 便宜上 “Barrier” と呼ぶが、こちらも実際に生成されるのは
ConstraintLayoutBaseScope.VerticalAnchor
やConstraintLayoutBaseScope.HorizontalAnchor
などで、 “Barrier” という型のオブジェクトでない点に注意
- 生成した Barrier に対して制約を設定する
- これも Guideline と同じく、生成された Barrier の参照に対して制約を張ればよい(
top
やstart
を意識する必要はない)
- これも Guideline と同じく、生成された Barrier の参照に対して制約を張ればよい(
サンプルコード
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
-
createXXXXXChain
で Chain を生成。 Chain を設定するUIへの参照を引数に与える- Chain を設定する参照は
vararg
となっており可変で、特に上限数は無い模様 - 引数
chainStyle
にChainStyle
クラスの定数Packed
,Spread
,SpreadInside
を指定する - Guideline や Barrier と違い
createXXXXXChain
を呼び出すだけで成立するので、関数の戻り値を意識する必要はない
- Chain を設定する参照は
- ConstraintLayoutBaseScope に Chain を生成するメソッドが用意されている
Chain を成立させる関数
Guideline や Barrier と違い、 Chain 関連の関数は2つだけ
戻り値の型もわりとそのまんまの名前
createHorizontalChain
fun createHorizontalChain(
vararg elements: LayoutReference,
chainStyle: ChainStyle = ChainStyle.Spread
): HorizontalChainReference
createVerticalChain
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
- 固定値は以下のどちらか。なお
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
-
- 制約に応じたサイズを設定する場合でも、
width
とheight
には適切な値をセットした方がよい- 期待する結果ではないレイアウトがされてしまう場合がある。具体的には以下
(参考)LazyColumn と Button を縦に並べた場合
前提条件
-
Button
の制約はbottom.linkTo(parent.bottom)
-
LazyColumn
の制約はtop.linkTo(parent.top)
かつbottom.linkTo(button.top)
-
Button
とLazyColumn
の間には16.dp
のマージンを設定 - 左図は
height
にDimension.fillToConstraint
を指定し、右図ではheight
を指定していない
結果について
- 右図は
height
にデフォルト値であるDimension.wrapContent
が採用された結果、LazyColumn
の一部がボタンにオーバーレイしてしまっている -
LazyColumn
とButton
の間にマージンもセットしている。左図では有効だが、右図では無視されてしまっている - 制約を設定した上で各 Composable に適切なサイズを指定することで、「自身の領域は
top
とbottom
でlinkTo
した部分のすべて」であると明示する。それによって、予想外のレイアウトがされてしまう危険性を減らす