Jetpack Compose入門(10) コンポーネントを配置する



Jetpack Compose入門基本編、今回は複数のUIコンポーネントを配置する方法を見ていきます。これまでに学んだTextやImageコンポーネントを、画面内にいくつか配置していきます。今回の内容をマスターすれば、一気にUI作成の自由度が上がります。

レイアウトコンポーザブル

Text()に代表されるように、一つのUIコンポーネントは一つのコンポーザブル関数で記述されます。では複数のUIコンポーネントを並べて表示するにはどうするかというと、レイアウト用のコンポーザブルを使います。レイアウト用コンポーザブルを親、並べたいコンポーザブルを子として入れ子関係にして記述することで、コンポーネントを並べて配置することができます。

レイアウトコンポーザブルの代表的なものに、ColumnRowBoxがあります。これらは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を使うと、任意の位置にコンポーネントを配置することができます。ColumnRowではコンポーネントを重ねて表示することができませんでしたが、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を指定すると、RowColumnの中でコンポーネントの間隔や配置を調整できます。最初の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()の幅は子コンポーネントの幅の合計と同じになりますので、Arrangementが働かないのです。ここでは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つは、子コンポーネント間にスペースを設けます。SpaceAroundSpaceEvenlyの違いが分かりにくいですが、SpaceAroundは両端の間隔がコンポーネント間の間隔の半分になっているのに対し、SpaceEvenlyは両端の間隔とコンポーネント間の間隔が同じになっています。

Arrangement.SpaceBetween
Arrangement.Start
Arrangement.SpaceAround
Arrangement.Center
Arrangement.SpaceEvenly
Arrangement.End

ここまでの例はRowで説明してきましたが、Columnでもほぼ同様にArrangementを使うことができます。RowColumnの違いは、Rowは横方向の配置を調整するのに対してColumnは縦方向の配置をちょうせいするということ、それに伴って、ColumnではStartEndの代わりにTopBottomを使うということです。

Modifier.weight()でサイズの比率を指定する

Arrangementでは子コンポーネントのサイズ自体は変更せず、位置と間隔を調整しました。ColumnRowの子コンポーネントのサイズを調整するには、子コンポーネントのmodifier引数にModifier.weight()を指定してサイズの比率を決めます。weight()を指定すると、weight()を指定したコンポーネント同士のサイズの比率を指定できます。weight()ColumnScopeRowScopeModifierの拡張関数として定義されています。そのため、ColumnRowの下の階層でだけ使用できます。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です。次の例では、RowverticalAlignment引数に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では縦方向を指定するので、選択肢はTopCenterVerticallyBottomの3つです。Columnでは横方向を指定するので、引数名はhorizontalArrangement、選択肢はStartCenterHorizontallyEndの3つになります。

AlignmentBoxでも使えます。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は、子コンポーネントにも指定することができます。親のRowColumnBoxと子コンポーネントとで異なる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()xyを指定すると、ArrangementAlignmentで指定した位置を基準に、上下左右に位置をずらすことができます。負の値も指定可能です。次の例では、犬の絵のoffsetyに負の値を設定して上に移動し、鳥の絵のoffsetyに正の値を設定して下に移動しています。

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)
    )
}

入れ子のレイアウト

レイアウトは入れ子にすることができます。ColumnRowBoxを入れ子にして組み合わせることで、さまざまなレイアウトを実現することができます。

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."
    )
}

まとめ

今回はコンポーネントのレイアウトについて説明しました。RowColumnBoxを組み合わせれば、多くの場合で希望通りのレイアウトを実現できると思います。