Jetpack Composeを使うことで、今までのxmlベースの実装では対応が難しかったいくつかの内容に対して、実装が容易になることがあります。
その1つがレスポンシブ対応です。
タブレット端末も含めると大小様々な画面サイズのデバイスがあり、また分割画面も考慮すると、対応すべき画面サイズは膨大なものになっています。
最近は折りたたみスマートフォンも増えてきましたね。
xmlベースでのUI構築では、xmlを分けるか、ゴリゴリコードを書いて命令的に更新する必要がありました。
Jetpack Composeは BoxWithConstraints を使うことで簡単に対応することが出来ます。
今回はその使い方について紹介します。
BoxWithConstraints
でカラム変更BoxWithConstraints
は基本のコンポーネントの1つである Box とよく似ています。
違いとしては、content
の中でminWidth
, maxWidth
, minHeight
, maxHeight
といった制約を取得することが出来ます。
例えば、幅が400dp以上あれば横並び、そうでなければ縦並びにするコードを考えます。
@Composable
fun BoxWithConstraintsSample() {
BoxWithConstraints {
if (maxWidth >= 400.dp) {
Row {
Text(
modifier = Modifier
.weight(1F)
.background(Color.Red)
.padding(16.dp),
text = "A",
textAlign = TextAlign.Center
)
Text(
modifier = Modifier
.weight(1F)
.background(Color.Blue)
.padding(16.dp),
text = "B",
textAlign = TextAlign.Center
)
}
} else {
Column {
Text(
modifier = Modifier
.fillMaxWidth()
.background(Color.Red)
.padding(16.dp),
text = "A",
textAlign = TextAlign.Center
)
Text(
modifier = Modifier
.fillMaxWidth()
.background(Color.Blue)
.padding(16.dp),
text = "B",
textAlign = TextAlign.Center
)
}
}
}
}
今回は横幅いっぱいに広げたときに入るかどうかを知りたいので、 maxWidth
を利用します。
横幅360dpのPixel 4と横幅480dpのPixel 4 XLで見比べると以下のようになります。
Pixel 4 | Pixel 4 XL |
---|---|
カルーセル(横スクロールのリスト)の表示をする際に、横スクロール可能であることがわかりやすいよう、どの画面サイズでも少しだけはみ出して見えるようにしたいことがあります。
ここでは、どの画面サイズでも2.2個分表示されるようにしてみましょう。
@Composable
fun CarouselSample() {
val items = listOf(
Color.Red,
Color.Green,
Color.Blue
)
BoxWithConstraints {
val cellWidth = maxWidth / 2.2F
Row(
modifier = Modifier.horizontalScroll(rememberScrollState())
) {
items.forEachIndexed { index, color ->
Text(
modifier = Modifier
.width(cellWidth)
.background(color)
.padding(16.dp),
text = index.toString(),
textAlign = TextAlign.Center
)
}
}
}
}
Pixel 4, Pixel 4 XLで見たときは以下のようになります。
Pixel 4 | Pixel 4 XL |
---|---|
更に横幅が広くなった時を考慮し、セル数を増減させるようにしても良いでしょう。
Box
と BoxWithConstraints
のinterfaceを比較してみると、このようになっています。
@Composable
inline fun Box(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content: BoxScope.() -> Unit
): @Composable Unit
@Composable
fun BoxWithConstraints(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content: BoxWithConstraintsScope.() -> Unit
): @Composable Unit
modifier
, contentAlignment
は同じですが、content
のscopeが BoxScope
から BoxWithConstraintsScope
になっています。
@LayoutScopeMarker
@Immutable
interface BoxScope {
@Stable
fun Modifier.align(alignment: Alignment): Modifier
@Stable
fun Modifier.matchParentSize(): Modifier
}
@Stable
interface BoxWithConstraintsScope : BoxScope {
val constraints: Constraints
val minWidth: Dp
val maxWidth: Dp
val minHeight: Dp
val maxHeight: Dp
}
BoxWithConstraintsScope
は BoxScope
を引き継いでおり、ここにmaxWidth
等を持っています。
これにより、BoxWithConstraints
の content
内では maxWidth
にアクセスが出来ることになります。
ちなみに、BoxScope
内にModifier
の拡張関数を用意することで、Box
のcontent内でのみ使えるModifier
を追加しています。(ここでは align
と matchParentSize
)
Jetpack Composeでは、Kotlinの様々な記法を組み合わせることによって、間違った書き方がしにくくなっています。
また、@Stable
は安定しており、勝手に値が書き換わらないことを示しており、@Immutable
は一切値が変更されないことを示しています。
これらは、不要なRecomposeを抑えるためのアノテーションです 詳細。
Box
とBoxWithConstraints
の両方で指定可能なpropagateMinConstraints
は、子供にminWidthやminHeightを伝播させるかを決定します。
Box
のminWidth
, minHeight
よりも content
が小さい場合、デフォルトの状態だと Alignment
に従って小さい状態で配置されますが、propagateMinConstraints=true
にすると、minWidth
, minHeight
に引き伸ばされて表示されます。
fun Sample() {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red),
propagateMinConstraints = true
) {
Box(
modifier = Modifier
.size(100.dp) // 親のminWidthを引き継ぎ、200dpで表示される
.background(Color.Blue)
)
}
}
今までViewのサイズを取得するには一度レンダリングしたあとに値を取得しに行く必要があり、コードが複雑になる傾向がありました。
Jetpack Composeなら、ViewのサイズによってUIを変える必要があっても、宣言的に書くことが出来ます。
これにより、レスポンシブなデザイン実装が非常に楽になったのでは無いでしょうか。
まだ試験運用中ですが、他にもLazyVerticalGrid には GridCells.Adaptive という仕組みがあり、 minSize
を指定していい感じにgridレイアウトを組むことが出来ます。
今後もJetpack Composeのトピックスをお届けしていこうと思います。