「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
回顾
虽然还没有系统地学习过,对 Modifier 我们也不陌生,它用来给组件设置各种属性与监听。对于 Modifier 和直接传参也做过简要的总结:前者多用于通用属性,后者多用于特有属性。
多个 Modifer 有先后关系,典型例子是 Modifier.padding()
一个函数,根据调用顺序不同,可以实现 margin
或 padding
两个属性的功能。
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...
}
通过对 any
和 all
两个函数的复写就能看出套娃的本质。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
只是工厂,具体实例是不同的。