Jetpack-Compose 学习笔记(四)—— Intrinsic 固有特性测量是个啥?看完这篇就知道了

时间:2021-10-07 01:05:25

终于可以写写技术文了~ 最近忙着各种总结,想必大家也是一样的吧?今年年初的规划,现在完成的怎么样了呢?是不是也像我一样“虎头蛇尾”?哈哈!至少竹子比去年进步了不少,这是今年的最后一篇啦!希望2022年大家一起加油!一起进步!

这一篇是为了填上一篇学习笔记三中提到的 Compose 也可多次测量的“坑”,那就是固有特性测量。

Google 起的这名字个人感觉太不直观了,第一次看到这个官方的翻译真的让我一头雾水,这是个啥?其实,这个东西主要作用就是,调节需要展示的 Composable 组件的宽高大小。

固有特性测量的基本用法

前面文章中也提到了,Compose 有一项规则,即子元素只能测量一次,测量两次就会引发运行时异常。但是,有时又需要先收集一些关于子组件的信息,然后再测量父组件。那么,借助固有特性,就可以先查询子组件,然后再进行实际测量。下面是一个栗子。

假如需要像下面展示的那样:

Jetpack-Compose 学习笔记(四)—— Intrinsic 固有特性测量是个啥?看完这篇就知道了

根据之前讲的布局内容,我们很容易就可以写出如下代码:

// code 1
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .wrapContentWidth(Alignment.CenterHorizontally),
            text = text1
        )

        Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
        Text(
            modifier = Modifier
                .weight(1f)
                .wrapContentWidth(Alignment.CenterHorizontally),
            text = text2
        )
    }
}

text1 和 text2 设置为 “Hello”、“World”。实际展示居然是这样的:

Jetpack-Compose 学习笔记(四)—— Intrinsic 固有特性测量是个啥?看完这篇就知道了

嗯??怎么中间的分割线“放飞自我”了?这是因为 Row 没有对它的子组件的测量做任何限制,而 Divider 的高度设置的是 fillMaxHeight,它会尽可能撑大父布局。那么要达到我们想要的效果,就需要使用固有特性 IntrinsicSize先规定一下测量的方式,这里需要将 Row 的 height 设置为 IntrinsicSize.Min,即把 Row 的高度调整为尽可能小的固有高度。具体代码如下:

// code 2
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        ...
    }
}

具体是如何做到的呢?实际上,是因为 Row 父组件通过 IntrinsicSize预先获取到了它左右两边的 Text 组件的高度信息了,然后计算出了两个 Text 组件的高度最大值作为它自己的高度值,最后将分割线的高度铺满整个父组件。

为了实现父组件能预先获得子组件宽高信息从而确定自身宽高信息,Compose 为开发者提供了固有特性测量机制,允许开发者在每个子组件正式测量前能获得各个子组件的宽高等信息。

那么,这玩意儿是怎么实现的呢?

很遗憾竹子没有翻到源码,哪位大神如果找到的话,欢迎一起交流~

虽然没有找到源码,但是也知道了一些关键点。下面是找源码未遂的过程,不感兴趣的同学可以跳过。。

固有特性测量实现的关键点

从使用的地方开始,从 code 2 的 height(IntrinsicSize.Min)进入,到了 Intrinsic.kt 中的一个静态内部类中:

// code 3
private object MinIntrinsicHeightModifier : IntrinsicSizeModifier {
    override fun MeasureScope.calculateContentConstraints(
        measurable: Measurable,
        constraints: Constraints
    ): Constraints {
        val height = measurable.minIntrinsicHeight(constraints.maxWidth)
        return Constraints.fixedHeight(height)
    }

    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
        measurable: IntrinsicMeasurable,
        width: Int
    ) = measurable.minIntrinsicHeight(width)
}

这个静态内部类又是实现了 IntrinsicSizeModifier这个接口,而这个 IntrinsicSizeModifier接口实际上又是实现了 LayoutModifier接口:

// code 4
private interface IntrinsicSizeModifier : LayoutModifier {
    val enforceIncoming: Boolean get() = true

    fun MeasureScope.calculateContentConstraints(
        measurable: Measurable,
        constraints: Constraints
    ): Constraints

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val contentConstraints = calculateContentConstraints(measurable, constraints)
        val placeable = measurable.measure(
            if (enforceIncoming) constraints.constrain(contentConstraints) else contentConstraints
        )
        return layout(placeable.width, placeable.height) {
            placeable.placeRelative(IntOffset.Zero)
        }
    }

    override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ) = measurable.minIntrinsicWidth(height)

    override fun IntrinsicMeasureScope.minIntrinsicHeight(
        measurable: IntrinsicMeasurable,
        width: Int
    ) = measurable.minIntrinsicHeight(width)

    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ) = measurable.maxIntrinsicWidth(height)

    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
        measurable: IntrinsicMeasurable,
        width: Int
    ) = measurable.maxIntrinsicHeight(width)
}

综合 code 3 和 code 4 可以看出,核心的方法就是 measurable.minIntrinsicHeight()这一类的方法。比如在 code 3 中,重写的 MeasureScope.calculateContentConstraints方法和 IntrinsicMeasureScope.maxIntrinsicHeight方法,最重要的部分都是调用了 measurable.minIntrinsicHeight()方法。这个 minIntrinsicHeight都会传一个 width 参数进去,点进去,发现是一个 IntrinsicMeasurable接口,接口方法的说明如下所示:

// code 5  IntrinsicMeasurable.kt
/**
 * Calculates the minimum height that the layout can be such that
 * the content of the layout will be painted correctly.
 */
fun minIntrinsicHeight(width: Int): Int

方法的注释写的很清楚:计算能正确绘制 layout 内容时的 layout 的最小高度。OK,因为每个 Composable 组件摆放子组件的方式不同,所以每个组件的 IntrinsicMeasurable接口的实现方式就不同了,但是没有找到 Row 组件具体实现的源码。。此路不通,看有没有其他的路,在上篇笔记三中,我们知道 Composable 组件计算自身宽高是在 Layout 方法中进行的,那么从 Layout 处入手看会怎样呢?

从 Row 的 Layout 方法进入到 Layout.kt,测量的部分肯定是在 MeasurePolicy 类中:

// code 6    Layout.kt
@Composable inline fun Layout(
    content: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
   ······
}

其实 MeasurePolicy 不是一个类,而是一个接口,在这个接口中,可以看到实现的 IntrinsicMeasureScope.minIntrinsicHeight()方法:

// code 7    MeasurePolicy.kt
    /**
     * The function used to calculate [IntrinsicMeasurable.minIntrinsicHeight]. It represents
     * defines the minimum height this layout can take, given  a specific width, such
     * that the content of the layout will be painted correctly.
     */
    fun IntrinsicMeasureScope.minIntrinsicHeight(
        measurables: List<IntrinsicMeasurable>,
        width: Int
    ): Int {
        val mapped = measurables.fastMap {
            DefaultIntrinsicMeasurable(it, IntrinsicMinMax.Min, IntrinsicWidthHeight.Height)
        }
        val constraints = Constraints(maxWidth = width)
        val layoutReceiver = IntrinsicsMeasureScope(this, layoutDirection)
        val layoutResult = layoutReceiver.measure(mapped, constraints)
        return layoutResult.height
    }

注释写的比较清楚:这个方法是用来计算 IntrinsicMeasurable.minIntrinsicHeight,它定义了在给定宽度的情况下,该布局在正确绘制布局内容的情况下,可以获得的最小高度。

怎么获得的呢?看代码是先通过 DefaultIntrinsicMeasurable 类求出每个子组件的最小高度,最小高度的计算还是调用的 measurable.minIntrinsicHeight(constraints.maxWidth)方法。。。呃。。又回到了之前的 IntrinsicMeasurable 接口中的 fun minIntrinsicHeight(width: Int): Int方法(笑Cry.jpg)。虽然没有找到真正实现这个接口的代码,但是通过上面的源码跟踪,竹子也得知了两个关键点。

关键点一就是 IntrinsicMeasurable 这个接口,不光是 minIntrinsicHeight方法,同样的还有 maxIntrinsicHeightminIntrinsicWidthmaxIntrinsicWidth这一类的方法都是在 IntrinsicMeasurable 接口,真正实现了这四个方法的地方就是真正实现了固有特性测量的地方。

再来看这些方法的参数,都是对应的另一个尺寸的极值,这些都在 DefaultIntrinsicMeasurable 类中所有体现:

// code 8    LayoutModifier.kt    DefaultIntrinsicMeasurable class
        override fun measure(constraints: Constraints): Placeable {
            if (widthHeight == IntrinsicWidthHeight.Width) {
                val width = if (minMax == IntrinsicMinMax.Max) {
                    measurable.maxIntrinsicWidth(constraints.maxHeight)
                } else {
                    measurable.minIntrinsicWidth(constraints.maxHeight)
                }
                return EmptyPlaceable(width, constraints.maxHeight)
            }
            val height = if (minMax == IntrinsicMinMax.Max) {
                measurable.maxIntrinsicHeight(constraints.maxWidth)
            } else {
                measurable.minIntrinsicHeight(constraints.maxWidth)
            }
            return EmptyPlaceable(constraints.maxWidth, height)
        }

比如之前是传入的 widthHeight = IntrinsicWidthHeight.HeightminMax = IntrinsicMinMax.Min,所以就是调用的 measurable.minIntrinsicHeight(constraints.maxWidth),也就是将 约束条件中 width 最大值传给了方法。

那之前我们仅使用 Modifier.height(IntrinsicSize.Min)为 Row 的高度设置了固有特性测量并没有设置宽度啊?那是因为会将约束条件的 width 最大值作为默认值传进去,如 code 3 中的代码,这里的最大值其实就是不限制宽度的大小,所以 Modifier.height(IntrinsicSize.Min)所表达的意思就是,当宽度不限时通过子组件预先测量的宽高信息所能计算的 Row 最小高度是多少。当然也可以自己设置一个宽度,那么子组件就可以根据你设置的 Row 宽度以及预先测量的宽高信息得出 Row 的最小高度是多少。这就是关键点二。

宽度受限会影响高度的例子很常见的就是 TextView 中显示长文本的情况。显示内容不变时,宽度越小高度自然会越大,可看参考文献2 中的例子。

上面说的都是在 Compose 官方提供的 Composable 组件中的情况,那么在自定义 Layout 中呢?很遗憾,如果我们要在自定义 Layout 中使用固有特性测量,则必须自己实现,否则会有问题。

实现自定义layout中的固有特性测量

由之前的 学习笔记三 可知,自定义 Layout 主要还是重写了 Layout()方法,如果我们要适配自己写的自定义 Layout 的固有特性测量,就需要对 Layout()方法中的 MeasurePolicy 接口进行重写了。

之前的自定义 Layout 主要是重写了 MeasurePolicy 接口的 measure方法,如果要实现固有特性测量,则还需要重写相应的 Intrinsic 方法,具体来说一共有四个:

override fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>, width: Int)
override fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List<IntrinsicMeasurable>, width: Int)
override fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int)
override fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int)

不一定全部都要实现,根据具体的需求,需要用到哪种固有特性测量,实现哪种方法即可。例如竹子这里实现了一个自定义的 Column 组件,实现了 minIntrinsicWidth方法:

// code 9
// 自定义 Column 组件
@Composable
fun MyColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content,
        measurePolicy = object: MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                // 自定义 Layout 实现测量与摆放的逻辑
                var height = 0    // 自定义 Layout 高度
                var width = 0    // 自定义 Layout 宽度
                val placeables = measurables.map {
                    val placeable = it.measure(constraints)
                    height += placeable.height
                    width = max(width, placeable.width)
                    placeable
                }

                return layout(width, height) {
                    var lastHeight = 0
                    placeables.map {
                        it.placeRelative(0, lastHeight)
                        lastHeight += it.height
                    }
                }
            }

            override fun IntrinsicMeasureScope.minIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,
                width: Int
            ): Int {
                TODO("Not yet implemented")
            }

            override fun IntrinsicMeasureScope.maxIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,
                width: Int
            ): Int {
                TODO("Not yet implemented")
            }

            override fun IntrinsicMeasureScope.minIntrinsicWidth(
                measurables: List<IntrinsicMeasurable>,
                height: Int
            ): Int {
                var maxWidth = 0
                measurables.forEach {
                    maxWidth = max(maxWidth, it.minIntrinsicWidth(height))
                }
                return maxWidth
            }

            override fun IntrinsicMeasureScope.maxIntrinsicWidth(
                measurables: List<IntrinsicMeasurable>,
                height: Int
            ): Int {
                TODO("Not yet implemented")
            }
        })
}

这样就可以在 MyColumn 组件的 Modifier 中使用 IntrinsicSize 了,具体使用及显示效果如下:

// code 10
                MyColumn(modifier = Modifier.width(IntrinsicSize.Min)) {
                    Text(text = "watermelon")
                    Text("apple")
                    Divider(color = Color.Black, modifier = Modifier.height(2.dp).fillMaxWidth())
                    Text("orange")
                }

Jetpack-Compose 学习笔记(四)—— Intrinsic 固有特性测量是个啥?看完这篇就知道了

如果不使用 Modifier.width(IntrinsicSize.Min)固有特性测量,则显示的效果就会是这样的:

Jetpack-Compose 学习笔记(四)—— Intrinsic 固有特性测量是个啥?看完这篇就知道了

所以如果需要自定义 Layout 适配固有特性测量,则需要实现相应的方法,个人觉得还是挺麻烦的。。。此外,在 code 9 中 minIntrinsicWidthmeasure方法中分别打上断点,可以发现,Compose 确实是在 measure 父组件前,就先调用了 minIntrinsicWidth方法去获取了子组件的宽高。

而且这里还有个 bug,如果设置的文案既有中文又有英文,则会换行。。。包含空格符也会换行,有兴趣的同学可以试一下。不知道 Compose 何时才能修复这个 bug~

总结

Compose 为了避免传统 View 体系重复测量导致的性能问题,规定了只能测量一次子组件的规则,否则会出现运行时异常。但是在有些需要多次测量的使用场景,Compose 提出了设置固有特性测量的解决方案。固有特性测量的设置,就是允许父组件在正式测量自身宽高前,去获取子组件的宽高信息,从而确定自己的宽高。从以上的例子可以看出,子组件可以根据自己的宽高信息来决定父组件的宽高信息,从而影响其他子组件的布局和宽高信息。

好了,固有特性测量就介绍到这里,欢迎关注我,解锁更多 Android 开发新知识!

参考文献

  1. developer.android.google.cn/codelabs/je…
  2. mp.weixin.qq.com/s/ESJHNzXXj…