カスタム レイアウト

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

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

シングルパス測定はパフォーマンスに優れており、Compose は深い UI ツリーを効率的に処理できます。要素が子を 2 回測定し、その子が自身の子のいずれかを 2 回測定した場合など、UI 全体をレイアウトしようとすると多くの作業が必要になるため、アプリのパフォーマンスを良好に保つことが難しくなります。しかし、子の測定 1 回でわかることに加えて、追加の情報が本当に必要な場合もあります。このような状況に効率的に対処できるアプローチについては、固有の測定値セクションで説明しています。

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

レイアウト修飾子の使用

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

fun Modifier.customLayoutModifier(...) =
    this.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 でのレイアウトの Codelab でカスタム レイアウトと修飾子の詳細をご覧になり、カスタム レイアウトを作成する Compose サンプルで API の実際の動作をご確認ください。