Jetpack Compose 笔记(6) - Modifier

309 阅读5分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。


回顾

虽然还没有系统地学习过,对 Modifier 我们也不陌生,它用来给组件设置各种属性与监听。对于 Modifier 和直接传参也做过简要的总结:前者多用于通用属性,后者多用于特有属性。

多个 Modifer 有先后关系,典型例子是 Modifier.padding() 一个函数,根据调用顺序不同,可以实现 marginpadding 两个属性的功能。

Modifier 是什么

就是字面意思「是什么」。

跟踪进源码不难发现,它既是接口,又是单例实现。单例实现利用了 kotlin 语法 companion object : Modifier。严格意义上,若想使用这个单例得这样写:Modifier.Companion.padding(),代表 Modifier 这个接口的伴生对象。幸运的是 kotlin 支持缩写,因此可以简单地写成 Modifier.padding()。事实上使用伴生对象正是为了能够以「与接口同名」的方式调用,让代码读起来更自然。

每一种 Modifier (padding, background...) 都是一个具体实现。比较特殊的有两个,一个是 CombinedModifier,顾名思义,就是把两个 Modifier 组合在一起,没错,叒套娃 🪆。

class CombinedModifier(private val outer: Modifier,private val inner: Modifier) : Modifier {
    override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
        outer.any(predicate) || inner.any(predicate)

    override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
        outer.all(predicate) && inner.all(predicate)
    // others...
}

通过对 anyall 两个函数的复写就能看出套娃的本质。any 是判断是否包含符合条件的 Modifier,all 则是判断是否所有 Modifier 都符合条件。

另一个特殊的是单例实现,它就是个废物 仅作为占位符使用。占位符?自定义 Compose 时,通常希望提供一个接口给外部以便控制样式,那么就要这么写:

@Composable
fun Custom(modifier: Modifier) {
    Box(modifier)
}

// 使用
Custom(modifier = Modifier.size(60.dp))

那如果 caller 不想特别指定任何值怎么办?那我们给一个「占位符」:

@Composable
fun Custom(modifier: Modifier = Modifier) {...}

⬆️ 这是自定义 Compose 中的常见写法,并且建议将 modifier 作为第一个有默认值的参数。

除了他俩,其他 Modifier 其实是实现了子接口 Element,这个子接口也是个废物。Element 的存在是为了提供四个函数的默认实现:foldIn, foldOut, any, all,这四个家伙只为了 CombinedModifier 而诞生。例如 all(),对于一个普通的实现,自己符合条件就符合,自己不符合就不符合,何来「全部」之说?其他三个同理。所以后面分析时,我们可以忽略 Element 了。

Modifier 如何影响测量

一如既往,我们还是从现象入手。上面已经给出了暴露接口给外部控制样式的写法,那如果两者冲突呢?

@Composable
fun Custom(modifier: Modifier = Modifier) {
    Box(modifier.background(Color.Green).size(40.dp))
}
// 使用
Custom(modifier = Modifier.size(200.dp))

最终结果是多少?200!虽然看起来先调用 200.dp 后调用 40.dp,40 会覆盖 200,但事实相反。要搞清楚这个,就得大致梳理下测量时布局相关的 Modifier 如何发挥作用。

跟踪 Box() 源码到 ComposeUiNode,是一个接口,包含属性 modifier,它的实现在 LayoutNode 里:

override var modifier: Modifier = Modifier
    set(value) {
      	// ... 省略部分代码
        val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
            var wrapper = toWrap
            if (mod is LayoutModifier) {
                wrapper = ModifiedLayoutNode(wrapper, mod).assignChained(toWrap)
            }
            // ... other modifier branches
            wrapper // ^foldOut
        }
        outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
        outerMeasurablePlaceable.outerWrapper = outerWrapper
    }

关键看这个 modifier.foldOut(),它是从初始值开始由内向外应用一系列的 Modifier 并返回最终值,中间每一次应用的结果都作为下一次的初始值。什么是由内向外?前面说到 CombinedModifier 是一个套娃,每一次 Modifier 函数的调用就是多套了一层。例如

Modifier.size(200.dp).size(40.dp)

的结果形式上类似于:

SizeModifier { // 200dp
    SizeModifier {} // 40dp
}

源码很清晰地看到它用每一层 Modifier 创建了 ModifiedLayoutNode,那么上面的例子最终形式上是这样的:

ModifiedLayoutNode(SizeModifier(200.dp)) {
    ModifiedLayoutNode(SizeModifier(40.dp))
}

这个最终结果一股脑塞给了 outerMeasurablePlaceable.outerWrapper

这时候就可以看 Compose 在 Android 平台的具体实现了:AndroidComposeView,跟踪它的 onMeasure() 最终调用了 outerMeasurablePlaceable.remeasure(constraints),是不是很眼熟?内部又调用了 outerWrapper.measure(constraints)。这个 outerWrapper 恰恰就是我们层层套娃的结果 ModifiedLayoutNode,那就来看它的 measure() 函数:

override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
    with(modifier) {
        measureResult = measureScope.measure(wrapped, constraints)
        this@ModifiedLayoutNode
    }
}

measureScope.measure() 又是接口,留意前面的 with(modifier),它改变了上下文,因此在我们这个例子中,接口的实现在 SizeModifier 里:

override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val wrappedConstraints = // ...
        val placeable = measurable.measure(wrappedConstraints)
        return layout(placeable.width, placeable.height) {
            placeable.placeRelative(0, 0)
        }
    }

measurable 是什么?往回捣弄,就是 ModifiedLayoutNode 第一个构造参数,进而追踪到 ModifiedLayoutNode.modifier 的 setter。这里就明了了,measurable 是内层的 ModifiedLayoutNode分析了那么多,终于得出了结论:Compose 先测量后调用的 Modifier。因此外部设置的属性可以覆盖内部的。

其他 Modifier

Modifier 有许多许多许多。哪怕光是类型也有许多。一次性把他们记住不太现实,还是要在实践中边学边用。这里只写记录目前遇到的比较特殊的。

Modifier.requiredSize

除了 size,还有 requiredSize,它将忽略左侧设置的 size 固执地按照自己的想法来绘制,但这不代表最终大小。可以把左侧 Modifier 想像成父 View,右侧的想像成子 View。

Custom(modifier = Modifier.size(120.dp).requiredSize(60.dp))

这个例子中虽然显示的是 60dp,但父 View 有 120dp 大,所以最后显示的是 120dp,只不过四周都是空白,如图:

Custom(modifier = Modifier.size(60.dp).requiredSize(120.dp))

反过来。虽然子 View 有 120dp,但实际显示的只有 60dp。假设这是一个图片,结果就是显示出来的被裁剪了。

ComposedModifier

ComposedModifier 只是一个壳,事实上可以理解为工厂。某些场景中共享 Modifier 会出现问题,为了避免不经意的错误,这类 Modifier 函数会返回 ComposedModifier,真正用到的时候将生成新的实例。例如:

val mod = Modifier.animateContentSize()
Custom1(mod)
Custom2(mod)

尽管看起来它们共享了 mod,但实际上 mod 只是工厂,具体实例是不同的。