固有特性测量是什么

在解释固有特性测量是怎么之前,我们先看这样的例子

假设我们要实现上面的代码的效果,也就是中间线条的宽度是根据两边文字动态来计算的,线条的宽度是两边文字的最大高度

1
2
3
4
5
6
7
8
9
Row(modifier = Modifier.height(IntrinsicSize.Min)) { // 设置 IntrinsicSize。Min
    Text(text = "hello", modifier = Modifier
        .weight(1f).wrapContentWidth())

    Divider(color = Color.Black, modifier = Modifier.width(2.dp).fillMaxHeight()) // 设置最大高度
    
    Text(text = "world", modifier = Modifier
        .weight(1f).wrapContentWidth())
}

在上面的例子中,我们的 Row 组件使用 Modifier.height(IntrinsicSize.Min) 进行修饰,它就是固有特性测量了,然后我们注意到 Divider 的高度设置的 fillMaxHeight,通常情况下 fillMaxHeight 会撑满整个屏幕可用空间高度,而这里并没有,这是因为固有特性测量的原理是,会先获取所有子组件的宽高信息,然后再给自身控件设置宽高,所以即使设置了 fillMaxHeight 本身的控件宽高已经被限制了

如果是常规的方式,不使用固有特性测量的方式,有一种方式,就是文字单独包括在一个层级,线条是文字外层级的高度进行动态设置,也是可以做到的

特性测量原理

固有特性测量的本质就是父组件可在正式测量布局前预先获取到每个子组件宽高信息后通过计算来确定自身的固定宽度或高度,从而间接影响到其中包含的部分子组件布局信息。也就是说子组件可以根据自身宽高信息从而确定父组件的宽度或高度,从而影响其他子组件布局。

SubcomposeLayout 定制测量顺序

在上面的例子中,固有特性测量是根据子组件的大小来确定父组件的大小,最终来影响到子组件的大小,那么有没有一种方式,我不想父组件参与,完全让子组件自己决定,那么就有 SubcomposeLayout 了,SubcomposeLayout 可以根据需要内部的子组件的测量顺序可以自己决定,如果是上方的例子,那么就可以优先测量两边文字的大小,有了文字的大小线条的高度就方便设置了

我们先来看看 SubcomposeLayout 是如何使用的

1
2
3
4
5
@Composable
fun SubcomposeLayout(
    modifier: Modifier = Modifier,
    measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
)

其实 SubComposeLayoutLayout 组件用法差不多。不同的是 measurePolicy 参数,需要传入 SubcomposeMeasureScope 上下文,同样返回一个 MeasureResult 对象, 而 SubcomposeMeasureScope 类中,只有一个方法

1
2
3
interface SubcomposeMeasureScope : MeasureScope {
    fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable>
}

其中,slotId 可以认为是唯一标识,而第二个参数 content 就是我们要测量的组件,返回的 List<Measurable> 其中就包含了测量好的组件宽高大小,接下来,还是上面的例子,我们来看看如何实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Preview
@Composable
fun SubcomposeLayoutTest() {
    Box(modifier = Modifier.fillMaxWidth()) {
        SubcomposeLayout { constraints ->
            val textsConstraint = constraints.copy(maxWidth = constraints.maxWidth / 2)

            // 优先测量左右两边的文字
            val text1Placeable = subcompose("Text1") { Text("hello") }[0].measure(textsConstraint)
            val text2Placeable = subcompose("Text2") { Text("world") }[0].measure(textsConstraint)

            // 测量线条的宽高,并设置线条的高度
            val dividerPlaceable =
                subcompose("Divider") { Divider(color = Color.Black, modifier = Modifier.width(2.dp)) }.first()
                    .measure(constraints.copy(minHeight = maxOf(text1Placeable.height, text2Placeable.height)))

            val width = text1Placeable.width + dividerPlaceable.width + text2Placeable.width

            // 摆放子组件
            layout(width, maxOf(text1Placeable.height, dividerPlaceable.height, text2Placeable.height)) {
                // 摆放左边
                text1Placeable.placeRelative(0, 0)
                // 摆放中间
                dividerPlaceable.placeRelative((constraints.maxWidth - dividerPlaceable.width) / 2, 0)
                // 摆放右边
                text2Placeable.placeRelative(constraints.maxWidth - text2Placeable.width, 0)
            }
        }
    }
}

这段代码首先是测量 Text1Text2 的宽高信息,然后有文字信息后,再给中间的线条设置宽度即可,然后再就是摆放组件,需要留意的是,这个时候我们全程都是根据子组件自身的测量来处理线条的宽度的,并没有父组件的参与,这个就是和固有特性测量的区别,所以代码运行后,效果是一样的

参考: