📂

【Jetpack Compose】レイアウトについてまとめる

2024/12/24に公開

初めに

今回は Jetpack Compose におけるレイアウトについてまとめてみたいと思います。
Flutter のレイアウトと似ている部分も多くありますが、Flutter の書き方と比較しつつ、 Jetpack Compose ではどのように実装するかみていきたいと思います。

記事の対象者

  • Jetpack Compose 初学者

目的

今回の目的は、 Jetpack Compose のレイアウトの実装方法をまとめることです。
Flutter だとどのようになるかを比較しつつ、基本的なレイアウトについては実現できるくらいまでまとめていきたいと思います。

実装

今回は以下の項目についてまとめてみたいと思います。

  • Column
  • Row
  • Box
  • Spacer
  • Layout

Column

まずは Column についてみていきます。
以下のような Column のサンプルを作って実行してみます。

@Composable
fun ColumnSample(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier,
    ) {
        Text(text = "Item 1", fontSize = 30.sp)
        Text(text = "Item 2", fontSize = 30.sp)
        Text(text = "Item 3", fontSize = 30.sp)
    }
}

実行結果は以下のようになります。
テキストの Column が画面の左上にまとまって表示されていることがわかります。
デフォルトではこのような表示になります。

verticalArrangement

次に verticalArrangement を変更してみます。
verticalArrangement は名前の通り、垂直方向のレイアウト を変更できます。
Jetpack Composeの場合の書き方、 Flutter の場合の書き方、見た目は以下の通りです。

見た目
Top Arrangement.Top MainAxisAlignment.start
Center Arrangement.Center MainAxisAlignment.center
Bottom Arrangement.Bottom MainAxisAlignment.end
SpaceBetween Arrangement.SpaceBetween MainAxisAlignment.spaceBetween
SpaceAround Arrangement.SpaceAround MainAxisAlignment.spaceAround
SpaceEvenly Arrangement.SpaceEvenly MainAxisAlignment.spaceEvenly
spacedBy Arrangement.spacedBy(50.dp) spacing: 50

horizontalAlignment

次に horizontalAlignment を変更してみます。
horizontalAlignment は名前の通り、水平方向のレイアウト を変更できます。
それぞれの場合のレイアウトは以下の通りです。

見た目
Start Alignment.Start CrossAxisAlignment.start
CenterHorizontally Alignment.CenterHorizontally CrossAxisAlignment.center
End Alignment.End CrossAxisAlignment.end

基本的にプロパティの名前は Jetpack Compose と Flutter で同じであることが多いです。
verticalArrangementspacedBy に関しても spacing を用いることで実現できるようになったので、同じような実装ができると言えそうです。

Row

次に Row についてみていきます。
以下のような Row のサンプルを作って実行してみます。

@Composable
fun RowSample(modifier: Modifier = Modifier) {
    Row(
        modifier = modifier,
    ) {
        Text(text = "Item 1", fontSize = 25.sp)
        Text(text = "Item 2", fontSize = 25.sp)
        Text(text = "Item 3", fontSize = 25.sp)
    }
}

実行結果は以下のようになります。
テキストの Row が画面の左上にまとまって横並びに表示されていることがわかります。
デフォルトではこのような表示になります。

horizontalArrangement

次に horizontalArrangement を変更してみます。
horizontalArrangement は名前の通り、水平方向のレイアウト を変更できます。
それぞれの場合のレイアウトは以下の通りです。

見た目
Start Arrangement.Start MainAxisAlignment.start
Center Arrangement.Center MainAxisAlignment.center
End Arrangement.Bottom MainAxisAlignment.end
SpaceBetween Arrangement.SpaceBetween MainAxisAlignment.spaceBetween
SpaceAround Arrangement.SpaceAround MainAxisAlignment.spaceAround
SpaceEvenly Arrangement.SpaceEvenly MainAxisAlignment.spaceEvenly
spacedBy Arrangement.spacedBy(50.dp) spacing: 50

verticalAlignment

次に verticalAlignment を変更してみます。
verticalAlignment は、垂直方向のレイアウト を変更できます。
それぞれの場合のレイアウトは以下の通りです。

見た目
Top Alignment.Top CrossAxisAlignment.start
CenterVertically Alignment.CenterVertically CrossAxisAlignment.center
Bottom Alignment.Bottom CrossAxisAlignment.end

Flutter では mainAxisAlignmentcrossAxisAlignment と表記しますが、 Jetpack Compose では verticalAlignmenthorizontalArrangement のように表記します。
使用しているのが Column か Row かによってどちらがメインでどちらがクロスかを考えなくて良いので、書きやすいと感じました。

Box

次に Box についてみていきます。
以下のような Box のサンプルを実行してみます。

@Composable
fun BoxSample(modifier: Modifier = Modifier) {
    Box(
        modifier = modifier
    ) {
        Image(
            painter = painterResource(id = R.drawable.sample_image),
            contentDescription = "Sample Image",
            modifier = Modifier.width(300.dp).height(200.dp),
            contentScale = ContentScale.Crop
        )
        Text(
            text = "Overlay Text",
            color = Color.Blue,
            fontSize = 20.sp,
            fontWeight = FontWeight.Bold
        )
    }
}

実行結果は以下のようになります。
画面の左上に画像とテキストが重なって表示されていることがわかります。
デフォルトではこのような表示になります。

contentAlignment

次に contentAlignment を変更してみます。
contentAlignment では Boxの子要素の配置 を変更できます。
Flutter の場合は Stackalignment で定義することが多いかと思います。

見た目
TopStart Alignment.TopStart Alignment.topLeft
TopCenter Alignment.TopCenter Alignment.topCenter
TopEnd Alignment.TopEnd Alignment.topRight
CenterStart Alignment.CenterStart Alignment.centerLeft
Center Alignment.Center Alignment.center
CenterEnd Alignment.CenterEnd Alignment.centerRight
BottomStart Alignment.BottomStart Alignment.bottomLeft
BottomCenter Alignment.BottomCenter Alignment.bottomCenter
BottomEnd Alignment.BottomEnd Alignment.bottomRight

Spacer

次に Spacer についてみていきます。
以下のような Spacer のサンプルを実行してみます。

@Composable
fun SpacerSample(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Item 1")
        Spacer(modifier = Modifier.height(20.dp))
        Text(text = "Item 2")
        Spacer(modifier = Modifier.height(20.dp))
        Text(text = "Item 3")
    }
}

実行結果は以下のようになります。

modifier

Spacer では modifier プロパティのみを引数に指定することができ、かつ必須のプロパティになっています。Column の中では SpacermodifierModifier.height(20.dp) のように指定することで、各要素間の高さを確保することができます。

Flutter の場合では SizedBox を用いた以下のような実装になるかと思います。

Column(
  children: [
    Text('Item 1'),
    SizedBox(height: 20),
    Text('Item 2'),
    SizedBox(height: 20),
    Text('Item 3'),
  ],
),

Layout

最後に Layout についてみていきます。
Layout では今まで見てきたものよりもさらに細かくレイアウトを決めることができます。
以下のようなコードを実行してみます。

@Composable
fun CustomLayoutModifierSample(modifier: Modifier = Modifier) {
    Layout(
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp),
        content = {
            Text(text = "Item 1")
            Text(text = "Item 2")
        }
    ) { measurable, constraints ->
        layout(constraints.minWidth, constraints.maxHeight) {
            val item1 = measurable[0].measure(Constraints())
            val item1Position = Alignment.Center.align(
                size = IntSize(item1.width, item1.height),
                space = IntSize(constraints.maxWidth, constraints.maxHeight),
                layoutDirection,
            )
            item1.place(item1Position)

            val item2 = measurable[1].measure(Constraints())
            val item2Position = item1Position.copy(x = item1Position.x + 100, y = item1Position.y + 100)
            item2.place(item2Position)
        }
    }
}

実行結果は以下のようになります。

コードを詳しくみていきます。

以下では、 Layout の中で modifiercontent を定義しています。
content では名前の通り、 Layout の中で表示させる Composable を定義できます。
以下ではテキストを二つ表示させています。

@Composable
fun CustomLayoutModifierSample(modifier: Modifier = Modifier) {
    Layout(
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp),
        content = {
            Text(text = "Item 1")
            Text(text = "Item 2")
        }

以下では、 layout メソッドを実装しています。
measurable には表示させる Composable のリストが格納されています。
item1 には measurable の一つ目の要素である Text(text = "Item 1") が格納されており、 measure メソッドで item1 のサイズを測っています。

item1Position では画面の中央の位置を保持しています。
item1.place(item1Position) では画面中央の位置に item1 を配置しています。

item2 には measurable の二つ目の要素である Text(text = "Item 2") が格納されており、その位置を item2Position として定義しています。
item2Position には、 item1Position の位置をもとにした位置を指定しています。具体的には、 item1Position の x軸、y軸の両方に 100 を足した値を指定しています。
item2 も同様に place メソッドで item2 を配置しています。

) { measurable, constraints ->
    layout(constraints.minWidth, constraints.maxHeight) {
        val item1 = measurable[0].measure(Constraints())
        val item1Position = Alignment.Center.align(
            size = IntSize(item1.width, item1.height),
            space = IntSize(constraints.maxWidth, constraints.maxHeight),
            layoutDirection,
        )
        item1.place(item1Position)

        val item2 = measurable[1].measure(Constraints())
        val item2Position = item1Position.copy(x = item1Position.x + 100, y = item1Position.y + 100)
        item2.place(item2Position)
    }
}

このように、 Layout ではより細かいレイアウトの指定をすることができ、上記のように特定の要素の相対的な位置も指定することができます。

相対的な位置を指定することで以下のようなUIも実装することができます。

上記UIのコード
@Composable
fun CustomLayoutModifierSample(modifier: Modifier = Modifier) {
    Layout(
        modifier = modifier
            .fillMaxSize()
            .background(Color.Black)
            .padding(16.dp),
        content = {
            Image(
                painter = painterResource(id = R.drawable.sun),
                contentDescription = "Sun Image",
                modifier = Modifier
                    .width(80.dp)
                    .height(80.dp),
                contentScale = ContentScale.Crop
            )
            Image(
                painter = painterResource(id = R.drawable.mercury),
                contentDescription = "Mercury Image",
                modifier = Modifier
                    .width(20.dp)
                    .height(20.dp),
                contentScale = ContentScale.Crop
            )
            Image(
                painter = painterResource(id = R.drawable.venus),
                contentDescription = "Venus Image",
                modifier = Modifier
                    .width(20.dp)
                    .height(20.dp),
                contentScale = ContentScale.Crop
            )
            Image(
                painter = painterResource(id = R.drawable.earth),
                contentDescription = "Earth Image",
                modifier = Modifier
                    .width(30.dp)
                    .height(30.dp),
                contentScale = ContentScale.Crop
            )
            Image(
                painter = painterResource(id = R.drawable.mars),
                contentDescription = "Mars Image",
                modifier = Modifier
                    .width(30.dp)
                    .height(30.dp),
                contentScale = ContentScale.Crop
            )
            Image(
                painter = painterResource(id = R.drawable.jupiter),
                contentDescription = "Jupiter Image",
                modifier = Modifier
                    .width(50.dp)
                    .height(50.dp),
                contentScale = ContentScale.Crop
            )
            Image(
                painter = painterResource(id = R.drawable.saturn),
                contentDescription = "Saturn Image",
                modifier = Modifier
                    .width(50.dp)
                    .height(50.dp),
                contentScale = ContentScale.Crop
            )
            Image(
                painter = painterResource(id = R.drawable.uranus),
                contentDescription = "Uranus Image",
                modifier = Modifier
                    .width(50.dp)
                    .height(50.dp),
                contentScale = ContentScale.Crop
            )
            Image(
                painter = painterResource(id = R.drawable.neptune),
                contentDescription = "Neptune Image",
                modifier = Modifier
                    .width(50.dp)
                    .height(50.dp),
                contentScale = ContentScale.Crop
            )
        }
    ) { measurable, constraints ->
        layout(constraints.minWidth, constraints.maxHeight) {
            val sun = measurable[0].measure(Constraints())
            val sunPosition = Alignment.TopStart.align(
                size = IntSize(sun.width, sun.height),
                space = IntSize(constraints.maxWidth, constraints.maxHeight),
                layoutDirection,
            )
            sun.place(sunPosition)

            val mercury = measurable[1].measure(Constraints())
            val mercuryPosition = sunPosition.copy(x = sunPosition.x + 100, y = sunPosition.y + 250)
            mercury.place(mercuryPosition)

            val venus = measurable[2].measure(Constraints())
            val venusPosition = mercuryPosition.copy(x = mercuryPosition.x + 100, y = mercuryPosition.y + 250)
            venus.place(venusPosition)

            val earth = measurable[3].measure(Constraints())
            val earthPosition = venusPosition.copy(x = venusPosition.x + 100, y = venusPosition.y + 250)
            earth.place(earthPosition)

            val mars = measurable[4].measure(Constraints())
            val marsPosition = earthPosition.copy(x = earthPosition.x + 100, y = earthPosition.y + 250)
            mars.place(marsPosition)

            val jupiter = measurable[5].measure(Constraints())
            val jupiterPosition = marsPosition.copy(x = marsPosition.x + 100, y = marsPosition.y + 250)
            jupiter.place(jupiterPosition)

            val saturn = measurable[6].measure(Constraints())
            val saturnPosition = jupiterPosition.copy(x = jupiterPosition.x + 100, y = jupiterPosition.y + 250)
            saturn.place(saturnPosition)

            val uranus = measurable[7].measure(Constraints())
            val uranusPosition = saturnPosition.copy(x = saturnPosition.x + 100, y = saturnPosition.y + 250)
            uranus.place(uranusPosition)

            val neptune = measurable[8].measure(Constraints())
            val neptunePosition = uranusPosition.copy(x = uranusPosition.x + 100, y = uranusPosition.y + 250)
            neptune.place(neptunePosition)
        }
    }
}

以上です。

まとめ

最後まで読んでいただいてありがとうございました。

調べていく中で、Flutter と似ている部分も多くあることがわかりましたが、細かいプロパティの指定方法等に差異があることもわかりました。
どちらかの書き方に慣れていると、わからなくなることもあると思うので、備忘録として活用いただければ幸いです。

誤っている点やもっと良い書き方があればご指摘いただければ幸いです。

参考

https://developer.android.com/develop/ui/compose/lists?hl=ja

https://at-sushi.work/blog/52/

Discussion