Google は、黒人コミュニティに対する人種平等の促進に取り組んでいます。取り組みを見る

Compose でのレイアウト

Jetpack Compose を使用すると、アプリの UI の設計と構築が簡単になります。このドキュメントでは、UI 要素のレイアウトに役立つ Compose のビルディング ブロックについて説明し、必要に応じてより特化したレイアウトを作成する方法を紹介します。

コンポーズ可能な関数は、Compose の基本的なビルディング ブロックです。コンポーズ可能な関数とは、UI の一部を記述する関数のことです。この関数は入力を受け取り、画面に表示されるものを生成します。コンポーザブルの詳細については、Compose のメンタルモデルについてのドキュメントをご覧ください。

コンポーズ可能な関数は、複数の UI 要素を出力することがあります。ただし、配置方法のガイダンスが提供されないと、Compose は望ましくない方法で要素を配置する可能性があります。たとえば、次のコードは 2 つのテキスト要素を生成します。

@Composable
fun ArtistCard() {
  Text("Alfred Sisley")
  Text("3 minutes ago")
}

配置方法のガイダンスがないと、Compose はテキスト要素を重ねてしまい、読み取れなくなります。

2 つのテキスト要素が重なって描画され、読み取れなくなる

Compose には、すぐに使用できるレイアウトのコレクションが用意されています。UI 要素の配置に役立ち、より特化した独自のレイアウトを簡単に定義できます。

標準レイアウト コンポーネント

多くの場合、Compose の標準レイアウト要素を使用するだけで済みます。

アイテムを画面上の垂直方向に配置するには、Column を使用します。

@Composable
fun ArtistCard() {
  Column {
    Text("Alfred Sisley")
    Text("3 minutes ago")
  }
}

2 つのテキスト要素を列レイアウトで配置して、テキストを読みやすくする

同様に、アイテムを画面上の水平方向に配置するには、Row を使用します。ColumnRow はどちらも、含まれる要素の gravity の構成をサポートしています。

@Composable
fun ArtistCard(artist: Artist) {
    Row(verticalGravity = Alignment.CenterVertically) {
        Image(...)
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

テキスト要素の列の横に小さなグラフィックが表示される、より複雑なレイアウトを示しています

ある要素を別の要素の上に配置するには、Stack を使用します。

column、row、stack という 3 つのシンプルなコンポーザブルの比較

ほとんどの場合、これらのビルディング ブロックだけで済みます。独自のコンポーズ可能な関数を作成することで、こうしたレイアウトを組み合わせ、アプリに適した、より手の込んだレイアウトにできます。

こうした基本的なレイアウトはそれぞれ、独自の gravity 設定を定義し、要素の配置方法を指定します。要素を構成するには、修飾子を使用します。

修飾子

修飾子を使用すると、コンポーザブルの表示方法を微調整できます。修飾子では、次のようなことができます。

  • コンポーザブルの動作と外観を変更する
  • ユーザー補助ラベルなどの情報を追加する
  • ユーザー入力を処理する
  • 要素をクリック可能、スクロール可能、ドラッグ可能、ズーム可能にするなど、高レベルの操作を追加する

修飾子は標準の Kotlin オブジェクトです。Modifier クラス関数のいずれかを呼び出して修飾子を作成します。こうした関数を連鎖させてコンポーズできます。

@Composable
fun ArtistCard(
    artist: Artist,
    onClick: () -> Unit
) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        Row(verticalGravity = Alignment.CenterVertically) { … }
        Spacer(Modifier.preferredSize(padding))
        Card(elevation = 4.dp) { … }
    }
}

修飾子を使用して、グラフィックの配置やユーザー入力に応答するエリアを変更する、さらに複雑なレイアウト

上記のコードでは、さまざまな修飾子関数を一緒に使用しています。

  • clickable() は、コンポーザブルをユーザー入力に反応させます。
  • padding() は、要素の周囲にスペースを入れます。
  • fillMaxWidth() は、コンポーザブルを親から与えられた最大幅に調整します。
  • preferredSize() は、要素の優先する幅と高さを指定します。

修飾子関数の順序は重要です。各関数は前の関数が返す Modifier を変更するため、順序は最終結果に影響を与えます。次の例をご覧ください。

@Composable
fun ArtistCard(...) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

エッジの周囲のパディングも含め、エリア全体がクリックに反応

上記のコードでは、padding 修飾子が clickable 修飾子の後に適用されるため、周囲のパディングを含むエリア全体がクリック可能となります。修飾子を他の順序で適用した場合、padding で追加されたスペースはユーザー入力に反応しません。

@Composable
fun ArtistCard(...) {
    val padding = 16.dp
    Column(
        Modifier
            .padding(padding)
            .clickable(onClick = onClick)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

レイアウトのエッジ周囲のパディングがクリックに反応しない

スクロール可能なレイアウト

ScrollableRow または ScrollableColumn を使用すると、Row または Column 内の要素をスクロールさせることができます。

@Composable
fun Feed(
  feedItems: List<Artist>,
  onSelected: (Artist) -> Unit
) {
  ScrollableColumn(Modifier.fillMaxSize()) {
    feedItems.forEach {
      ArtistCard(it, onSelected(it))
    }
  }
}

スクロール可能な列の複数の類似レイアウト

このアプローチは、表示する要素が少ない場合はうまく機能しますが、大規模なデータセットではすぐにパフォーマンスの問題となる可能性があります。画面に表示されている要素の一部のみを表示するには、LazyColumnFor または LazyRowFor を使用します。

@Composable
fun Feed(
  feedItems: List<Artist>,
  onSelected: (Artist) -> Unit
) {
  Surface(Modifier.fillMaxSize()) {
    LazyColumnFor(feedItems) { item ->
      ArtistCard(it, onSelected(it))
    }
  }
}

組み込みのマテリアル コンポーネント

Compose の最高レベルの UI 抽象化はマテリアル デザインです。Compose には、UI の構築を容易にするためにすぐに使える、さまざまなコンポーザブルが用意されています。DrawerFloatingActionButtonTopAppBar などの要素がすべて提供されます。

マテリアル コンポーネントはスロット API を多用します。これは、コンポーザブルの上にカスタマイズのレイヤを適用するために Compose で導入されたパターンです。スロットは UI に空のスペースを残し、デベロッパーが自由に使用できるようにします。たとえば、TopAppBar でカスタマイズできるスロットは次のとおりです。

UI 要素を追加できるアプリバーのスロットを示しています

コンポーザブルは通常、content コンポーザブル ラムダ(content: @Composable () -> Unit)を取ります。スロット API は、特定の用途のために複数の content パラメータを公開します。たとえば、TopAppBar を使用すると、titlenavigationIconactions のコンテンツを提供できます。

マテリアルで最も高レベルのコンポーザブルは Scaffold です。Scaffold を使用すると、マテリアル デザインの基本的なレイアウト構造で UI を実装できます。Scaffold には、TopAppBarBottomAppBarFloatingActionButtonDrawer など、最も一般的なトップレベルのマテリアル コンポーネント向けのスロットが用意されています。Scaffold を使用すると、こうしたコンポーネントを適切に配置し、ともに正しく動作させることが簡単になります。

Scaffold を使用してマテリアル デザインに合わせて要素を配置したレイアウト

@Composable
fun HomeScreen(...) {
    Scaffold (
        drawerContent = { ... },
        topBar = { ... },
        bodyContent = { ... }
    )
}

ConstraintLayout

ConstraintLayout は、コンポーザブルを画面上の他の要素に対し相対的に配置するのに役立ちます。複数の RowColumnStack 要素を使用する代わりの手段です。ConstraintLayout は、配置要件がより複雑な、大規模なレイアウトを実装する場合に便利です。

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 で配置されたボタンとテキスト要素を示しています

ConstraintLayout のその他の使用例については、レイアウトの Codelab をご覧ください。

API の分離

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

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

  1. ConstraintSet を、ConstraintLayout のパラメータとして渡します。
  2. ConstraintSet で作成した参照を、tag 修飾子を使用してコンポーザブルに割り当てます。
@Composable
fun DecoupledConstraintLayout() {
    WithConstraints {
        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.tag("button")
            ) {
                Text("Button")
            }

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

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

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

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

カスタム レイアウト

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

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

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

レイアウト修飾子の使用

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

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

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

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

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

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = Modifier.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.toIntPx() - 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.placeRelative(x, y) を呼び出して、画面上に子を配置します。子が配置されていないと、表示されません。y 位置は上パディング(テキストの最初のベースライン位置)に対応します。placeRelative は、右から左へのコンテキストで位置を自動的に反映します。

これが期待どおりに機能することを確認するには、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 修飾子は、コンポーザブルを 1 つだけ変更します。複数のコンポーザブルを手動で制御するには、代わりに Layout コンポーザブルを使用します。このコンポーザブルを使用することで、子を手動で測定して配置できます。ColumnRow などの上位レベルのレイアウトはすべて、Layout コンポーザブルで作成されます。

Column の簡単な実装を作成してみましょう。ほとんどのカスタム レイアウトは、このパターンに従います。

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

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

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    children: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = children
    ) { 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 制約によって制約され、前のコンポーザブルの yPosition に基づいて配置されます。

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

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

alt_text

レイアウト方向

コンポーザブルのレイアウト方向は、LayoutDirection アンビエントを使用して変更します。

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

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

詳細

詳細については、Jetpack Compose でのレイアウトの Codelab をご覧ください。