はじめに
Modifierでサイズを指定するとき、width
とrequiredWidth
、height
とrequiredHeight
、size
とrequiredSize
があるよね。
Composeの公式ドキュメントを読めば言いたいことは大体理解できる。
親レイアウトが指定したサイズを無視して、子レイアウトのサイズを決定したい場合にrequiredXXX
を使うよってことよね。
これはわかる。
わかるんだけど、いざコードのコメントを和訳して読んでみると、
requireSize
コンテンツのサイズを正確に[size] dpの幅と高さになるように宣言します。 着信測定[制約]はこの値を上書きしません。
コンテンツが着信[制約]を満たさないサイズを選択した場合、親レイアウトは[制約]で強制されたサイズとして報告され、コンテンツの位置は、割り当てられたスペースを中心に自動的にオフセットされます。
[制約]が尊重されることを前提とした親レイアウトによる子。
はいこの通り。
全然さっぱりわけがわからない。
このままでは、予測にrequiredXXX
が出てくるたびにモヤモヤしてしまう。
それはあまりにも精神衛生に良くない。
ということで、実際にコードを読んでみて、なんとなくModifier.requiredXXX
について理解し、モヤモヤせず開発できるようになるくらいの粒度でまとめていくよ。
ちなみに、**「width requireWidth 違い」**って結果だけ欲しい人は、まとめだけ読んでくれれば多分満足できるよ。
widthとrequiredWidthの違い
まずは、Modifier.width
とModifier.requiredWidth
がどう違うのか見ていく。
■ Modifier.width
fun Modifier.width(width: Dp) = this.then(
SizeModifier(
minWidth = width,
maxWidth = width,
enforceIncoming = true,
inspectorInfo = debugInspectorInfo {
name = "width"
value = width
}
)
)
■ Modifier.requiredWidth
fun Modifier.requiredWidth(width: Dp) = this.then(
SizeModifier(
minWidth = width,
maxWidth = width,
enforceIncoming = false,
inspectorInfo = debugInspectorInfo {
name = "requiredWidth"
value = width
}
)
)
見比べると違いは一目瞭然。
enforceIncomingに渡している真偽値が、width
の場合はtrue、requiredWidth
の場合はfalseになっているね。
なんとなくだけど、このenforceIncomingが何に使われているのかがわかればスッキリしそうな気がする。
SizeModifierの中身を読む
enforceIncomingのフラグで動きがどう変わるのか見るために、SizeModifierの中身を読んでいく。
■ SizeModifier
private class SizeModifier(
private val minWidth: Dp = Dp.Unspecified,
private val minHeight: Dp = Dp.Unspecified,
private val maxWidth: Dp = Dp.Unspecified,
private val maxHeight: Dp = Dp.Unspecified,
private val enforceIncoming: Boolean,
inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
private val Density.targetConstraints: Constraints
get() {
val maxWidth = if (maxWidth != Dp.Unspecified) {
maxWidth.coerceAtLeast(0.dp).roundToPx()
} else {
Constraints.Infinity
}
val maxHeight = if (maxHeight != Dp.Unspecified) {
maxHeight.coerceAtLeast(0.dp).roundToPx()
} else {
Constraints.Infinity
}
val minWidth = if (minWidth != Dp.Unspecified) {
minWidth.roundToPx().coerceAtMost(maxWidth).coerceAtLeast(0).let {
if (it != Constraints.Infinity) it else 0
}
} else {
0
}
val minHeight = if (minHeight != Dp.Unspecified) {
minHeight.roundToPx().coerceAtMost(maxHeight).coerceAtLeast(0).let {
if (it != Constraints.Infinity) it else 0
}
} else {
0
}
return Constraints(
minWidth = minWidth,
minHeight = minHeight,
maxWidth = maxWidth,
maxHeight = maxHeight
)
}
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val wrappedConstraints = targetConstraints.let { targetConstraints ->
if (enforceIncoming) {
constraints.constrain(targetConstraints)
} else {
val resolvedMinWidth = if (minWidth != Dp.Unspecified) {
targetConstraints.minWidth
} else {
constraints.minWidth.coerceAtMost(targetConstraints.maxWidth)
}
val resolvedMaxWidth = if (maxWidth != Dp.Unspecified) {
targetConstraints.maxWidth
} else {
constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth)
}
val resolvedMinHeight = if (minHeight != Dp.Unspecified) {
targetConstraints.minHeight
} else {
constraints.minHeight.coerceAtMost(targetConstraints.maxHeight)
}
val resolvedMaxHeight = if (maxHeight != Dp.Unspecified) {
targetConstraints.maxHeight
} else {
constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight)
}
Constraints(
resolvedMinWidth,
resolvedMaxWidth,
resolvedMinHeight,
resolvedMaxHeight
)
}
}
val placeable = measurable.measure(wrappedConstraints)
return layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
// ここから先は関係なかったので省略
}
幸いにも、SizeModifierの中でenforceIncomingを参照しているのはequals
を除いて、MeasureScope.measure
の一箇所だけだった。
MeasureScope.measure
では多分レイアウトの計算をしているんだろうけど、その中でenforceIncomingが **true(widthを使った場合)**はconstraints.constrain(targetConstraints)
の結果が、 **false(requiredWidthを使った場合)**はなんかゴチャゴチャやった結果がそれぞれwrappedConstraintsの中に入れられてるのがわかる。
wrappedConstraintsは最後の方でmeasurable.measure
に渡されているので、多分wrappedConstraints
が持つ制約を元にしてレイアウトが計算されるって考えて良さそう。
ってことはこの二つのパターンをそれぞれ理解すれば、もう怖いものはなさそうね。
enforceIncoming == true の場合
再度trueの場合を見てみる。
constraints.constrain(targetConstraints)
一件シンプルに見えるけど、それはtargetConstraintsが中の処理を隠してくれているからね。
targetConstraintsの初期化処理をみると
private val Density.targetConstraints: Constraints
get() {
val maxWidth = if (maxWidth != Dp.Unspecified) {
maxWidth.coerceAtLeast(0.dp).roundToPx()
} else {
Constraints.Infinity
}
val maxHeight = if (maxHeight != Dp.Unspecified) {
maxHeight.coerceAtLeast(0.dp).roundToPx()
} else {
Constraints.Infinity
}
val minWidth = if (minWidth != Dp.Unspecified) {
minWidth.roundToPx().coerceAtMost(maxWidth).coerceAtLeast(0).let {
if (it != Constraints.Infinity) it else 0
}
} else {
0
}
val minHeight = if (minHeight != Dp.Unspecified) {
minHeight.roundToPx().coerceAtMost(maxHeight).coerceAtLeast(0).let {
if (it != Constraints.Infinity) it else 0
}
} else {
0
}
return Constraints(
minWidth = minWidth,
minHeight = minHeight,
maxWidth = maxWidth,
maxHeight = maxHeight
)
}
こんな感じで、結構複雑そうに見える。
けど、ちゃんと読めばそんなに難しくはなかった。
最初は最大値の制約について。
maxWidthが指定されていた場合(maxWidth != Dp.Unspecified)はmaxWidthが0dp以上であることを保証してから、px変換する。
指定されていなかった場合は最大値(Constraints.Infinity)になる。
heightに関しても同じ。
次に最低値の制約について。
minWidthが指定されていた場合(minWidth != Dp.Unspecified)はまずpx変換してから、最大値以下であることを保証(coerceAtMost(maxWidth))して、0px以上であることを保証してから、最大値が指定されていなかった場合は指定した値を、最大値が指定されていたら0に設定している。
指定されていなかった場合は0になる。
heightに関しても同じ。
上記の処理で作った高さ、幅の最大値/最小値制約を持っているのが、targetConstraintsということね。
これを
constraints.constrain
に渡しているので、中身も見てみる。
fun Constraints.constrain(otherConstraints: Constraints) = Constraints(
minWidth = otherConstraints.minWidth.coerceIn(minWidth, maxWidth),
maxWidth = otherConstraints.maxWidth.coerceIn(minWidth, maxWidth),
minHeight = otherConstraints.minHeight.coerceIn(minHeight, maxHeight),
maxHeight = otherConstraints.maxHeight.coerceIn(minHeight, maxHeight)
)
イマイチこれだとわからないので、コメントを読んでみる。
otherConstraintsを受け取り、現在の制約でそれらを強制した結果を返します。
これは、結果の制約を満たすサイズは現在の制約を満たしますが、2つの制約セットが互いに素である場合はotherConstraintsを満たさない可能性があることに注意してください。
例(幅のみを表示、高さは同じように機能します):( minWidth = 2、maxWidth = 10).constrain(minWidth = 7、maxWidth = 12)->(minWidth = 7、maxWidth = 10)(minWidth = 2、maxWidth = 10).constrain(minWidth = 11、maxWidth = 12)->(minWidth = 10、maxWidth = 10)(minWidth = 2、maxWidth = 10).constrain(minWidth = 5、maxWidth = 7)->(minWidth = 5 、maxWidth = 7)
これはなんとなく言ってることわかるね。
つまり、constraint.constrain
を使って、親レイアウト無視で作られた制約のtargetConstraintsを、親レイアウトを考慮した制約として生成し直しているわけね。
なるへそ〜。
enforceIncoming == false の場合
次はfalseの場合。
さっきは複雑に見えた処理も、targetConstraintsを読み解いた後なら何をやっているのか見えてくる。
val resolvedMinWidth = if (minWidth != Dp.Unspecified) {
targetConstraints.minWidth
} else {
constraints.minWidth.coerceAtMost(targetConstraints.maxWidth)
}
val resolvedMaxWidth = if (maxWidth != Dp.Unspecified) {
targetConstraints.maxWidth
} else {
constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth)
}
val resolvedMinHeight = if (minHeight != Dp.Unspecified) {
targetConstraints.minHeight
} else {
constraints.minHeight.coerceAtMost(targetConstraints.maxHeight)
}
val resolvedMaxHeight = if (maxHeight != Dp.Unspecified) {
targetConstraints.maxHeight
} else {
constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight)
}
Constraints(
resolvedMinWidth,
resolvedMaxWidth,
resolvedMinHeight,
resolvedMaxHeight
)
falseの場合は最小値、最大値ともに、指定されていた場合はtargetConstraintsの値を使う。
指定されていなかった場合は最小値が最大値以下であること、最大値が最小値以上であることを保証する。
今回はtrueの場合と違い、constraints.constrain
を使っていないので、親レイアウトは完全無視の制約を使うことになるわけですね〜。
はい、完全に理解した。
対戦ありがとうございました。僕の勝ち。
まとめ
-
Modifier.width/height/size
は親レイアウトの最小値/最大値制約を考慮してサイズを決定する。- 内部でenforceIncomingにtrueが渡されることで
constraints.constrain
が実行されて、親レイアウトを考慮した制約が適用される。
- 内部でenforceIncomingにtrueが渡されることで
-
Modifier.requiredXXX
は親レイアウトの最小値/最大値制約を無視してサイズを決定する。- 内部でenforceIncomingにfalseが渡されることで、親レイアウト無視で生成された制約がそのまま適用される。
こんな感じかしら。
モヤモヤしなくなるくらいには理解できた。やったね。
Droid Kaigiのセッションに触発されて中身のコード読んでみたけど、理解するのもまとめるのも大変な分、恩恵は大きいなあと感じた。
また今度、別のモヤモヤポイントも読んでまとめてみる。
おわり。