Compose でのレイアウト

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

Compose でのレイアウトの目標

Jetpack Compose でのレイアウト システムの実装には、パフォーマンスの向上と、カスタム レイアウトの容易な作成の 2 つの主な目的があります。Compose では、レイアウトの子を複数回測定するのを防ぐことで、高パフォーマンスを実現できます。複数回の測定が必要な場合、Compose には、固有の測定値という特別なシステムが用意されています。この機能について詳しくは、固有の測定値のセクションをご覧ください。

コンポーズ可能な関数の基本

コンポーズ可能な関数は、Compose の基本的なビルディング ブロックです。コンポーズ可能な関数とは、UI の一部を記述する Unit を出力する関数のことです。この関数は入力を受け取り、画面に表示されるものを生成します。コンポーザブルの詳細については、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 はどちらも、含まれる要素の配置の構成をサポートしています。

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

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

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

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

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

Row 内の子の位置を設定するには、horizontalArrangement 引数と verticalAlignment 引数を設定します。Column の場合は、verticalArrangement 引数と horizontalAlignment 引数を設定します。

@Composable
fun ArtistCard(artist: Artist) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.End
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

アイテムは右揃え

修飾子

修飾子を使用すると、コンポーザブルを装飾または拡張できます。修飾子では、次のようなことができます。

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

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

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

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

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

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

修飾子の順序の重要性

修飾子関数の順序は重要です。各関数は前の関数が返す 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
    }
}

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

組み込み修飾子

Jetpack Compose には、コンポーザブルの装飾や拡張に役立つ組み込み修飾子のリストが用意されています。paddingclickablefillMaxWidth などの修飾子がすでに導入されており、他にも、次のリストに示すような一般的な修飾子があります。

size

Compose で提供されるレイアウトは、デフォルトでは子をラップしていますが、size 修飾子を使用してサイズを設定できます。

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

指定したサイズがレイアウトの親の制約を満たさない場合、そのサイズが適用されないことがあります。親の制約に関係なくコンポーザブルのサイズを固定する必要がある場合は、requiredSize 修飾子を使用します。

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.requiredSize(150.dp)
        )
        Column { /*...*/ }
    }
}

子の画像が親の制約よりも大きい

この例では、親 height100.dp に設定されていても、requiredSize 修飾子が優先されるため、Image の高さは 150.dp になっています。

親によって許可されているすべての高さを子レイアウトで埋めるには、fillMaxHeight 修飾子を追加します(Compose では、fillMaxSizefillMaxWidth も提供されています)。

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.fillMaxHeight()
        )
        Column { /*...*/ }
    }
}

画像の高さが親と同じ

テキストのベースラインの上にパディングを追加して、レイアウトの上部からベースラインまで一定の距離を空ける場合は、paddingFromBaseline 修飾子を使用します。

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(
                text = artist.name,
                modifier = Modifier.paddingFromBaseline(top = 50.dp)
            )
            Text(artist.lastSeenOnline)
        }
    }
}

上にパディングが設定されたテキスト

オフセット

レイアウトを元の位置に対して相対的に配置するには、次のように offset 修飾子を追加し、x 軸と y 軸でオフセットを設定します。オフセットは、正の数でも負の数でもかまいません。paddingoffset の違いは、offset をコンポーザブルに追加しても測定値自体は変更されない点です。

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(artist.name)
            Text(
                text = artist.lastSeenOnline,
                modifier = Modifier.offset(x = 4.dp)
            )
        }
    }
}

親コンテナの右側に移動したテキスト

offset 修飾子は、レイアウト方向に従って横方向に適用されます。正の offset を設定する場合、左から右方向のコンテキストでは要素が右に移動し、右から左方向のコンテキストでは要素が左に移動します。レイアウト方向を考慮せずにオフセットを設定する必要がある場合は、absoluteOffset 修飾子をご覧ください。この修飾子を使用すると、正のオフセット値を設定した場合に要素が常に右へ移動します。

Compose での型の安全性

Compose には、特定のコンポーザブルの子に適用される場合にのみ動作する修飾子があります。たとえば、子を Box サイズに影響を与えずに親の Box と同じサイズにするには、matchParentSize 修飾子を使用します。

Compose では、カスタム スコープによってこの型の安全性が適用されます。たとえば、matchParentSizeBoxScope でのみ使用できます。そのため、この修飾子を使用できるのは Box 内で子が使用されている場合に限られます。

スコープ修飾子は、子に関して親が知っておくべき情報を親に通知します。これは、一般に親データ修飾子とも呼ばれます。その内部構造は汎用修飾子とは異なりますが、使用量の観点から見るとこれらの違いは重要ではありません。

Box の matchParentSize

前述のように、Box サイズに影響を与えずに子レイアウトを親 Box と同じサイズにする場合は、matchParentSize 修飾子を使用します。

matchParentSizeBox スコープ内でしか使用できず、Box コンポーザブルの直接の子にのみ適用されます。

以下の例では、子 Spacer は親 Box からサイズを取得し、親はそのサイズを最も大きな子(この場合は ArtistCard)から取得しています。

@Composable
fun MatchParentSizeComposable() {
    Box {
        Spacer(Modifier.matchParentSize().background(Color.LightGray))
        ArtistCard()
    }
}

コンテナいっぱいに表示されたグレーの背景

matchParentSize ではなく fillMaxSize を使用した場合、Spacer は親が使用できるすべてのスペースを使用します。この場合、親は使用可能なスペース全体に拡大します。

画面全体に表示されたグレーの背景

行と列の比

前のセクションのパディングとサイズで説明したように、デフォルトでは、コンポーザブルのサイズは自身がラップするコンテンツによって定義されます。RowScopeColumnScope でのみ使用可能な weight 修飾子を使用すると、コンポーザブルのサイズを親の内部で柔軟に変動するように設定することもできます。

たとえば、2 つの Box コンポーザブルが含まれる Row があるとします。最初のボックスには、2 番目のボックスの 2 倍の weight が指定されているため、幅も 2 倍になります。Row の幅は 210.dp なので、最初の Box の幅は 140.dp、2 番目のボックスの幅は 70.dp になります。

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Image(
            /*...*/
            modifier = Modifier.weight(2f)
        )
        Column(
            modifier = Modifier.weight(1f)
        ) {
            /*...*/
        }
    }
}

画像の幅はテキストの 2 倍

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

スクロール可能なレイアウトについて詳しくは、Compose の操作のドキュメントをご覧ください。

リストと遅延リストについては、Compose リストのドキュメントをご覧ください。

レスポンシブ レイアウト

レイアウトは、さまざまな画面の向きとフォーム ファクタのサイズを考慮して設計する必要があります。Compose には、コンポーザブルのレイアウトをさまざまな画面構成に簡単に適応させることができるように、すぐに使えるメカニズムがいくつか用意されています。

制約

親の制約を把握し、それに応じてレイアウトを設計するには、BoxWithConstraints を使用します。測定の制約は、コンテンツ ラムダのスコープにあります。これらの測定の制約を使用して、画面構成ごとに異なるレイアウトをコンポーズできます。

@Composable
fun WithConstraintsComposable() {
    BoxWithConstraints {
        Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
    }
}

スロットベースのレイアウト

Compose では、androidx.compose.material:material の依存関係(Android Studio で Compose プロジェクトを作成するときに組み込まれる)によって、マテリアル デザインをベースにしたさまざまなコンポーザブルが提供されるため、簡単に UI を作成できます。DrawerFloatingActionButtonTopAppBar などの要素がすべて提供されます。

マテリアル コンポーネントはスロット API を多用します。これは、コンポーザブルの上にカスタマイズのレイヤを適用するために Compose で導入されたパターンです。このアプローチでは、自身を構成できる子要素をコンポーネントが受け入れるため、子のすべての構成パラメータを公開する必要がなく、より柔軟にコンポーネントを利用できます。スロットは UI に空のスペースを残し、デベロッパーが自由に使用できるようにします。たとえば、TopAppBar でカスタマイズできるスロットは次のとおりです。

マテリアル コンポーネントのアプリバーで使用可能なスロットを示す図

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

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

Scaffold を使用して複数の要素を配置している JetNews サンプルアプリ

@Composable
fun HomeScreen(/*...*/) {
    Scaffold(
        drawerContent = { /*...*/ },
        topBar = { /*...*/ },
        content = { /*...*/ }
    )
}

ConstraintLayout

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

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

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha08"

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 で作成した参照を、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 を渡すだけで済みます。

詳細

Jetpack Compose でのレイアウトの Codelab にある Constraint Layout のセクションで、Compose の ConstraintLayout の詳細をご覧になり、ConstraintLayout を使用する Compose サンプルで API の実際の動作をご確認ください。

カスタム レイアウト

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,
        children = 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!")
    }
}

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

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

Jetpack Compose でのレイアウトの Codelab でカスタム レイアウトと修飾子の詳細をご覧になり、カスタム レイアウトを作成する Compose サンプルで API の実際の動作をご確認ください。

レイアウト方向

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

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

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

固有の測定値

Compose のルールのひとつとして、子を 1 回しか測定できないことが挙げられます。子を 2 回測定した場合、ランタイム例外がスローされます。ただし、測定する前に子の情報が必要になる場合もあります。

Intrinsic を使用すると、実際に測定する前に子をクエリできます。

コンポーザブルに対して、次のように intrinsicWidth または intrinsicHeight を要求できます。

  • (min|max)IntrinsicWidth: ある高さの場合に、コンテンツを正しく描画できる最小 / 最大の幅はいくつでしょうか。
  • (min|max)IntrinsicHeight: ある幅の場合に、コンテンツを正しく描画できる最小 / 最大の高さはいくつでしょうか。

たとえば、TextminIntrinsicHeight を無限大の width で要求した場合、テキストが 1 行で描画されているかのように、Textheight が返されます。

Intrinsic の使い方

次のように、2 つのテキストを分割線で区切って画面上に表示するコンポーザブルを作成するとします。

2 つのテキスト要素を横に並べ、その間に縦方向の分割線を配置します

これを実現するには、Row 内に 2 つの Text を含めてテキストが可能な限り拡大するようにして、中央に Divider を含めます。Divider の高さは最も高い Text と同じにして、幅は狭くします(width = 1.dp)。

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )

        Divider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

@Preview
@Composable
fun TwoTextsPreview() {
    MaterialTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

これをプレビューすると、想定とは異なり、分割線が画面全体に拡大されます。

2 つのテキスト要素が横に並び、分割線で区切られていますが、分割線がテキストの下まで伸びています

これは、Row がそれぞれの子を個別に測定するため、Text の高さを使用して Divider を制約できないためです。Divider が指定の高さで空きスペースを埋めるように設定する必要があります。そのためには、height(IntrinsicSize.Min) 修飾子を使用します。

height(IntrinsicSize.Min) は、子の高さが Intrinsic の最小の高さと同じになるように強制します。この修飾子は再帰的であるため、Row とその子の minIntrinsicHeight をクエリします。

これを次のようにコードに適用すると、想定どおりに動作します。

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        Divider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

プレビュー:

2 つのテキスト要素を横に並べ、その間に縦方向の分割線を配置します

Row コンポーザブルの minIntrinsicHeight が、子の最大 minIntrinsicHeight になります。Divider element'sminIntrinsicHeight は、制約が設定されていない場合はスペースを占有しないため、0 になります。TextminIntrinsicHeight は、特定の width が指定されたテキストの minIntrinsicHeight になります。したがって、Row 要素の height 制約が、Text の最大 minIntrinsicHeight になります。Divider は自身の height を、Row で指定された height 制約まで拡大します。

カスタム レイアウトでの Intrinsic

カスタムの Layout 修飾子または layout 修飾子を作成すると、近似値に基づいて固有の測定値が自動的に計算されます。このため、すべてのレイアウトで計算が不正確になる場合があります。これらの API には、こうしたデフォルトの値をオーバーライドするオプションが用意されています。

カスタム Layout の固有の測定値を指定するには、レイアウト作成時に、MeasurePolicy インターフェースの minIntrinsicWidthminIntrinsicHeightmaxIntrinsicWidthmaxIntrinsicHeight をオーバーライドします。

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    return object : MeasurePolicy {
        override fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult {
            // Measure and layout here
        }

        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurables: List<IntrinsicMeasurable>,
            height: Int
        ) = {
            // Logic here
        }

        // Other intrinsics related methods have a default value,
        // you can override only the methods that you need.
    }
}

カスタムの layout 修飾子を作成する場合は、LayoutModifier インターフェースの関連するメソッドをオーバーライドします。

fun Modifier.myCustomModifier(/* ... */) = this.then(object : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // Measure and layout here
    }

    override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int = {
        // Logic here
    }

    // Other intrinsics related methods have a default value,
    // you can override only the methods that you need.
})

詳細

固有の測定値について詳しくは、Jetpack Compose でのレイアウトの Codelab にある Intrinsic のセクションをご覧ください。

詳細

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