Compose レイアウトの固有の測定値

Compose のルールのひとつとして、子を 1 回しか測定できないことが挙げられます。子を 2 回測定した場合、ランタイム例外がスローされます。ただし、測定する前に子の情報が必要になる場合もあります。

Intrinsic を使用すると、実際に測定する前に子をクエリできます。

コンポーザブルに対して、次のように IntrinsicSize.Min または IntrinsicSize.Max を要求できます。

  • Modifier.width(IntrinsicSize.Min) - コンテンツを適切に表示するために必要な最小幅はどれくらいですか?
  • Modifier.width(IntrinsicSize.Max) - コンテンツを正しく表示するために必要な最大幅はどれくらいですか?
  • Modifier.height(IntrinsicSize.Min) - コンテンツを適切に表示するために必要な最小の高さ。
  • Modifier.height(IntrinsicSize.Max) - コンテンツを適切に表示するために必要な最大の高さ。

たとえば、カスタム レイアウトで無限大の width 制約を持つ Text の minIntrinsicHeight を要求すると、テキストが 1 行で描画された Text の height が返されます。

Intrinsic の使い方

2 つのテキストを分割線で区切って画面上に表示するコンポーズ可能な関数を作成できます。

2 つのテキスト要素を横に並べ、その間に縦方向の分割線を配置します

これを行うには、利用可能なスペースを埋める 2 つの Text コンポーザブルと、中央の Divider を含む Row を使用します。Divider の高さは最も高い Text と同じにして、幅は狭くします(width = 1.dp)。

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        VerticalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

Divider が画面全体に拡大されます。これは想定される動作ではありません。

2 つのテキスト要素が横に並び、分割線で区切られていますが、分割線がテキストの下まで伸びています

これは、Row がそれぞれの子を個別に測定し、Text の高さを Divider の制約に使用できないためです。

代わりに、Divider が指定の高さで空きスペースを埋めるように設定するには、height(IntrinsicSize.Min) 修飾子を使用します。

height(IntrinsicSize.Min) は、子の高さが Intrinsic の最小の高さと同じになるようにします。この修飾子は再帰的であるため、Row の minIntrinsicHeight とその子をクエリします。

この修飾子をコードに適用すると、想定どおりに動作します。

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        VerticalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

// @Preview
@Composable
fun TwoTextsPreview() {
    MaterialTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

プレビュー:

2 つのテキスト要素を横に並べ、その間に縦方向の分割線を配置します

Row の高さは次のように決定されます。

  • Row コンポーザブルの minIntrinsicHeight は、その子の最大 minIntrinsicHeight です。
  • Divider 要素の minIntrinsicHeight は、制約がない場合はスペースを占有しないため 0 になります。
  • Text minIntrinsicHeight は、特定の width のテキストのものです。
  • そのため、Row 要素の height 制約は、Text の最大 minIntrinsicHeight になります。
  • Divider は自身の height を、Row で指定された height 制約まで拡大します。

カスタム レイアウトでの Intrinsic

カスタムの Layout 修飾子または layout 修飾子を作成すると、近似値に基づいて固有の測定値が自動的に計算されます。このため、すべてのレイアウトで計算が不正確になる場合があります。これらの API には、こうしたデフォルト値をオーバーライドするオプションが用意されています。

カスタム Layout の組み込み測定値を指定するには、作成時に MeasurePolicy インターフェースの minIntrinsicWidth、minIntrinsicHeight、maxIntrinsicWidth、maxIntrinsicHeight をオーバーライドします。オーバーライドは measure の動作と一致する必要があります。たとえば、子を垂直方向に積み重ね、各子に列幅全体を割り当てるカスタム列では、特定の width の最小 Intrinsic の高さは、その width の子の最小 Intrinsic の高さの合計になります。

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier,
        measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                val childConstraints = constraints.copy(minHeight = 0)
                val placeables = measurables.map { measurable ->
                    measurable.measure(childConstraints)
                }
                val width = constraints.constrainWidth(
                    placeables.maxOfOrNull { it.width } ?: 0
                )
                val height = constraints.constrainHeight(
                    placeables.sumOf { it.height }
                )
                return layout(width, height) {
                    var yPosition = 0
                    placeables.forEach { placeable ->
                        placeable.placeRelative(x = 0, y = yPosition)
                        yPosition += placeable.height
                    }
                }
            }

            override fun IntrinsicMeasureScope.minIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,
                width: Int
            ): Int {
                return measurables.sumOf { measurable ->
                    measurable.minIntrinsicHeight(width)
                }
            }

            // Other intrinsics related methods have a default value,
            // you can override only the methods that you need.
        }
    )
}

カスタム layout 修飾子を作成する場合は、LayoutModifier インターフェースの関連するメソッドをオーバーライドします。組み込み計算は measure と一致する必要があります。たとえば、特定の height に対して、すべての辺に固定パディングを追加する修飾子は、子の minIntrinsicWidth をクエリする前に垂直パディングを減算し、結果に水平パディングを追加します。

fun Modifier.myCustomModifier(padding: Dp = 8.dp) =
    this then object : LayoutModifier {
        override fun MeasureScope.measure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
            val paddingPx = padding.roundToPx()
            val horizontal = paddingPx * 2
            val vertical = paddingPx * 2
            val placeable = measurable.measure(
                constraints.offset(-horizontal, -vertical)
            )
            val width = constraints.constrainWidth(placeable.width + horizontal)
            val height = constraints.constrainHeight(placeable.height + vertical)
            return layout(width, height) {
                placeable.place(paddingPx, paddingPx)
            }
        }

        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurable: IntrinsicMeasurable,
            height: Int
        ): Int {
            val paddingPx = padding.roundToPx()
            val horizontal = paddingPx * 2
            val vertical = paddingPx * 2
            val childHeight = if (height == Constraints.Infinity) {
                Constraints.Infinity
            } else {
                (height - vertical).coerceAtLeast(0)
            }
            return measurable.minIntrinsicWidth(childHeight) + horizontal
        }

        // Other intrinsics related methods have a default value,
        // you can override only the methods that you need.
    }