Compose での ConstraintLayout に関するまとめ

## はじめに

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

## ダウンロード

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

- バージョンカタログ

```kotlin
androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintLayoutCompose" }
```

- build.gradle.kts

```kotlin
implementation(libs.androidx.constraintlayout.compose)
```

## ConstraintLayout コンポーザブル  

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/package-summary?hl=en#ConstraintLayout(androidx.constraintlayout.compose.ConstraintSet,androidx.compose.ui.Modifier,kotlin.Int,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec,kotlin.Function0,kotlin.Function0))

```kotlin
@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の `content` で `createRef()` または `createRefs()` を使って制約の参照を生成
3. 制約によって配置される側の `modifier` に、2で生成した参照を利用して制約を設定

### 参照の生成

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

#### createRef()

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutScope#createRef())

```kotlin
fun createRef(): ConstrainedLayoutReference
```

- `val textRef = createRef()` という具合に使う

#### createRefs()

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutScope#createRefs())

```kotlin
fun createRefs(): ConstraintLayoutScope.ConstrainedLayoutReferences
```

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

### 制約の設定

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutScope?hl=en#(androidx.compose.ui.Modifier).constrainAs(androidx.constraintlayout.compose.ConstrainedLayoutReference,kotlin.Function1))

```kotlin
fun Modifier.constrainAs(
    ref: ConstrainedLayoutReference,
    constrainBlock: ConstrainScope.() -> Unit
): Modifier
```

- ⁠`Modifier.constrainAs()` で、制約を設定するための `Modifier` を生成する
- `ref` には `createRef` や `createRefs` で生成した参照をセット
- `constrainBlock` 内で、自身から相手方への参照に対して制約を設定する
    - `constrainBlock` の中で [ConstrainedLayoutReference](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstrainedLayoutReference) のプロパティである `top` や `start` などをレシーバーに、 [linkTo()](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstrainScope#linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor,androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,kotlin.Float)) や [centerTo()](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstrainScope#centerTo(androidx.constraintlayout.compose.ConstrainedLayoutReference)) や [centerVerticallyTo()](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstrainScope#centerVerticallyTo(androidx.constraintlayout.compose.ConstrainedLayoutReference,kotlin.Float)) を呼び出す
    - `linkTo` の引数には、制約の相手型に対応する参照をセットする。 `app:layout_ConstraintTop_toBottomOf` などを使うときと同じイメージ
    - `linkTo` の第2引数 `margin` にはマージンも設定できる

#### センタリング

- `linkTo` を複数組み合わせることでもできるが、 `centerTo` や `centerHorizontally` などを使えば1行で設定できる

```kotlin
// 自身を親の中央に配置する場合
centerTo(parent)

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

### サンプルコード

```kotlin
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()](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintSetScope?hl=en#createRefFor(kotlin.Any)) または [createRefsFor()](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintSetScope?hl=en#createRefsFor(kotlin.Array)) で参照を生成
3. 2で生成した参照を引数に `constrain` 関数を呼び出して `linkTo` などで制約を設定
4. 1で生成した `ConstraintSet` を `ConstraintLayout` の `constraintSet` パラメータに与える
5. 4の `ConstraintLayout()` の `content` パラメータでデザインを定義する際、2の `createRefFor()` または `createRefsFor()` で生成した参照に付与したIDを、各 Composable の⁠ `Modifier.layoutId()` で指定

### ConstraintSet の生成

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/package-summary#ConstraintSet(kotlin.Function1)) （オーバーロードあり）

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

### 参照の生成

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

#### createRefFor()

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintSetScope?hl=en#createRefFor(kotlin.Any))

```kotlin
fun createRefFor(id: Any): ConstrainedLayoutReference
```

#### createRefsFor()

[ConstraintLayout-compose バージョン 1.1.0-alpha13](https://developer.android.com/jetpack/androidx/releases/constraintlayout?hl=ja#1.1.0-alpha13) 以降でないと使えないことに注意

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintSetScope?hl=en#createRefsFor(kotlin.Array))

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

### サンプルコード

```kotlin
// 制約を定義した 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](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope) に Guideline を生成するメソッドがいくつも用意されている
    - [createGuidelineFromStart](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope#createGuidelineFromStart(kotlin.Float)) や [createGuidelineFromTop](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope#createGuidelineFromTop(kotlin.Float)) など
    - 便宜上 “Guideline” と呼ぶが、これらの関数で生成されるのは `ConstraintLayoutBaseScope.VerticalAnchor` や `ConstraintLayoutBaseScope.HorizontalAnchor` などであり “Guideline” という型のオブジェクトではない点に注意
2. 生成した Guideline に対して制約を設定する
    - UIや `parent` と異なり、生成された Guideline の参照に対して制約を張ればよく、 `top` や `start` を意識する必要はない

#### サンプルコード

```kotlin
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](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope) には [createBottomBarrier](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope#createBottomBarrier(kotlin.Array,androidx.compose.ui.unit.Dp)) や [createStartBarrier](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope#createStartBarrier(kotlin.Array,androidx.compose.ui.unit.Dp)) など、 Barrier を生成するメソッドがいくつも用意されている
    - 便宜上 “Barrier” と呼ぶが、こちらも実際に生成されるのは `ConstraintLayoutBaseScope.VerticalAnchor` や `ConstraintLayoutBaseScope.HorizontalAnchor` などで、 “Barrier” という型のオブジェクトでない点に注意
2. 生成した Barrier に対して制約を設定する
    - これも Guideline と同じく、生成された Barrier の参照に対して制約を張ればよい（ `top` や `start` を意識する必要はない）

#### サンプルコード

```kotlin
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` となっており可変で、特に上限数は無い模様
    - 引数 `chainStyle` に `ChainStyle` クラスの定数 `Packed`, `Spread`, `SpreadInside` を指定する
    - Guideline や Barrier と違い `createXXXXXChain` を呼び出すだけで成立するので、関数の戻り値を意識する必要はない
2. [ConstraintLayoutBaseScope](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope) に Chain を生成するメソッドが用意されている

#### Chain を成立させる関数

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

##### createHorizontalChain

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope#createHorizontalChain(kotlin.Array,androidx.constraintlayout.compose.ChainStyle))

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

##### createVerticalChain

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope#createVerticalChain(kotlin.Array,androidx.constraintlayout.compose.ChainStyle))

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

#### サンプルコード

```kotlin
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リファレンス](https://developer.android.com/reference/kotlin/androidx/constraintlayout/compose/Dimension)

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

#### サンプルコード

```kotlin
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` を指定していない

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/f322be2f-28f1-b7b7-ca76-a7107a671ab9.png" width="240" />　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/72c3ca9c-4157-0311-ae86-d7893d676ce2.png" width="240" />

#### 結果について

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

- [ConstraintLayout in Compose - Android Developers](https://developer.android.com/develop/ui/compose/layouts/constraintlayout)