Compose 版面配置中的內建函式測量資料

Compose 的其中一項規則是只能測量子項一次;如果測量子項兩次,系統將擲回執行階段例外狀況。不過有時候,您必須先掌握子項的某些相關資訊,才能進行測量。

內建函式可讓您在實際測量前查詢子項相關資訊。

如果是可組合項,您可以查詢其 IntrinsicSize.Min 或 IntrinsicSize.Max:

  • Modifier.width(IntrinsicSize.Min):正確顯示內容所需的最小寬度為何?
  • Modifier.width(IntrinsicSize.Max):正確顯示內容所需的最大寬度為何?
  • Modifier.height(IntrinsicSize.Min):正確顯示內容所需的最小高度為何?
  • Modifier.height(IntrinsicSize.Max):正確顯示內容所需的最大高度為何?

舉例來說,如果在自訂版面配置中,width 限制設為無限的情況下查詢 Text 的 minIntrinsicHeight,系統會將文字視為繪製在單一直線上並傳回 Text 的 height。

內建函式實際使用狀況

您可以建立一個可組合項,在畫面上顯示兩個文字元素並以分隔線隔開:

兩個文字元素並排顯示,中間有一個垂直的分隔線

方法是使用 Row,搭配兩個填滿可用空間的 Text 可組合項,以及中間的 Divider。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 展開至整個畫面,這並非預期行為:

兩個文字元素並排顯示,中間有一個垂直的分隔線,一直延伸至文字底部以下

之所以會發生這種情況,是因為 Row 會個別測量每個子項,而 Text 的高度無法用於限制 Divider。

如要讓 Divider 填滿指定高度的可用空間,請改用 height(IntrinsicSize.Min) 修飾符。

height(IntrinsicSize.Min) 會將子項的高度調整為內建函式的最低高度。由於這個修飾符有遞迴性,因此會查詢 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")
        }
    }
}

預覽如下:

兩個文字元素並排顯示,中間有一個垂直的分隔線

Row 的高度決定方式如下:

  • Row 可組合項的 minIntrinsicHeight 是其子項的 minIntrinsicHeight 上限。
  • Divider 元素的 minIntrinsicHeight 為 0,因為在未指定限制的情況下,該元素不會占用空間。
  • Text minIntrinsicHeight 是特定 width 的文字。
  • 因此,Row 元素的 height 限制會變成 Text 的 minIntrinsicHeight 上限。
  • Divider 隨即會將其 height 擴展至 Row 指定的 height 限制。

自訂版面配置中的內建函式

建立自訂 Layout 或 layout 修飾符時,系統會根據估計值自動計算內建函式測量結果。因此,這些計算結果的正確性會依版面配置而異。這些 API 會提供覆寫這些預設值的選項。

如要指定自訂 Layout 的內建函式測量資料,請在建立 MeasurePolicy 介面時覆寫 minIntrinsicWidth、minIntrinsicHeight、maxIntrinsicWidth 和 maxIntrinsicHeight。

@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 介面中覆寫相關方法。

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.
    }