Jetpack Compose入門基本編、今回は複数のUIコンポーネントを配置する方法を見ていきます。これまでに学んだTextやImageコンポーネントを、画面内にいくつか配置していきます。今回の内容をマスターすれば、一気にUI作成の自由度が上がります。
目次
レイアウトコンポーザブル
Text()
に代表されるように、一つのUIコンポーネントは一つのコンポーザブル関数で記述されます。では複数のUIコンポーネントを並べて表示するにはどうするかというと、レイアウト用のコンポーザブルを使います。レイアウト用コンポーザブルを親、並べたいコンポーザブルを子として入れ子関係にして記述することで、コンポーネントを並べて配置することができます。
レイアウトコンポーザブルの代表的なものに、Column
、Row
、Box
があります。これらはandroidx.compose.foundation.layoutパッケージに定義されています。順にみていきましょう。
縦に並べる (Column)
まずは、これまでにもサンプルで何度か登場したColumn
です。Column
を使うと、コンポーネントを縦に並べることができます。
Column {
Text(text = "Good Morning!")
Text(text = "Good Afternoon!")
Text(text = "Good Evening!")
Text(text = "Good Night!")
}
Column
の定義は以下のようになっています。上の例では、先頭3つの引数は省略(デフォルト値を使用)し、省略不可のcontent
だけを指定しています。「Hello World! テンプレート利用編」でも少し触れましたが、Kotlinでは最後の引数のラムダ式は ( ) の外に出して書くのが一般的です。そしてJetpack Composeではこの書き方が多用されます。この書き方をすることによって、コンポーザブルの階層構造が分かりやすくなるというメリットもあります。
@Composable
inline fun Column(
modifier: Modifier? = Modifier,
verticalArrangement: Arrangement.Vertical? = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal? = Alignment.Start,
content: (@Composable @ExtensionFunctionType ColumnScope.() -> Unit)?
): Unit
引数content
は、コンポーザブル関数になっています。Column
などのレイアウト用コンポーザブル関数は、引数に別のコンポーザブル関数を取ります。これによってUIの階層構造を実現しているわけです。上の例では、Text()
を4つ含んだラムダ式が、一つのコンポーザブル関数としてColumn
の引数に与えられているという形になっています。
横に並べる (Row)
Row
を使うと、コンポーネントを横に並べることができます。
Row {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.cat),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.bird),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
}
Row
の定義は以下の通りです。ここでもColumn
と同じように、content
引数だけを使っています。
@Composable
inline fun Row(
modifier: Modifier? = Modifier,
horizontalArrangement: Arrangement.Horizontal? = Arrangement.Start,
verticalAlignment: Alignment.Vertical? = Alignment.Top,
content: (@Composable @ExtensionFunctionType RowScope.() -> Unit)?
): Unit
任意の位置に配置する (Box)
Box
を使うと、任意の位置にコンポーネントを配置することができます。Column
やRow
ではコンポーネントを重ねて表示することができませんでしたが、Box
では可能です。位置が重なっている場合は、Box
の { }
内で記述した順に描画されるので、ソースコードで上にあるコンポーネントほど画面では下になります。次のサンプルでは、Image
の上にText
を重ねています。Text
の位置はModifier.offset()
で調整しています。Modifier
についてよくわからない場合は、「コンポーネントを装飾する」をご覧ください。
Box {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
)
Text(
text = "This is a dog.",
color = Color.White,
modifier = Modifier.offset(x = 10.dp, y = 80.dp)
)
}
Box
の定義も紹介しておきます。
@Composable
inline fun Box(
modifier: Modifier? = Modifier,
contentAlignment: Alignment? = Alignment.TopStart,
propagateMinConstraints: Boolean? = false,
content: (@Composable @ExtensionFunctionType BoxScope.() -> Unit)?
): Unit
サイズや位置を指定する
ここからはレイアウトをより自在に設定するためのオプションを説明していきます。
Arrangementで間隔を指定する
Arrangement
を指定すると、Row
やColumn
の中でコンポーネントの間隔や配置を調整できます。最初のRow
のサンプルをもう一度見てみると、3枚の絵が左側によっています。
Row {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.cat),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.bird),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
}
Arrangement
を使うと、この3枚の絵の位置や間隔を調整することができます。次の例ではRow()のhorizontalArrangementにArrangement.SpaceEvenlyを指定し、画像を等間隔に並べています。ここで、Row()
自体のサイズも指定する必要があるということに注意してください。Arrangement
は、Row()
の幅が子コンポーネントの幅の合計よりも大きい時にしか働きません。Row()
の幅を指定しない場合は、Row()
の幅は子コンポーネントの幅の合計と同じになりますので、Arrangemen
tが働かないのです。ここではModifier.fillMaxSize()
を指定して、Row()
が画面全体を占めるサイズになるようにしています。結果として、3枚の画像が画面の横幅全体に等間隔に並ぶことになります。
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.cat),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.bird),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
}
Arrangementは他にもいくつも種類があります。上の段の3つは、子コンポーネント間にスペースを設けます。SpaceAround
とSpaceEvenly
の違いが分かりにくいですが、SpaceAround
は両端の間隔がコンポーネント間の間隔の半分になっているのに対し、SpaceEvenly
は両端の間隔とコンポーネント間の間隔が同じになっています。
ここまでの例はRow
で説明してきましたが、Column
でもほぼ同様にArrangement
を使うことができます。Row
とColumn
の違いは、Row
は横方向の配置を調整するのに対してColumn
は縦方向の配置を調整するということ、それに伴って、Column
ではStart
とEnd
の代わりにTop
とBottom
を使うということです。
Modifier.weight()でサイズの比率を指定する
Arrangement
では子コンポーネントのサイズ自体は変更せず、位置と間隔を調整しました。Column
やRow
の子コンポーネントのサイズを調整するには、子コンポーネントのmodifier
引数にModifier.weight()
を指定してサイズの比率を決めます。weight()
を指定すると、weight()
を指定したコンポーネント同士のサイズの比率を指定できます。weight()
はColumnScope
、RowScope
でModifier
の拡張関数として定義されています。そのため、Column
やRow
の下の階層でだけ使用できます。Column
では高さ、Row
では幅の比率が、weight()
で指定した比率になります。
次の例では、画像の幅が1:2:1の比率になっています。
Row {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.weight(1f)
)
Image(
painter = painterResource(id = R.drawable.cat),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.weight(2f)
)
Image(
painter = painterResource(id = R.drawable.bird),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.weight(1f)
)
}
weight()
を指定しているコンポーネントと指定していないコンポーネントが混在している場合は、まずweight()
を指定していないコンポーネントが、そのコンポーネント自身のサイズで配置されます。その後、weight()
を指定しているコンポーネントがweight()
に指定した比率に従って配置されます。例えば次の例では、真ん中の猫の絵が最初に幅50dpで配置され、残った幅を両端の犬の絵と鳥の絵で1:1で分け合います。
Row {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.weight(1f)
)
Image(
painter = painterResource(id = R.drawable.cat),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.bird),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.weight(1f)
)
}
Alignmentで位置を揃える
ここまでの例では子コンポーネントの画像はすべて上端に張り付いて(上端が揃って)いました。この位置揃えを指定するのが、Alignment
です。次の例では、Row
のverticalAlignment
引数にAlignment.CenterVertically
を指定して、画面の中央に揃えています(真ん中の画像だけサイズを大きくしています。中心線がそろっているのが分かると思います)。
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.cat),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(75.dp)
)
Image(
painter = painterResource(id = R.drawable.bird),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
}
Row
では縦方向を指定するので、選択肢はTop
、CenterVertically
、Bottom
の3つです。Column
では横方向を指定するので、引数名はhorizontalArrangement
、選択肢はStart
、CenterHorizontally
、End
の3つになります。
Alignment
はBox
でも使えます。contentAlignment
引数に値を指定すると、指定した位置に子コンポーネントが重ねて配置されます。
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Text(
text = "This is a dog.",
color = Color.Red,
)
}
Alignmentは、子コンポーネントにも指定することができます。親のRow
、Column
、Box
と子コンポーネントとで異なるAlignment
を指定した場合は、親に指定したAlignment
がそのレイアウト内でのデフォルトになり、子コンポーネントに別の値を指定すると、そのコンポーネントだけ例外的に別の配置になります。
次の例では、真ん中の猫の絵だけ、Alignment.Top
で上端に配置しています。
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.cat),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(75.dp).align(Alignment.Top)
)
Image(
painter = painterResource(id = R.drawable.bird),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
}
位置の微調整
dp単位でコンポーネントの位置を微調整するには、modifier.offset()
を使います。offset()
でx
とy
を指定すると、Arrangement
やAlignment
で指定した位置を基準に、上下左右に位置をずらすことができます。負の値も指定可能です。次の例では、犬の絵のoffset
のy
に負の値を設定して上に移動し、鳥の絵のoffset
のy
に正の値を設定して下に移動しています。
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp).offset(y = (-10).dp)
)
Image(
painter = painterResource(id = R.drawable.cat),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(70.dp)
)
Image(
painter = painterResource(id = R.drawable.bird),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp).offset(y = 10.dp)
)
}
入れ子のレイアウト
レイアウトは入れ子にすることができます。Column
、Row
、Box
を入れ子にして組み合わせることで、さまざまなレイアウトを実現することができます。
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.cat),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
Image(
painter = painterResource(id = R.drawable.bird),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(50.dp)
)
}
Text(
text = "There are three animals."
)
}
まとめ
今回はコンポーネントのレイアウトについて説明しました。Row
、Column
、Box
を組み合わせれば、多くの場合で希望通りのレイアウトを実現できると思います。