Compose の ConstraintLayout

ConstraintLayout は画面上の他のコンポーザブルを基準にコンポーザブルを配置できるレイアウトです。複数のネストされた RowColumnBox 要素や他のカスタムのレイアウト要素を使用する代わりの手段です。ConstraintLayout は、配置要件がより複雑な、大規模なレイアウトを実装する場合に便利です。

以下のシナリオでは ConstraintLayout の使用をご検討ください。

  • 画面上の要素を配置するために複数の ColumnRow をネストすることを回避し、コードを読みやすくする。
  • 他のコンポーザブルを基準にコンポーザブルを配置する、またはガイドライン、バリア、チェーンに基づいてコンポーザブルを配置する。

注: View システムでは、大規模で複雑なレイアウトを作成する場合、ConstraintLayout を使用することが推奨されていました。これは、ネストされたビューよりもフラットなビュー階層の方がパフォーマンスに優れているためです。しかし、深いレイアウト階層を効率的に扱える Compose では、このような懸念はありません。

ConstraintLayout をはじめよう

Compose で ConstraintLayout を使用するには、(Compose の設定に加えて)build.gradle に次の依存関係を追加する必要があります。

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"

Compose の ConstraintLayoutDSL を使用して次のように動作します。

  • createRefs() または createRefFor() を使用して ConstraintLayout の各コンポーザブルの参照を作成します。
  • 制約は、参照をパラメータとして受け取る constrainAs() 修飾子を使用して指定します。この修飾子により、本文のラムダで制約を指定できます。
  • 制約は、linkTo() またはその他の便利なメソッドを使用して指定します。
  • parent は、ConstraintLayout コンポーザブル自体に対する制約を指定するために使用できる、既存の参照です。

ConstraintLayout を使用したコンポーザブルの例を次に示します。

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Create references for the composables to constrain
        val (button, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            // Assign reference "button" to the Button composable
            // and constrain it to the top of the ConstraintLayout
            modifier = Modifier.constrainAs(button) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button")
        }

        // Assign reference "text" to the Text composable
        // and constrain it to the bottom of the Button composable
        Text(
            "Text",
            Modifier.constrainAs(text) {
                top.linkTo(button.bottom, margin = 16.dp)
            }
        )
    }
}

このコードは、Button の頂部を親に対しマージン 16.dp に制約し、TextButton の底部に対しマージン 16.dp に制約します。

ConstraintLayout で配置されたボタンとテキスト要素を示しています

API の分離

ConstraintLayout の例では、制約はインラインで指定され、適用対象のコンポーザブルで修飾子が付けられていました。しかし、適用対象のレイアウトから制約を分離した方がよい場合もあります。たとえば、画面構成に基づいて制約を変更する場合や、2 つの制約セットの間でアニメーション化を行う場合があります。

このような場合は、別の方法で ConstraintLayout を使用できます。

  1. ConstraintSet を、ConstraintLayout のパラメータとして渡します。
  2. ConstraintSet で作成した参照を、layoutId 修飾子を使用してコンポーザブルに割り当てます。

@Composable
fun DecoupledConstraintLayout() {
    BoxWithConstraints {
        val constraints = if (minWidth < 600.dp) {
            decoupledConstraints(margin = 16.dp) // Portrait constraints
        } else {
            decoupledConstraints(margin = 32.dp) // Landscape constraints
        }

        ConstraintLayout(constraints) {
            Button(
                onClick = { /* Do something */ },
                modifier = Modifier.layoutId("button")
            ) {
                Text("Button")
            }

            Text("Text", Modifier.layoutId("text"))
        }
    }
}

private fun decoupledConstraints(margin: Dp): ConstraintSet {
    return ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        constrain(button) {
            top.linkTo(parent.top, margin = margin)
        }
        constrain(text) {
            top.linkTo(button.bottom, margin)
        }
    }
}

その後、制約を変更する必要がある場合は、別の ConstraintSet を渡すだけで済みます。

ConstraintLayout のコンセプト

ConstraintLayout には、コンポーザブル内での要素の配置に役立つ、ガイドライン、バリア、チェーンなどのコンセプトがあります。

ガイドライン

ガイドラインは、レイアウトの設計に使用する小さいビジュアル ヘルパーです。コンポーザブルは、ガイドラインに制約することができます。ガイドラインは、親コンポーザブル内に特定の dppercentage で要素を配置する場合に便利です。

ガイドラインには、垂直方向と水平方向の 2 つの種類があります。水平方向は topbottom、垂直方向は startend です。

ConstraintLayout {
    // Create guideline from the start of the parent at 10% the width of the Composable
    val startGuideline = createGuidelineFromStart(0.1f)
    // Create guideline from the end of the parent at 10% the width of the Composable
    val endGuideline = createGuidelineFromEnd(0.1f)
    //  Create guideline from 16 dp from the top of the parent
    val topGuideline = createGuidelineFromTop(16.dp)
    //  Create guideline from 16 dp from the bottom of the parent
    val bottomGuideline = createGuidelineFromBottom(16.dp)
}

ガイドラインを作成するには、必要なガイドラインの種類で createGuidelineFrom* を使用します。これにより、Modifier.constrainAs() ブロックで使用できる参照が作成されます。

バリア

バリアは、複数のコンポーザブルを参照し、指定された側の最も遠いウィジェットに基づく仮想ガイドラインを作成します。

バリアを作成するには、createTopBarrier()(または createBottomBarrier()createEndBarrier()createStartBarrier())を使用し、バリアを構成する参照を提供します。

ConstraintLayout {
    val constraintSet = ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        val topBarrier = createTopBarrier(button, text)
    }
}

作成したバリアは、Modifier.constrainAs() ブロックで使用できます。

チェーン

チェーンは、1 つの軸(水平または垂直)でグループのような動作を提供します。もう 1 つの軸は別個に制約できます。

チェーンを作成するには、createVerticalChain または createHorizontalChain を使用します。

ConstraintLayout {
    val constraintSet = ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        val verticalChain = createVerticalChain(button, text, chainStyle = ChainStyle.Spread)
        val horizontalChain = createHorizontalChain(button, text)
    }
}

作成したチェーンは Modifier.constrainAs() ブロックで使用できます。

チェーンは、コンポーザブルの周囲のスペースを処理する方法を決める、以下のようなさまざまな ChainStyles で構成できます。

  • ChainStyle.Spread: スペースは、最初のコンポーザブルの前と最後のコンポーザブルの後の空きスペースを含め、すべてのコンポーザブル間で均等に配分されます。
  • ChainStyle.SpreadInside: スペースは、最初のコンポーザブルの前と最後のコンポーザブルの後に空きスペースを入れずに、すべてのコンポーザブル間で均等に配分されます。
  • ChainStyle.Packed: スペースは最初のコンポーザブルの前と最後のコンポーザブルの後に配分され、コンポーザブルは、相互の間にスペースを入れずにまとめてパックされます。

詳細

Compose の ConstraintLayout について、実際に動作する API の詳細: ConstraintLayout を使用する Compose サンプル