LoginSignup
12
8

More than 1 year has passed since last update.

【Android】Modifier.requiredXXXについて、モヤモヤしなくなるくらいに理解する【Jetpack Compose】

Last updated at Posted at 2021-11-28

はじめに

Modifierでサイズを指定するとき、widthrequiredWidthheightrequiredHeightsizerequiredSizeがあるよね。

Composeの公式ドキュメントを読めば言いたいことは大体理解できる。
親レイアウトが指定したサイズを無視して、子レイアウトのサイズを決定したい場合にrequiredXXXを使うよってことよね。

これはわかる。
わかるんだけど、いざコードのコメントを和訳して読んでみると、


requireSize
コンテンツのサイズを正確に[size] dpの幅と高さになるように宣言します。 着信測定[制約]はこの値を上書きしません。
コンテンツが着信[制約]を満たさないサイズを選択した場合、親レイアウトは[制約]で強制されたサイズとして報告され、コンテンツの位置は、割り当てられたスペースを中心に自動的にオフセットされます。
[制約]が尊重されることを前提とした親レイアウトによる子。


はいこの通り。
全然さっぱりわけがわからない。

このままでは、予測にrequiredXXXが出てくるたびにモヤモヤしてしまう。
それはあまりにも精神衛生に良くない。
ということで、実際にコードを読んでみて、なんとなくModifier.requiredXXXについて理解し、モヤモヤせず開発できるようになるくらいの粒度でまとめていくよ。

ちなみに、「width requireWidth 違い」って結果だけ欲しい人は、まとめだけ読んでくれれば多分満足できるよ。

widthとrequiredWidthの違い

まずは、Modifier.widthModifier.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が実行されて、親レイアウトを考慮した制約が適用される。
  • Modifier.requiredXXXは親レイアウトの最小値/最大値制約を無視してサイズを決定する。
    • 内部でenforceIncomingにfalseが渡されることで、親レイアウト無視で生成された制約がそのまま適用される。

こんな感じかしら。
モヤモヤしなくなるくらいには理解できた。やったね。

Droid Kaigiのセッションに触発されて中身のコード読んでみたけど、理解するのもまとめるのも大変な分、恩恵は大きいなあと感じた。
また今度、別のモヤモヤポイントも読んでまとめてみる。

おわり。

参考

12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8