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

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

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

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

固有特性测量的基本用法

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

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

图 1

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

// 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”。实际展示居然是这样的:

图2

嗯??怎么中间的分割线“放飞自我”了?这是因为 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")
                }
图 3

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

图 4

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

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

总结

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

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

参考文献

  1. https://developer.android.google.cn/codelabs/jetpack-compose-layouts?continue=https%3A%2F%2Fdeveloper.android.google.cn%2Fcourses%2Fpathways%2Fcompose%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-layouts#10
  2. https://mp.weixin.qq.com/s/ESJHNzXXjdF95jCf9CNA6Q
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容