カスタム レイアウト

Compose では、UI 要素は呼び出されたときに UI の一部を出力するコンポーズ可能な関数によって表され、画面上にレンダリングされる UI ツリーに追加されます。各 UI 要素には親が 1 つあり、場合によっては多くの子があります。各要素は、(x, y) 位置として指定された親内の場所に、width および height で指定されたサイズで配置されます。

親は子要素の制約を定義します。要素は、これらの制約内で自身のサイズを定義するように要求されます。制約により、要素の widthheight の最小値と最大値が制限されます。要素に子要素がある場合、親の要素は、自身のサイズを判断しやすくするために、子のそれぞれを測定できます。要素が自身のサイズを決定して報告すると、カスタム レイアウトの作成で詳しく説明されているように、子要素を自身に対し相対的に配置する方法を定義できるようになります。

UI ツリー内の各ノードのレイアウトは、次の 3 つのステップで行います。各ノードは次のことを行う必要があります。

  1. すべての子を測定する
  2. ノード自体のサイズを決定する
  3. 子を配置する

ノード レイアウトの 3 つのステップ: 子の測定、サイズの決定、子の配置

スコープの使用により、子の測定と配置を行えるタイミングが決まります。レイアウトは測定パスとレイアウトパスの実行中のみ測定でき、子の配置はレイアウトパスの実行中に、測定した後でのみ行うことができます。この操作は、Compose スコープ(MeasureScopePlacementScope など)によりコンパイル時に適用されます。

レイアウト修飾子を使用する

layout 修飾子を使用して、要素の測定方法と配置方法を変更できます。Layout はラムダです。パラメータには、測定可能な要素(measurable として渡される)と、そのコンポーザブルの定義された制約(constraints として渡される)が含まれます。カスタム レイアウト修飾子は次のようになります。

fun Modifier.customLayoutModifier() =
    layout { measurable, constraints ->
        // ...
    }

画面に Text を表示し、上端からテキスト先頭行のベースラインまでの距離を制御してみましょう。これは paddingFromBaseline 修飾子の動作とまったく同じです。ここでは、これを例として実装します。そのためには、layout 修飾子を使用して、コンポーザブルを画面に手動で配置します。Text の上パディングを 24.dp に設定した場合の望ましい動作は次のとおりです。

要素間のスペースを設定する通常の UI パディングと、あるベースラインから次のベースラインまでのスペースを設定するテキスト パディングの違い

この間隔を生成するコードを次に示します。

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = layout { measurable, constraints ->
    // Measure the composable
    val placeable = measurable.measure(constraints)

    // Check the composable has a first baseline
    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline]

    // Height of the composable with padding - first baseline
    val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
    val height = placeable.height + placeableY
    layout(placeable.width, height) {
        // Where the composable gets placed
        placeable.placeRelative(0, placeableY)
    }
}

このコードの流れは次のとおりです。

  1. measurable ラムダ パラメータで measurable.measure(constraints) を呼び出して、測定可能なパラメータで表される Text を測定します。
  2. layout(width, height) メソッドを呼び出して、コンポーザブルのサイズを指定します。これにより、ラップされた要素の配置に使用するラムダも提供されます。この場合、最後のベースラインと追加された上パディングの間の高さです。
  3. placeable.place(x, y) を呼び出して、ラップされた要素を画面に配置します。配置されていないと、ラップされた要素は表示されません。y 位置は上パディングと、テキストの最初のベースライン位置との差に対応します。

これが期待どおりに機能することを確認するには、Text で次の修飾子を使用します。

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.padding(top = 32.dp))
    }
}

テキスト要素の複数のプレビュー(一方は要素間の通常のパディングを示し、もう一方はあるベースラインから次のベースラインまでのパディングを示している)

カスタム レイアウトを作成する

layout 修飾子は呼び出し元のコンポーザブルのみを変更します。複数のコンポーザブルを測定して配置するには、代わりに Layout コンポーザブルを使用します。このコンポーザブルを使用することで、子を手動で測定して配置できます。ColumnRow などの上位レベルのレイアウトはすべて、Layout コンポーザブルで作成されます。

Column の非常に基本的なバージョンを作成しましょう。ほとんどのカスタム レイアウトは、このパターンに従います。

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
        // ...
    }
}

layout 修飾子と同様に、measurables は測定する必要がある子のリストです。constraints は親からの制約です。前と同じロジックに従って、MyBasicColumn は次のように実装できます。

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each children
            measurable.measure(constraints)
        }

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Track the y co-ord we have placed children up to
            var yPosition = 0

            // Place children in the parent layout
            placeables.forEach { placeable ->
                // Position item on the screen
                placeable.placeRelative(x = 0, y = yPosition)

                // Record the y co-ord placed up to
                yPosition += placeable.height
            }
        }
    }
}

子コンポーザブルは Layout 制約によって制約され(minHeight で制約されていない場合)、前のコンポーザブルの yPosition に基づいて配置されます。

カスタム コンポーザブルの使用方法は次のとおりです。

@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
    MyBasicColumn(modifier.padding(8.dp)) {
        Text("MyBasicColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

列内で重ねられたいくつかのテキスト要素

レイアウト方向

コンポーザブルのレイアウト方向を変更するには、LocalLayoutDirection コンポジション ローカルを変更します。

コンポーザブルを手動で画面に配置する場合、LayoutDirection は、layout 修飾子または Layout コンポーザブルの LayoutScope の一部になります。

layoutDirection を使用する場合は、place を使用してコンポーザブルを配置します。placeRelative メソッドと異なり、place はレイアウト方向(左から右 / 右から左)に基づいて変更されることはありません。

カスタム レイアウトの実例

レイアウトと修飾子の詳細については、Jetpack Compose でのレイアウトをご覧ください。また、カスタム レイアウトの実例については、カスタム レイアウトを作成する Compose サンプルをご確認ください。

詳細

Compose のカスタム レイアウトについて詳しくは、以下の参考情報をご覧ください。

動画