Compose レイアウトの基本

Jetpack Compose を使用すると、アプリの UI の設計と構築が簡単になります。Compose は、次の方法で状態を UI 要素に変換します。

  1. 要素のコンポジション
  2. 要素のレイアウト
  3. 要素の描画

Compose はコンポジション、レイアウト、描画によって状態を UI に変換する

このドキュメントでは、要素のレイアウトに焦点を当て、UI 要素のレイアウトに役立つ Compose のビルディング ブロックについて説明します。

Compose でのレイアウトの目標

Jetpack Compose でのレイアウト システムの実装には、2 つの主な目的があります。

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

コンポーズ可能な関数は、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 ArtistCardColumn() {
    Column {
        Text("Alfred Sisley")
        Text("3 minutes ago")
    }
}

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

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

@Composable
fun ArtistCardRow(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

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

要素を別の要素の上に配置するには、Box を使用します。Box では、それに含まれる要素の特定の配置を設定することも可能です。

@Composable
fun ArtistAvatar(artist: Artist) {
    Box {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Icon(Icons.Filled.Check, contentDescription = "Check mark")
    }
}

重なっている 2 つの要素が示されています

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

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

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

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

右揃えにされたアイテム

レイアウト モデル

レイアウト モデルでは、UI ツリーは単一のパスでレイアウトされます。各ノードはまず自身を測定するよう求められ、次にすべての子を再帰的に測定して、サイズ制約をツリー上の子ノードに渡します。その後、リーフノードのサイズと配置が設定され、解決されたサイズと配置の指示がツリーに戻されます。

簡単に言うと、親は子より前に測定しますが、サイズと配置の設定は子より後です。

次の SearchResult 関数について考えてみましょう。

@Composable
fun SearchResult() {
    Row {
        Image(
            // ...
        )
        Column {
            Text(
                // ...
            )
            Text(
                // ...
            )
        }
    }
}

この関数は、次の UI ツリーを生成します。

SearchResult
  Row
    Image
    Column
      Text
      Text

SearchResult の例では、UI ツリー レイアウトはこの順序に従います。

  1. ルートノード Row が測定するように求められます。
  2. ルートノード Row は、最初の子 Image に測定するよう求めます。
  3. Image はリーフノードである(つまり子がない)ため、サイズを報告し、配置指示を返します。
  4. ルートノード Row は、2 番目の子 Column に測定するよう求めます。
  5. Column ノードは、最初の子 Text に測定するよう求めます。
  6. 最初の Text ノードはリーフノードであるため、サイズを報告し、配置指示を返します。
  7. Column ノードは、2 番目の子 Text に測定するよう求めます。
  8. 2 番目の Text ノードはリーフノードであるため、サイズを報告し、配置指示を返します。
  9. Column ノードが子を測定し、サイズを設定して、配置したため、自身のサイズと配置を決定できます。
  10. ルートノード Row が子を測定し、サイズを設定して、配置したため、自身のサイズと配置を決定できます。

検索結果の UI ツリーでの測定、サイズ設定、配置の順序

パフォーマンス

Compose は、子を一度測定するだけで高いパフォーマンスを実現します。シングルパス測定はパフォーマンスに優れており、Compose は深い UI ツリーを効率的に処理できます。要素が子を 2 回測定し、その子が自身の子をそれぞれ 2 回測定した場合など、UI 全体をレイアウトしようとすると多くの作業が必要になるため、アプリのパフォーマンスを保つことが難しくなります。

なんらかの理由でレイアウトで複数回の測定が必要な場合、Compose には固有の測定値という特別なシステムが用意されています。この機能の詳細については、Compose レイアウトの固有の測定値をご覧ください。

測定と配置はレイアウトパスの異なるサブフェーズであるため、アイテムの配置にのみ影響し測定に影響しない変更は別々に実行できます。

レイアウトでの修飾子の使用

Compose 修飾子で説明したように、修飾子を使用すると、コンポーザブルを装飾または拡張できます。修飾子は、レイアウトをカスタマイズするうえで不可欠です。たとえば、以下では複数の修飾子を連結して ArtistCard をカスタマイズしています。

@Composable
fun ArtistCardModifiers(
    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 = CardDefaults.cardElevation(defaultElevation = 4.dp),
        ) { /*...*/ }
    }
}

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

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

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

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

スクロール可能なレイアウトについて詳しくは、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(/*...*/) {
    ModalNavigationDrawer(drawerContent = { /* ... */ }) {
        Scaffold(
            topBar = { /*...*/ }
        ) { contentPadding ->
            // ...
        }
    }
}