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。 您的替换项需要与 measure 的操作一致。例如,在垂直堆叠其子项并为每个子项提供完整列宽的自定义列中,给定 width 的最小固有高度是其子项针对该 width 的最小固有高度之和:

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