Jetpack Compose のレイアウトまとめ1: 基本的な仕組み
Jetpack Compose でコンポーネントのサイズ計算や配置をする仕組みは従来の Android View や Web などとは異なります[1]。Jetpack Compose でのレイアウトの考え方をよく知らずに使うと、意図通りのレイアウトができなかったり無駄に複雑なレイアウトを組んでしまったりする可能性があります。
ここでは主に従来のプラットフォームに慣れた人が見落としそうな部分を中心に、レイアウトの機能の基本部分を解説します。
なお、レイアウトに関しては公式のドキュメントが非常によく書かれてあります。こちらも参考にすると良いと思います。
Modifier によるレイアウト設定
コンポーネントに Modifier
オブジェクトを渡す事でサイズや位置を指定します。
// 20x20 の赤いボックスが作られる
Box(modifier = Modifier.size(20.dp).background(Color.Red))
代表的な modifier
アプリケーションを作る場合は以下の modifier を覚えていれば大体済みます。
メソッド | 従来の Android View における機能 |
---|---|
size(), width(), height() サイズを直接指定する |
layout_width="〜dp" layout_height="〜dp"
|
fillMaxSize(), fillMaxWidth(), fillMaxHeight() 最大サイズで配置する |
layout_width="match_parent" layout_height="match_parent"
|
sizeIn(), widthIn(), heightIn() 最小サイズ・最大サイズを指定する |
minWidth="〜dp" minHeight="〜dp"
|
padding() マージンを指定する |
layout_margin="〜dp" padding="〜dp"
|
align() 左・中央・右に配置する |
layout_gravity="center" など |
weight() 余白を指定された分配率で埋める |
layout_weight="1" など |
一部 〜size()
, 〜width()
, 〜height()
というメソッドがたくさん並んでいますが、名前の通り幅/高さが違うだけで基本的には同じ機能を提供するものです。幅や高さだけを設定したい場合は 〜width()
や 〜height()
を、両方指定したい場合は 〜size()
を使うと良いでしょう。
このように従来あった layout_*
パラメーターに対応する物が揃っています。これなら同じように使えそうですね!でも実際はそれなりに違うので、何も知らないままだとうまく使えなかったりします(後述します)。
コンポーネント引数によるレイアウト設定
サイズや位置調整をコンポーネントの引数で行う場合もあります。例えばボタンは contentPadding
引数を提供しています。
// ボタンのパディングを指定する
Button(onClick = {}, contentPadding = PaddingValues(8.dp)) { Text("Click!") }
代表的なコンポーネント引数
引数 | コンポーネント | 従来の Android View における機能 |
---|---|---|
contentPadding パディングを指定する |
Button LazyColumn など様々 |
padding="〜dp" |
horizontalAlignment, verticalAlignment 子要素の並びを揃える |
Column Row LazyGrid などのリスト系 |
gravity="center_horizontal" など |
verticalArrangement, horizontalArrangement 子要素を詰める方法を指定する |
Column Row LazyGrid などのリスト系 |
gravity="center_vertical" など? |
textAlign テキストの並びを揃える |
Text |
gravity="center_horizontal" など |
これらの引数は一部 modifier で代用する事もできますが、引数を使った方が便利だったり何らかの事情があって引数を提供しています。従ってこういった引数があれば modifier よりもこちらを使う事を考慮に入れましょう。
Padding
多くの人が真っ先に混乱するのは padding だと思います。padding は以下の特徴があります。
padding の呼び出し順序を変えてみる
実際に padding
と size
を指定した二つのボックスを書いてみます。呼んでいる modifier は一緒ですが、順序が違います。
Box(Modifier.size(120.dp).background(Color.White).inspect(Color.Black)) {
// padding が先
Box(modifier = Modifier.padding(8.dp).size(80.dp).inspect(Color.Blue))
// padding が後
Box(modifier = Modifier.size(80.dp).padding(8.dp).inspect(Color.Red))
}
ちなみに inspect()
modifier は今回サイズの可視化用に作ったメソッドで、Jetpack Compose ライブラリに含まれていません。
結果は以下のようになります(1dp = 2px に引き伸ばしています)。
padding
を size
より先に指定した青いボックスの方は、8dp の margin が指定された 80dp のボックスになっています。一方で、赤いボックスの方は 64dp になっています。
Modifier は一種のコンポーネントっぽい動きをする
modifier は Box
などのコンポーネントのサイズなどのプロパティをセットしているというより、それ自身が一つのコンポーネントであるように振る舞います。例えば上記の赤いボックスのように padding を後にした場合、
Box {
Text(
text = "hello",
modifier = Modifier.size(80.dp).padding(8.dp)
)
}
コンポーネントが modifier に対応するボックスでラップされたような形になります。仮に modifier をコンポーネントとして表すと以下のコードのようになります。
Box {
SizeModifier(80.dp) {
PaddingModifier(8.dp) {
Text("hello")
}
}
}
このように padding()
を size()
の後に呼び出した場合、padding は内側に作られるので従来の padding のような動きになります。一方で padding()
を size()
の前に呼び出した場合、padding は外側に作られるので従来の margin のような動きになります。
contentPadding 引数
Button
や LazyColumn
などの多くのコンポーネントは contentPadding
引数を用意しています。
Button(onClick = {}, contentPadding = PaddingValues(8.dp)) { Text("Click!") }
contentPadding
を使わなくても padding modifier で代用できる事も多いですが、意図しない動作になったりする事も多いです。そのため contentPadding
がある場合は基本的にはそちらを使う方が良いです。
Constraints の伝搬
padding と同じく慣れないうちはややこしいのが、実際のサイズとは異なる制約(constraint)という概念です。例えば size
や fillMaxSize
はサイズを決めているのではなく、制約を追加しています。
シンプルなケースだとあまり気にしなくても動作するレイアウトを書けるのですが、多少複雑なレイアウトを組む時や、Jetpack Compose のソースを参考にする場合などは仕組みを理解しないと難しい事があるので、理解しておいた方が良いでしょう。
Constraints
Jetpack Compose のサイズ制約は Constraints
というクラスで表現されています。 Constraints
は以下のようなクラスで[2]、幅/高さそれぞれの範囲で表現されます。
class Constraints(val minWidth: Int, val maxWidth: Int, val minHeight: Int, val maxHeight: Int)
Constraints はルート要素から始まり、値を変えながら子のコンポーネントや次の modifier に再帰的に渡していきます(modifier がコンポーネントのように振る舞うのは先ほどと同様です)。
各 modifier やコンポーネントが子要素に渡す制約は以下のようになります。
変換内容 | 変換例 (左は受け取った制約、右は子要素に渡す制約) |
---|---|
Modifier.size() 制約の上限・下限を指定の値にする(可能な限り) |
Modifier.size(40.dp) |
Modifier.fillMaxSize() 制約の下限を上限値にする |
Modifier.fillMaxSize() |
Modifier.sizeIn() 制約を指定の範囲に狭める |
Modifier.sizeIn(20.dp, 20.dp, 60.dp, 60.dp) |
Modifier.padding() 制約の上限・下限を狭める |
Modifier.padding(8.dp) |
Box, Column などのコンポーネントの多く 制約の下限を0にリセットする |
Box { … } |
size()
fillMaxSize()
sizeIn()
は制約を強める事はあっても弱める事がない事に注意が必要です。例えば、Modifier.fillMaxSize().size(40.dp)
という風に指定したとしても fillMaxSize()
で既に上限・下限が固定されてしまっているため、size(40.dp)
の指定は無視されます。
(発展) wrapContentSize と propagateMinConstraints
Box
などの多くのコンポーネントは制約の下限を0にリセットしますが、同様の挙動を wrapContentSize
という modifier で行う事もできます。wrapContentSize
を使う事でコンポーネントの入れ子を減らしコードを簡潔にできる場合があります。また wrapContentSize
は上限もリセットできます。
逆に Box
で制約の下限をリセットしたくない場合は propagateMinConstraints
引数を指定する事で挙動を変える事ができます。
変換内容 | 変換例 (左は受け取った制約、右は子要素に渡す制約) |
---|---|
Modifier.wrapContentSize() 制約の下限を0にリセットする |
Modifier.wrapContentSize() |
Box(propagateMinConstraints = true) 制約をそのままにする |
Box(propagateMinConstraints = true) { … } |
サイズの決定
制約を受け取ったコンポーネントや modifier は子要素に制約を渡した後、各子要素からサイズを受け取り、それを元に自身のサイズを決定します。
大抵の要素は制約を満たした上でなるべく小さいサイズを返します。modifier で制約が指定されておらず、子要素がない場合はサイズは0になります。
Column を例に挙げると、高さは各子要素の高さの合計、幅は各子要素の幅の最大値になります(デフォルト設定の場合)。
このように各要素のサイズは子要素のサイズが決まってから決まるため、制約とは逆にサイズは下から上へと順に決まっていく事になります。
まとめ
今回は Jetpack Compose のレイアウトの基本的な仕組み部分について解説しました。
次回
Discussion