Jetpack Compose 笔记(6) - 硬核深入 Modifier
⚠️ 警告:这一节不是罗列各种 Modifier 的使用与效果,而是深入它们的原理,不适合快餐式学习。 只想学简单用法的建议看文档或随便一个免费的视频教程。
⚠️ 警告:这一节非常硬核,我花了三天学习,所以不建议尝试短时间看完并理解。
本节源码参照 Compose 1.3.0-alpha02
回顾
虽然还没有系统地学习过,对 Modifier 我们也不陌生,它用来给组件设置各种属性与监听。对于 Modifier 和直接传参也做过简要的总结:前者多用于通用属性,后者多用于特有属性。
多个 Modifer 有先后关系,典型例子是 Modifier.padding()
一个函数,根据调用顺序不同,可以实现 margin
或 padding
两个属性的功能。
Modifier 是什么
就是字面意思「是什么」。
接口与单例实现
跟踪进源码不难发现,它既是接口,又是单例实现。单例实现利用了 kotlin 语法 companion object : Modifier
。严格意义上,若想使用这个单例得这样写:Modifier.Companion.padding()
,代表 Modifier
这个接口的伴生对象。幸运的是 kotlin 支持缩写,因此可以简单地写成 Modifier.padding()
。事实上使用伴生对象正是为了能够以「与接口同名」的方式调用,让代码读起来更自然。
除此之外这个单例实现就是个废物, 仅作为占位符使用。占位符?自定义 Compose 时,通常希望提供一个接口给外部以便控制样式,那么就要这么写:
@Composable
fun Custom(modifier: Modifier) {
Box(modifier)
}
// 使用
Custom(modifier = Modifier.size(60.dp))
那如果 caller 不想特别指定任何值怎么办?那我们给一个「占位符」:
@Composable
fun Custom(modifier: Modifier = Modifier) {...}
⬆️ 这是自定义 Compose 中的常见写法,并且建议将 modifier
作为第一个有默认值的参数。
注意 modifier: Modifier = Modifier
这里,两个 Modifier
不是一个东西,前者是接口定义,后者是伴生对象的单例实现,它等价于这么写:modifier: Modifier = Modifier.Companion
。
CombinedModifier
每一种 Modifier (padding, background...) 都是一个实现类。比较特殊的除了单例实现外,就是 CombinedModifier
,顾名思义,它是把两个 Modifier 组合在一起,没错,叒套娃 🪆,这个套娃由 Modifier.then()
方法创建,先加入的 Modifier 作为 outer
,后加入的作为 inner
。
例如调用 Modifier.padding(8.dp).size(60.dp)
,就会创建一个 CombinedModifier
,它的 outer
是 PaddingModifier
,inner
是 SizeModifier
。
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 都符合条件。
举个例子:
Modifier.padding(8.dp).size(60.dp).padding(4.dp) 返回值如下:
CombinedModifier(
outer = CombinedModifier(
outer = PaddingModifier(8.dp),
inner = SizeModifier(60.dp),
),
inner = PaddingModifier(4.dp),
)
Element
除了单例实现与 CombinedModifier
,其他 Modifier 其实是实现了子接口 Element
,这个子接口也是个废物。Element
的存在是为了提供四个方法的默认实现避免编写重复代码:foldIn, foldOut, any, all
。这四个家伙只为了 CombinedModifier
而诞生,对于其他实现类来说没啥意义。不妨再解释一下这四个方法的作用:
foldIn()
: 从外层(先加入的 Modifier)开始,依次应用到初始对象initial: R
上,返回应用后的对象。foldOut()
: 从内层(后加入的 Modifier)开始,依次应用到初始对象initial: R
上,返回应用后的对象。any()
: 自己或自己所包含的 Modifier 是否有一个符合给定条件。all()
: 自己以及自己所包含的 Modifier 是否全部符合给定条件。
例如 all()
,对于一个普通的实现类,它不包含其他 Modifier,自己符合条件就符合,自己不符合就不符合,何来「全部」之说?其他三个同理。所以后面分析时,我们可以忽略 Element
了。
ComposedModifier
ComposedModifier 的最简用法如下:
val modifier = Modifier.composed { Modifier.padding(8.dp) }
// 然后就可以像普通 Modifier 那样使用。
ComposedModifier 实现的是 Element
接口,所以某种程度上来讲,它没有前几个那么「特殊」,但也很特殊。它同样不提供任何功能,而是工厂模式的实现 —— 把另一个 Modifier 包在一个工厂函数里。在自己真正被使用时才会执行这个工厂函数创建出实际的 Modifier。
什么叫被真正使用? 随便跟踪一个接收 Modifier 的 Compose 函数,最终可以找到一个扩展函数:Composer.materialize(modifier: Modifier)
,它的内部代码如下:
// Composer.materialize(modifier: Modifier)
// 删除了一些代码
val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->
acc.then(
if (element is ComposedModifier) {
val factory = element.factory as Modifier.(Composer, Int) -> Modifier
val composedMod = factory(Modifier, this, 0) // 执行工厂函数得到具体的 Modifier
materialize(composedMod)
} else {
newElement
}
)
}
可以看出,这个函数遍历 Modifier 然后创建了一串新的 Modifier,这不是多此一举吗?关键在于这个过程中递归执行了 ComposedModifier
的工厂函数,用生成的 Modifier 替换原先的 ComposedModifier,这就是所谓「被真正使用」的时候。
那... 有啥用? Modifier.composed()
的注释给了我们提示:它用于创建独立的有状态的 Modifier。所谓「独立」就是它会生成多个 Modifier 对象,互不影响。这里的「状态」和前几节讨论的 Compose 本身的「状态」是一个意思。比如 Modifier.padding(8.dp)
,它的状态是 8dp
,但这个值只是一个外部传递的参数,不能算作它内部的状态(PaddingModifier
没有能力维护/修改这个值)。此时用 ComposedModifier 的话可以这么写:
val modifier = Modifier.composed {
var padding by remember { mutableStateOf(8.dp) }
Modifier.padding(padding).clickable { padding = 0.dp }
}
Text("chenhe", modifier)
这里 padding
就成了名副其实的内部状态,并且可以修改它了。
此时好事者会排出一个类似的代码,质疑这两种写法有什么区别:
// 另一种写法
var padding by remember { mutableStateOf(8.dp) }
val modifier = Modifier.padding(padding).clickable { padding = 0.dp }
Text("chenhe", modifier)
在这个用例下的确没区别,但如果把 modifier 应用到多个组件中,前者 ComposedModifier 的写法可以保持独立,即某一个组件点击后 padding 变成 0,其他的依然是初始值 8。后一种写法所有组件共享状态,点击某一组件后所有组件的 padding 都会变成 0。
道理懂了,那使用场景呢?
像上面那种演示写法实际项目中不会出现的,这种需求我们宁愿显式创建多个普通的 Modifier,这样更清晰。ComposedModifier
主要用途是:
- 封装带状态的 Modifier。
比如现在想把上面的演示写法封装进一个自定义 Modifier 方法
Modifier.clickToClearPaddingModifier()
那就只能这么写。 - 封装的 Modifier 需要 Compose 环境。
有时候我们想要的并不是状态,而仅仅是一个 Compose 执行环境,比如希望使用
LaunchedEffect()
方法或者读取 ComposiltionLocal 值的时候,Modifier.composed()
接收的 lambda 自带@Composable
注解。
比如官方的 Modifier.animateEnterExit()
就利用 ComposedModifier 来获得 Compose 环境,从而可以调用其他 Compose 方法。
LayoutModifier
上面已经研究完了特殊的 Modifier,现在开始研究有实际意义的那些。首当其冲的是 LayoutModifier
,它是一个接口,与布局相关,padding, size
等都实现了它。
工作原理
先看一个现象,如果多次调用 Modifier.size()
,最终的值是多少呢?
@Composable
fun Custom(modifier: Modifier = Modifier) {
Box(modifier.background(Color.Green).size(40.dp))
}
// 使用
Custom(modifier = Modifier.size(200.dp))
虽然看起来后调用 40 会覆盖先调用 200,但事实相反,最终值是 200。要搞清楚这个,就得大致梳理下测量时 LayoutModifier 如何发挥作用。
事实上,Compose 每一个组件最终都会生成 LayoutNode
对象,作为这个组件的代表存在内存中。它们的测量与布局由 LayoutNode.remeasure()
和 LayoutNode.replace()
方法负责,其实前者已经做完了所有工作,包括布局的计算,后者只是用计算的结果布局一下。那我们跟踪一下 LayoutNode.remeasure()
方法,发现最终调用的是 LayoutNodeLayoutDelegate.performMeasure()
:
// LayoutNodeLayoutDelegate
private fun performMeasure(constraints: Constraints) {
check(layoutState == LayoutState.Idle) {
"layout state is not idle before measure starts"
}
layoutState = LayoutState.Measuring
measurePending = false
layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(
layoutNode,
affectsLookahead = false
) {
outerWrapper.measure(constraints) // 关键看这一行
}
// ... 省略一些代码
}
LayoutNodeLayoutDelegate
是真正负责测量的对象,Google 团队把这块逻辑抽了出去以避免 LayoutNode 代码太多(看看 View 的源码你会深有体会)。那 outerWrapper
是啥?我们得研究下操作这个 Delegate 的地方。
LayoutNode.modifier
储存了经过预处理(解析 ComposedModifier
等工作)的所有添加到这个组件的 Modifier(若有多个则以 CombinedModifier
的套娃形式保存),看看它的 setter:
// LayoutNode
// 省略部分代码
internal val innerLayoutNodeWrapper = InnerPlaceable(this)
override var modifier: Modifier = Modifier
set(value) {
val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
// mod 是遍历到的那一个 Modifier,toWrap 是上一轮应用的结果(或初始值)
var wrapper = toWrap
if (mod is LayoutModifier) {
wrapper = ModifiedLayoutNode(toWrap, mod).apply {
onInitialize()
updateLookaheadScope(mLookaheadScope)
}
}
// ... other modifier branches
wrapper // ^foldOut
}
outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
layoutDelegate.outerWrapper = outerWrapper
}
关键看这个 modifier.foldOut()
,它是从初始值开始由内向外(从后添加的向先添加的)应用一系列的 Modifier 并返回最终值,中间每一次应用的结果都作为下一次的初始值。最终结果一股脑塞给了 layoutDelegate.outerWrapper
,它的默认值是 LayoutNode.innerLayoutNodeWrapper
,现在被替换成了包裹了一系列 Modifier 的 outWrapper
。注意,outWrapper
包裹的是最外层,也就是最先添加的 Modifier。
看一个例子:
Box(Modifier.padding(10.dp).padding(20.dp))
生成的 outerWrapper 是这样的:
ModifiedLayoutNode(
modifier = PaddingModifier(10.dp),
wrapped = ModifiedLayoutWrapper(
modifier = PaddingModifier(20.dp),
wrapped = InnerPlaceable
)
)
那么继续看 ModifiedLayoutNode.measure()
方法:
// ModifiedLayoutNode
override fun measure(constraints: Constraints): Placeable {
performingMeasure(constraints) {
with(modifier) { // 这个 modifier 是 LayoutModifier
measureResult = measure(wrapped, constraints) // 处于 LayoutModifier 上下文内
this@ModifiedLayoutNode
}
}
onMeasured()
return this
}
内部调用了 LayoutModifier
接口内的 MeasureScope.measure()
方法,显然,每一个 LayoutModifier
的实现类都一定会实现这个方法。这里传入的参数 wrapped
来自构造函数,在 LayoutNode.modifier
的 setter 中传入的,就是内层的 ModifiedLayoutNode
(或初始值 innerLayoutNodeWrapper
)。
到此就可以串起来了,最先被调用的是 outWrapper.measure()
,它内部再调用对应 Modifier 的 measure(measurable: Measurable, constraints: Constraints)
方法,传入内层的 ModifiedLayoutNode
作为参数。这个方法的具体实现中肯定要先调用 measurable.measure()
再在返回值的基础上修改。所以真正先执行测量的是内部 Modifier(也就是后添加的)。 先应用的 size 覆盖了后应用的也就不足为奇了。
小测验
Box(modifier.padding(10.dp).padding(40.dp))
执行结果是什么?
这里就不是覆盖了,具体什么行为要看 Modifier 怎么对待内部的测量结果。PaddingModifier 选择在内部结果的基础上添加边距,所以执行结果是添加了 50dp 的边距。
Modifier.layout()
LayoutModifierImpl
是 Modifier.layout()
的返回值,从名字就能看出它是 LayoutModifier
接口的最基本实现,可以调整这个组件的位置与尺寸。
Andriod 传统 UI 系统中,自定义 View 有三大块:
- 自定义布局
- 自定义绘制
- 自定义触摸
在 Compose 中虽然没有了 View 的概念,但自定义的概念与需求依然存在。
Modifier.layout()
方法就与「自定义布局」息息相关。
Modifier.layout()
的用处是修改对应组件的尺寸与位置偏移。注意,只是修改,不能完全自定义。 先来看一下基本用法,实现与默认情况一致的布局效果:
Text(text = "Chenhe", Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints) // 测量自己,此时可以修改限制
layout(placeable.width, placeable.height) { // 布局自己,此时可以传入修改后的尺寸
placeable.placeRelative(0, 0) // 设置位置偏移 placeRelative 可自适应 RTL
}
})
和传统的 View 系统很像,这里包含两个步骤:使用父组件给的限制测量自己,然后布局一下自己。里面的 layout()
函数是创建 MeasureResult
的便捷写法。既然这样,就做一下类比。
功能 | 自由度 | |
---|---|---|
Modifier.layout() |
测量自己 | 只能修改标准测量的结果 |
View.onMeasure() |
测量子 View,然后测量自己。 | 可以完全自己计算宽高 |
layout 中的 placementBlock 参数 |
设置自己的位置偏移 | 只能设置整体偏移,不能布局子组件 |
View.onLayout() |
具体布局子 View | 随意布局子 View |
可以看出 Modifier.layout()
不如传统的自定义 View 灵活。这很好理解,Modifier 终究只是一个附加的东西,它不是组件自己,也无法获取组件的各种参数,得不到参数又怎样进行完全自定义的计算呢?而 View
不同,它的测量与布局方法就写在自己的类里,完全有能力根据各种参数以及子 View 的各种参数来计算。
所以 LayoutModifierImpl
不能实现完整的自定义布局,它适用于给组件增加装饰效果。 所谓「装饰效果」就是不干涉组件内部测量布局,只在外部调整,例如 padding。
如果想修改组件内部效果,唯一的办法是改组件源码。如果组件不是自己写的,那只能把它拷贝一份来修改。注意函数不是类,不能像 View 那样继承。
Modifier.requiredSize()
除了 size,还有 requiredSize
,它将忽略左侧设置的 size 固执地按照自己的想法来测量,但它不能左右最终大小。
Custom(modifier = Modifier.size(120.dp).requiredSize(60.dp))
这个例子中 size
给内层测量的限制是 120 并自己测量为 120,但内层是一个固执的 requiredSize
,它偏要测量为 60。那么视觉上只有 60,但实际上是 120,效果是四周都是空白,如图:
Custom(modifier = Modifier.size(60.dp).requiredSize(120.dp))
反过来。size
给内部的限制是 60,requiredSize
再一次固执地按照 120 测量。那么这个组件按照 120 绘制,但显示出来的时候只有 60 的那一部分。假设这是一个图片,结果就是被裁剪了。
DrawModifier
LayoutModifier
与布局相关,DrawModifier
则与绘制相关。background
等是它的实现。
工作原理
处理
要知道工作原理,就得找到这类 Modifier 在哪被处理的。其实也在 LayoutNode.modifier
的 setter 里:
// LayoutNode
// 省略部分代码
internal val innerLayoutNodeWrapper = InnerPlaceable(this)
override var modifier: Modifier = Modifier
set(value) {
val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod: Modifier.Element, toWrap:LayoutNodeWrapper ->
// mod 是遍历到的那一个 Modifier,toWrap 是上一轮应用的结果(或初始值)
toWrap.entities.addBeforeLayoutModifier(toWrap, mod) // <--- 看这一行
// ...
}
outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
layoutDelegate.outerWrapper = outerWrapper
}
上面分析 LayoutModifier
工作原理时已经看过这段代码,toWrap
要么是包裹了 LayoutModifier
的 ModifiedLayoutNode
,要么是初始的负责组件的内部测量的 InnerPlaceable
。
DrawModifier
是在 toWrap.entities.addBeforeLayoutModifier(toWrap, mod)
中处理的,它究竟干了什么呢?也是把 Modifier 包装一层,只不过这一次用的是 LayoutNodeEntity
的子类,而是不 ModifiedLayoutNode
了。
graph TB
s[遍历 Modifier] --mod,toWrap--> jt[/判断 mod 类型/]
jt --LayoutModifier--> pl[用 ModifiedLayoutNode 包装] ---> 基于包装结果执行下一轮遍历
jt --四种 Modifier--> pe[用 LayoutNodeEntity 包装] ---> pee[把包装结果添加到 toWrap.entities 中]
jt --其他 Modifier --> ...
上图中的「四种 Modifier」包括:DrawModifier
/ PointerInputModifier
/ SemanticsModifier
/ ParentDataModifier
。
toWrap.entities
是 EntityList
,其内部的 entities
才是真正的储存结构,它是多个链表的头节点组成的数组,每一个元素是一个链表,每一个链表保存着一类 Modifier 的包装,「把包装结果添加到 toWrap.entities 中」具体来是用头插法加入到对应链表中。先添加的 Modifer 在外层,这里是用从内向外遍历,使用头插法的结果就是链表里的顺序与添加的顺序一致:先添加(左边)的 DrawModifier 在链头。示意图如下:
class EntityList {
val entities = [
0: DrawEntity(DrawModifier1) -> DrawEntity(DrawModifier2) -> ...,
1: PointerInputEntity(PointerInputModifier) -> ...,
2: SemanticsEntity(SemanticsEntity) -> ...,
3: SimpleEntity(ParentDataModifier) -> ...,
4: SimpleEntity(OnPlacedModifier) -> ...,
5: SimpleEntity(OnRemeasuredModifier) -> ...,
6: SimpleEntity(LookaheadOnPlacedModifier) -> ...,
]
}
这一段很容易理解,看不懂的需要补一下数据结构知识。
至此我们知道了所有 DrawModifier 都被包装后装进了 LayoutNodeWrapper 里保存。那什么时候具体使用呢?
调用
当然是绘制的时候调用啦。上面提到测量与布局由 LayoutNode.remeasure()
和 LayoutNode.replace()
方法负责,绘制则是由 LayoutNode.draw()
方法负责。它的实现非常简单:
// LayoutNode
internal fun draw(canvas: Canvas) = outerLayoutNodeWrapper.draw(canvas)
outerLayoutNodeWrapper
是啥前面也分析过了,忘记的同学复习一下 LayoutModifier 工作原理。简单回顾,它是最外侧的 LayoutModifier 的包装,如果没有的话就是初始的 innerLayoutNodeWrapper = InnerPlaceable(this)
。这里就看它们的公共父类中的方法源码:
// LayoutNodeWrapper
/** Draws the content of the LayoutNode */
fun draw(canvas: Canvas) {
val layer = layer
if (layer != null) {
layer.drawLayer(canvas)
} else {
val x = position.x.toFloat()
val y = position.y.toFloat()
canvas.translate(x, y)
drawContainedDrawModifiers(canvas)
canvas.translate(-x, -y)
}
}
注意:这里的 layer
与传统 View 系统中的 layer 以及传统 Canvas 中的 layer 都没有关系!
背景知识
Android 上任何 UI 框架都逃不过原生 View 系统,Compose 也不例外,也只是对 View 的封装。具体来讲,Compose 在 Android 中以 ComposeView 的形式存在,它是传统 View 的子类。从这个角度,Compose 干的事情就是在一个 View 中绘制出 Compose 的组件树,并自行处理不同组件的触摸。
因为所有组件都在一个 View 中绘制,就难以通过原生 View API 实现透明度、裁剪等功能,否则会影响整个 Compose 而不是某个组件。为了解决这个问题,额外的抽象层 layer 应运而生。
既然 layer 是个抽象层,在不同系统的具体实现自然也不一样。在 Android 中,>= API 29 时使用 RenderNode 实现,< API 29 时尝试使用 RenderNode,失败则回退使用独立的 View 实现。
Compose 中的 layer 有两个作用:
- 类似
View.setLayerType()
,可以独立绘制与刷新,某些情况下(比如动画)可以提高性能。 - 分层隔离。类似 Photoshop 中的层。
从读源码的角度,layer 不影响绘制逻辑,只影响往哪里绘制,可以暂时忽略它。那我们就不找麻烦了,通过没有 layer 的情况来研究 DrawModifier 的使用。显然,核心方法是 drawContainedDrawModifiers(canvas)
:
// LayoutNodeWrapper
private fun drawContainedDrawModifiers(canvas: Canvas) {
// 别忘了 entities 的链表数组
// 那么 head 就是 DrawEntity 这个链表的第一个元素,也是最后添加的元素(别忘了是头插法添加的)
val head = entities.head(EntityList.DrawEntityType)
if (head == null) {
// 没有设置任何 DrawEntity
performDraw(canvas)
} else {
// 从最后一个设置的 DrawEntity 开始绘制
head.draw(canvas)
}
}
==先看没有 DrawModifier 的情况:==
// LayoutNodeWrapper
open fun performDraw(canvas: Canvas) {
wrapped?.draw(canvas)
}
很直接。wrapped
是啥?研究 LayoutModifier
的时候也说过了。当具体实现是 ModifiedLayoutNode
时它就是内层的 LayoutNodeWrapper
。最内层肯定是 InnerPlaceable
,它的 wrapped
是 null。
等等,InnerPlaceable 的 wrapped 是 null,那组件原本的内容谁来绘制? 其实每一个组件函数内部都是创建 DrawModifier 来绘制的。所以即使我们没有手动添加,它也已经有一个或多个 DrawModifier 了。由此可见,DrawModifier
是 Compose 绘制的核心,就像 View 中的 onDraw()
方法一样。
==如果有 DrawModifier 呢?==
从上面源码可以看到它取出第一个(也是最先添加的 DrawModifier)DrawEntity 调用 draw()
方法,看下源码:
// DrawEntity
fun draw(canvas: Canvas) {
val size = size.toSize()
// ...
val drawScope = layoutNode.mDrawScope
drawScope.draw(canvas, size, layoutNodeWrapper, this) {
with(drawScope) {
with(modifier) {
draw() // <--------- 终于!!最终调用 DrawModifier.draw() 方法
}
}
}
}
还差一个问题,这里似乎没有链式调用呀,那剩余的 DrawModifier 怎么办? 这就要求开发者需要在 DrawModifier 里手动调用 drawContent()
方法,如果不调用的话,那确实,后面的 DrawModifier 就失效了。看一下 drawContent()
的实现:
// LayoutNodeDrawScope
override fun drawContent() {
drawIntoCanvas { canvas ->
val drawEntity = drawEntity!!
val nextDrawEntity = drawEntity.next
if (nextDrawEntity != null) {
nextDrawEntity.draw(canvas)
} else {
drawEntity.layoutNodeWrapper.performDraw(canvas)
}
}
}
很明显,继续调用链表中的下一个 DrawModifier 了吧。如果没有下一个则调用内层的 LayoutNodeWrapper
绘制, performDraw()
的源码刚刚已经看过了。
总结:
- Compose 从最外层的 LayoutNodeWrapper 开始绘制。
- 如果这一层没有 DrawModifier 则深入内层,直到 InnerPlaceable 结束。
- 若中间任意一层有 DrawModifier,则调用它(这个应该是当前层中最先添加的 DrawModifier)。在 DrawModifier 内部开发者应该调用
drawContent()
触发后面的绘制。 drawContent()
可能触发两种调用:- 若链上还有 DrawModifier 则调用它。
- 若没有则触发内层的 LayoutNodeWrapper。
小测验
Box(modifier = Modifier.requiredSize(80.dp).background(Color.Green).requiredSize(40.dp))
绿色区域有多大?
绘制区域的大小取决于 DrawModifier 所属的
LayoutNodeWrapper
。处理 Modifier 时从内向外遍历,先添加的(左边)是外侧,后添加的(右边)是内侧。所以 backgraound 属于requiredSize(40.dp)
对应的 LayoutNodeWrapper,故绿色大小为 40dp。
再看一个例子
Box(modifier = Modifier.size(40.dp).padding(8.dp).background(Color.Green))
绿色区域大小是 24dp,看起来似乎是由它左边的 Modifier 决定的?
非也!左边的 Modifier 只决定了对右侧的传入限制,background 的大小还是由右侧的测量结果决定。右侧...有东西吗?有的!就是最内层的
InnerPlaceable
呀。搞不清这个,遇到第一题的情况就不会咯。
Modifier.drawWithContent()
DrawWithContentModifier
是 DrawModifier
最简单的实现,它等价于这么写:
// 与 Modifier.drawWithContent{} 等价
Modifier.then(object :DrawModifier{
override fun ContentDrawScope.draw() {
TODO("Not yet implemented")
}
})
注意,这个 Modifier 不是在组件的基础上额外绘制其他东西,而是直接控制整个组件的绘制。 看过了原理关于这一点应该是水到渠成的,Compose 根本没有所谓的「组件的绘制」,一切绘制都是基于 Modifier 的,包括组件本身。通常在其中需要调用一下 drawContent()
方法。
PointerInputModifier
了解了布局与绘制,现在轮到事件输入了。
与 View 类比
暂且忽略键盘、鼠标等,单单是触摸也已经是很大的话题了。在传统 View 中对事件的处理大概有三个等级:
- 最简单的是注册监听器。例如
setOnClickListener()
。 - 稍微复杂一点的可以重写事件方法,调用手势检测器
GestureDetectorCompat
。 - 最复杂的就是重写事件方法,完全自己处理。
在 Compose 中也有这种区分。
-
最简单的是使用顶层的 Modifier:
Box(modifier = Modifier.clickable { }) // 或可以监听多种手势的 Box(modifier = Modifier.combinedClickable(onDoubleClick = {}, onLongClick = {}, onClick = {}))
-
复杂的使用底层 Modifier,但在内部可以使用手势检测函数:
Box(modifier = Modifier.pointerInput(Unit) { detectTapGestures(onTap = {}, onPress = {}, onDoubleTap = {}, onLongPress = {}) })
不难发现
detectTapGestures()
与Modifier.combinedClickable()
有重复。区别在于前者更底层,比如后者内部处理了onPress
来显示触摸反馈,没有把这个事件暴露出来。 -
更复杂的还是使用这个 Modifier,但用的是更底层的函数:
Box(modifier = Modifier.pointerInput(Unit) { forEachGesture { awaitPointerEventScope { /*监听处理最原始的事件*/ } } })
工作原理
再一次回到 LayoutNode.modifier
的 setter,PointerInputModifier
与 DrawModifier
一起在 toWrap.entities.addBeforeLayoutModifier(toWrap, mod)
中被处理,包装成 PointerInputEntity
后添加到链表中。这里就不重复分析了,忘记的去看看 「DrawModifier 工作原理 - 处理」章节。
由此可以猜到,PointerInputModifier
针对的是它右边最近的 LayoutModifier
或组件本身。如果一层中有多个 PointerInputModifier
那左边的先被调用。 知道这个结论以及原因其实就够了。如果有兴趣,我们一起再接着研究具体的调用。
这次的调用入口在 LayoutNode.hitTest()
,它负责检测这次事件应该分发到哪些组件上。依然和之前一样,它只简单地调用最外层 outerLayoutNodeWrapper
的同名方法。这个方法的核心是取链表头节点(也是最左边的 PointerInputModifier 的包装)调用 LayoutNodeWrapper.hit()
方法:
// LayoutNodeWrapper
private fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> T?.hit(
hitTestSource: HitTestSource<T, C, M>,
pointerPosition: Offset,
hitTestResult: HitTestResult<C>,
isTouchEvent: Boolean,
isInLayer: Boolean
) {
if (this == null) {
hitTestChild(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
} else {
hitTestResult.hit(hitTestSource.contentFrom(this), isInLayer) {
next.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
}
}
}
它的核心在 12 行,这次调用的其中一个参数来自 hitTestSource.contentFrom(this)
,这是一个接口,并且只有匿名实现类,AS 中不能直接跳转。其中一个实现在 LayoutNodeWrapper
的 1205 行左右:
val PointerInputSource =
object : HitTestSource<PointerInputEntity, PointerInputFilter, PointerInputModifier> {
override fun contentFrom(entity: PointerInputEntity) = entity.modifier.pointerInputFilter
// ...
}
看看 contentFrom()
的实现,不就是返回 PointerInputEntity
包裹的那个 Modifier 实例么,也就是我们写的 Modifier.pointerInput(Unit)
。为啥要那么麻烦?主要因为 PointerInputModifier
有两个实现,其中一个用于和 View 交互,另一个才是 Compose 内部使用的,这么写是为了适配两种情况。
弄清了这个参数的来源,继续追踪 hitTestResult.hit()
到 HitTestResult.hitInMinimumTouchTarget()
:
// HitTestResult
fun hitInMinimumTouchTarget(
node: T, // 这个就是 PointerInputModifier
distanceFromEdge: Float,
isInLayer: Boolean,
childHitTest: () -> Unit
) {
val startDepth = hitDepth
hitDepth++
ensureContainerSize()
values[hitDepth] = node // <--------------- 顺序把 Modifier 加入数组
distanceFromEdgeAndInLayer[hitDepth] = DistanceAndInLayer(distanceFromEdge, isInLayer).packedValue
resizeToHitDepth()
childHitTest() // <---------------- 调用下一个节点
hitDepth = startDepth
}
它做了两个事:
- 顺序把 Modifier 放进一个数组
- 调用
childHitTest
lambda 表达式。它从LayoutNodeWrapper.hit()
传递过来,具体代码是{ next.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)}
可以想象,整体来讲这部分是把链表的元素一个个提取出 Modifier 放进数组保存。所以数组中 Modifier 的顺序与我们写的代码顺序一样,从左到右,从先应用的到后应用的。
其实在放进数组之前 Compose 做了判断,只有本次事件应该触达的那些组件的 PointerInputModifer 会被放进去。那么这个数组就是需要处理本次事件的 Modifier,后面依次调用下就行了。不过这个调用不再是 LayoutNode
的工作,这里也不深究了。我们目前研究的核心是搞清楚不同 Modifier 的处理顺序与相互影响,不能无限展开。
ParentDataModifier
ParentDataModifier
用于辅助测量布局。从名字可以略窥一二,这类 Modifier 主要是给父组件看的,而不是所属的那个组件。weight
就用到了此接口。
在传统 View 系统中也采用了相同的方案。所有
layout_
开头的布局属性都由父 View 处理。
想象一下用 LayoutModifier
来实现 weight
功能。Modifier 内部只能获取 1. 父组件给自己的限制;2. 原始测量结果。但是 weight
还需要综合其他子组件的权重才能计算自己的大小,显然,实现不了。这就是 ParentDataModifier
的用武之地。
自定义使用
由于 ParentDataModifier
需要父组件配合才能生效,所以是个完整可用的 ParentDataModifier 需要编写至少两部分代码:
- 一个 Modifier 扩展函数,便于用户创建我们的 ParentDataModifier。
- 自定义组件,并解析我们的 ParentDataModifier。
其中自定义组件是另一个大话题,这里仅作演示,不深入。假如要自己实现一个 weight,可以这么写:
// 扩展函数,方便用户创建 ParentDataModifier
fun Modifier.myWeight(weight: Float) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
return weight
}
})
// 自定义布局,省略了真正的布局代码
@Composable
fun MyLayout(modifier: Modifier, content: @Composable () -> Unit) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
measurables.forEach {
val data = it.parentData as? Float
// 利用这个 data 去测量布局
}
layout(100, 100) { /*...*/ }
}
}
在自定义布局中,measurables
参数是所有子组件对应的 Measurable
,通过它的 parentData
属性可以拿到附加的 ParentDataModifier
提供的值(如果有)。这个值可能是任何类型,根据需要判断与强转。
如果多次附加 ParentDataModifier 呢? 仔细看看 ParentDataModifier
的实现类,方法接受了一个参数,这就是右边的 ParentDataModifier 所提供的数据,它的返回值会提供给左边的 ParentDataModifier
或作用到组件上。如果需要的话可以融合多个值,不过更多情况下只需要最新值就行了。
那如果需要多种数据咋办? 这种需要就得写一个类承载多个数据然后做融合了。比如这样:
class MyLayoutData(var weight: Float = 0f, var expended: Boolean = false)
fun Modifier.myWeight(weight: Float) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any =
((parentData as? MyLayoutData) ?: MyLayoutData()).also { it.weight = weight }
})
fun Modifier.expended(expended: Boolean) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any =
((parentData as? MyLayoutData) ?: MyLayoutData()).also { it.expended = expended }
})
这样写有个巨大问题:API 污染。 现在 Modifier.expended()
在任何地方都可以调用,但显然只有我们自己的 MyLayout
组件才能识别。不仅开发者会受到 IDE 提示的打扰,也有可能影响其他组件的工作。为此,应该创建一个作用域接口来限制这些方法的使用。
@LayoutScopeMarker // 禁止在间接作用域里调用(Kotlin 语法允许在间接作用域里调用)
@Immutable
object MyLayoutScope {
fun Modifier.myWeight(weight: Float) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any =
((parentData as? MyLayoutData) ?: MyLayoutData()).also { it.weight = weight }
})
fun Modifier.expended(expended: Boolean) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any =
((parentData as? MyLayoutData) ?: MyLayoutData()).also { it.expended = expended }
})
}
虽然创建接口,再用 object
创建一个单例是标准写法(看看 Row
怎么写的),但是太麻烦了,作为例子我们直接创建了一个单例。然后为我们的自定义 Layout 提供作用域:
@Composable
fun MyLayout(modifier: Modifier, content: @Composable MyLayoutScope.() -> Unit) {
Layout(content = { MyLayoutScope.content() }, modifier = modifier) { measurables, constraints ->
measurables.forEach {
val data = it.parentData as? Float
// 利用这个 data 去测量布局
}
layout(100, 100) { /*...*/ }
}
}
工作原理
ParentDataModifier
的处理与存储方式与 PointerInputModifier
, DrawModifier
都一样,最后被存在一个链表中。
烦了,不说了,自己去看之前的分析 (メ`[]´)/
直接看怎么被调用的。不是我们自己调用的吗?那就跟踪 IntrinsicMeasurable.parentData
的实现,显然,应该看 LayoutNodeWrapper
这一个:
// LayoutNodeWrapper
override val parentData: Any?
get() = entities.head(EntityList.ParentDataEntityType).parentData
private val SimpleEntity<ParentDataModifier>?.parentData: Any?
get() = if (this == null) {
wrapped?.parentData
} else {
with(modifier) {
/**
* ParentData provided through the parentData node will override the data provided
* through a modifier.
*/
modifyParentData(next.parentData)
}
}
注意,这次拿到的 head
是 SimpleEntity
,它没有 parentData
属性,而是 LayoutNodeWrapper
中给它添加了一个扩展属性。思路依然和之前类似:如果当前层(LayoutNodeWrapper)没有关联的包装有 ParentDataModifier
的 SimpleEntity
,就进入内层,否则先递归执行右边的 ParentDataModifier 或进入内层,拿到结果作为参数再执行这一个。
虽然储存结构上 ParentDataModifier
也是绑定在它右边最近的 LayoutModifier
或组件本身上,但它不直接操作组件。所以到底和谁绑定不影响父组件读取它,自然也不影响显示效果。
从技术上,组件自己的 LayoutModifier 也可以读取到 parentData 的数据,但没有什么用。
SemanticsModifier
SemanticsModifier
用于构建 Semantics Tree,这是啥?
我们知道经过组合,Compose 会生成 UI 树,其中每个组件是一个节点。如果把这个树精简一下,删除/合并一些节点只留下有意义的东西,得到的就是 Semantics Tree 语义树。所谓「有意义」指可以供用户查看/操作的独立组件,例如图片,而一个单纯用于布局 Row 在这个语境下就没有意义。
Semantics Tree 主要用于:1. 无障碍功能。2. 开发者测试(用于查找组件,类似与 View 的 id)。
使用
一个简单的组件:
Box(modifier = Modifier.width(80.dp).background(Color.Green))
在 Talk Back 开启的情况下用户触摸这个组件系统不会有任何反映。因为默认情况下 Compose 认为它是无意义的。但若这个组件偏偏有意义怎么办?比如我就是想用一个红色的方框代表已禁用(虽然很难看),现在视觉障碍用户完全不知道这一含义。此时可以使用 semantics
来设置一个 contentDescription:
Box(modifier = Modifier.width(80.dp).background(Color.Green).semantics {
contentDescription = "选项已禁用"
})
这么设置之后用户再触摸这个区域,系统就会读出这句描述。
插一句题外话,大陆对残障人士太不友好了,断掉的盲道,45度的轮椅坡,拥挤的公交车...当然,我们有很多客观理由,比如还是发展中国家,人口太多等等等。
但作为一名开发者,尤其当你是一名没有被资本绑架的个人开发者时,希望能够多考虑一下无障碍支持,而不是写一个没意义的字符串来抑制 warn。相比传统行业,我相信技术圈还是有不少人,依然心怀理想。
再看一个例子:
Button(onClick = { /*TODO*/ }) {
Text(text = "打开新窗口")
}
如果点击 Text 是读文本呢还是读按钮呢?其实无论点 Text 还是点按钮的其他部分,系统都会一起读。因为在 Semantics Tree 中这两个组件被合并为一个节点了。大部分需要合并的场景 Compose 已经做了自动合并,个别没有识别的场景可以通过 Modifier.semantics(mergeDescendants = true)
指明需要合并子组件,并且防止自己被父组件合并。
还有一个 Modifier.clearAndSetSemantics{ }
函数,正如起名,它强制清除内部组件的无障碍信息。
工作原理
SemanticsModifier
又又又是和 DrawModifier
一样的储存与处理方式。那个链表数组,第四遍了,大家应该能记得了吧。
现在只看具体调用。其实具体调用也不用看了,SemanticsModifier
与 PointerInputModifier
共用一套机制,都需要根据触摸点去找组件。入口方法在 LayoutNode.hitTestSemantics()
OnRemeasuredModifier
OnRemeasuredModifier
的调用时机相当于 View 系统里的 onMeasure
写法。
使用
在传统的 View 中我们覆盖 View.onMeasure()
一般有两种需求:
- 获取到 View 的尺寸。
- 干预测量。
在 Compose 中,这两种用法被分开了,代码更清晰。干预测量应该用前面说的 LayoutModifier
。
获取组件尺寸:
Box(modifier = Modifier.onSizeChanged { intSize -> }
它创建的是 OnSizeChangedModifier
,做了一些优化,只有在尺寸改变的时候才调用回调,省得我们自己额外判断了。
其实这种 Modifier 更多情况下是与其他的一起使用。例如 Modifier.scrollable()
内部也实现了这个接口,因为它需要获取子组件的大小才好安排滚动。
工作原理
OnRemeasuredModifier
的处理与前面 DrarModifier
之类有一点点不同,不过还是在 LayoutNode.modifier
的 setter 里:
// LayoutNode
// 省略部分代码
internal val innerLayoutNodeWrapper = InnerPlaceable(this)
override var modifier: Modifier = Modifier
set(value) {
val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod: Modifier.Element, toWrap:LayoutNodeWrapper ->
// mod 是遍历到的那一个 Modifier,toWrap 是上一轮应用的结果(或初始值)
toWrap.entities.addBeforeLayoutModifier(toWrap, mod)
// ... 处理 LayoutModifier
wrapper.entities.addAfterLayoutModifier(wrapper, mod) // <-----------!!!
}
outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
layoutDelegate.outerWrapper = outerWrapper
}
OnRemeasuredModifier
, OnPlacedModifier
, LookaheadOnPlacedModifier
这三类在 addAfterLayoutModifier()
中处理。这和 AddBefore 有什么区别?
回顾一下,这俩函数是把 Modifier 添加到链表中,这个链表保存在当前的 ModifiedLayoutNode (LayoutModifier 包装类) 中。AddBefore 与 AddAfter 只有在一个对象同时实现了 LayoutModifier 和其他类 Modifier 时才有区别。对于前者的处理,其他类的 Modifier 会附加到右边最近的 ModifiedLayoutNode 中;对于后者的处理会附加到自己的 ModifiedLayoutNode 上。
在新版的代码中有一些变动。LayoutModifier 不再包装后存到某层(ModifiedLayoutNode)的链表数组中,而是直接保存到
LayoutNode.nodes
里,它是一个 NodeChain,是Modifier.Node
的双链表,不区分层。但是分层关系还是有的。
Modifier.Node
是新的 Modifier 的包装,并添加一些附加信息,比如属于哪个NodeCoordinator
。NodeCoordinator
相当于原来的ModifiedLayoutNode
,是LayoutModifier
专属包装。
至于具体的调用,我们利用 IDE 轻松找到来源于 LayoutNodeWrapper.onMeasured()
,继续寻找,可以找到两个上游调用:
InnerPlaceable.onMeasured()
这是测量组件本身尺寸的ModifiedLayoutNode.onMeasured()
这是 LayoutModifier 的测量
逻辑比较清晰:测量完成就调用属于这一层的 OnRemeasuredModifier 回调,如果有多个 LayoutModifier 就会多次触发回调。
比如这么写:
Modifier.padding(8.dp).onSizeChanged { }.padding(10.dp)
onSizeChanged
在 10dp 的那个 padding 测量完成之后调用,拿到的也是它的尺寸。
OnPlacedModifier
OnPlacedModifier
与 OnRemeasuredModifier
有点类似,都是添加一个回调,只是这一个是「被摆放」回调。它们俩触发时机分别像 View.onLayout()
与 View.onMeasure()
。
使用
View.onLayout()
和 View.onMeasure()
一样有两个作用:
- 能够获得自己尺寸与位置。
- 需要布局子 View(调用子 View 的
layout()
方法)。
Compose 中这两功能也被分开了,OnPlacedModifier
只用于获得自身位置尺寸的需求。
既然类似,这个有和 OnRemeasuredModifier
有啥区别?
OnPlacedModifier |
OnRemeasuredModifier |
|
---|---|---|
调用时机 | 布局流程结束 | 测量完毕,依然在布局流程中 |
回调数据 | 尺寸、位置等完整布局信息 | 尺寸信息 |
工作原理
OnPlacedModifier
的处理与存储与 OnRemeasuredModifier
一模一样,不多说了。
利用 IDE 寻找调用方,也是找到了两个:
-
LayoutNode.onLayoutComplete()
。从源码明显看出是InnerPlaceable
布局完成后触发的。// LayoutNode override fun onLayoutComplete() { // innerLayoutNodeWrapper 就是 InnerPlaceable innerLayoutNodeWrapper.entities.forEach(EntityList.OnPlacedEntityType) { it.modifier.onPlaced(innerLayoutNodeWrapper) } }
-
LayoutNodeWrapper.onPlaced()
。显然这一个就是 LayoutNodeModifier 所生成的包装类触发的。内部遍历这一层所有的 OnPlacedModifier 依次调用它们。
事实上 OnPlacedModifier 回调中得到的
LayoutCoordinates
就是ModifiedLayoutNode
,它实现了接口。
LookaheadOnPlacedModifier
这是一个 internel 类,虽然名字相似但没有实现 OnPlacedModifier
。
Lookahead 就是往前看看。用编译器举个例子,扫描下面字符串时:
for (int i = 0; i<10; i++) ^
当扫描到
for
单词的末尾,编译器无法判定这是一个循环开始普通变量(比如forecast
),需要先往下看看,才能确定已经扫描完的这部分是什么。
Compsoe 借用了这个概念来满足 LookaheadLayout
的需要。LookaheadLayout
会进行两次测量,第一次是预先测量(与摆放)把结果存起来,根据预先测量的结果按需添加 Modifier 再进行真正的测量。主要用于过渡动画,这里不详细研究了。
回到 LookaheadOnPlacedModifier
,它也是一个「被摆放」的回调,区别是除了真正的数据外,还额外提供了预先测量的数据。只能在 LookaheadLayout
的子组件身上使用,也是 Modifier.onPlaced{}
函数,但是 lambda 表达式接受两个参数。
LookaheadOnPlacedModifier
的调用时机与普通的 OnPlacedModifier
一样,不受「预先测量」这种行为影响。只不过被调用的时候把保存的预先测量的结果也传过去了。
OnGloballyPositionedModifier
又是一个回调 Modifier,并且回调的参数与 OnPlacedModifier
一样?!
那是因为 OnGloballyPositionedModifier
也是回调尺寸与位置,回调的是它所属的那一层(ModifierLayout / ModifiedLayoutNode)所负责的区域位置/尺寸。
这...听起来似乎和 OnPlacedModifier
没区别啊?区别在于回调时机:
-
OnPlacedModifier
回调时机是所属的那一层测量完成之后,外层摆放它时。这就意味着它内层还没有被摆放,可以做一些处理来影响内层摆放。 比如这样:var offsetX by remember { mutableStateOf(0) } var offsetY by remember { mutableStateOf(0) } Modifier .onPlaced { offsetX = (it.positionInParent().x / 2).toInt() offsetY = (it.positionInParent().y / 2).toInt() } .offset { IntOffset(offsetX, offsetY) }
offset
的回调发生在onPlaced
之后,可以受到影响。 -
OnGloballyPositionedModifier
回调对于位置来讲,只有相对于 Windows 位置改变的时候才触发。 比如聊天列表界面是可以滑动的,每一项都有一个头像。上下滑动时头像的位置相对于父组件没有移动,所以OnPlacedModifier
不触发。但是相对于 Windows 是改的,所以OnGloballyPositionedModifier
触发。这就是「Globally」的含义。
那有没有什么情况可以触发
OnPlacedModifier
但不触发OnGloballyPositionedModifier
呢?理论上有的。比如父组件往左移动 100px,子组件往右移动 100px,此时子组件相对 Windows 没有移动。但事实上,
OnGloballyPositionedModifier
并没有进行前后位置比较,而是只要它位置变了,就回调。 Compose 故意这么实现的。
显然,OnGloballyPositionedModifier
回调的次数更多,所以尽量使用轻量级的 OnPlacedModifier
。
ModifierLocal
说到 Local,应该可以联想到另外两个:CompositionLocal
, ThreadLocal
, 他俩表示这个数据在某个范围内可共享,ModifierLocal
一样。
ModifierLocal
在 SDK / 公共库中比较常用,App 开发用的不多。
使用
与 CompositionLocal 类似,ModifierLocal 的使用分为三步:
- 创建一个对象。
- 提供值。
- 取用值。
// 创建一个 ModifierLocal 对象
val sharedData = modifierLocalOf { "NA" }
Modifier
// 提供值
.modifierLocalProvider(sharedData){ "New Dara" }
// 取值
.modifierLocalConsumer {
val data = sharedData.current
}
}
这...看起来它们在独立的 Modifier 中,怎么才能给现有的 Modifier 提供值呢?这就麻烦了,我们得舍弃便捷函数,手动创建 Modifier 并让它实现 ModifierLocalProvider
或 ModifierLocalConsumer
接口。
例如 Modifier.layout{}
如果想读取共享数据,得这么写:
Modifier.then(object : LayoutModifier, ModifierLocalConsumer {
lateinit var data: String
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
// 这里可以放心使用 data
val p = measurable.measure(constraints)
return layout(p.width, p.height) {
p.placeRelative(0, 0)
}
}
override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
with(scope) {
data = sharedData.current // 接收共享数据
}
}
})
onModifierLocalsUpdated()
在测量之前被调用,所以 LayoutModifier 中可以放心地使用共享数据。
也可以同时实现 Prodiver 与 Consumer 接口,用于接收上游数据,处理之后再继续往下游传递。如果同时实现,Compose 可以保证 onModifierLocalsUpdated()
先被调用,然后才是 Provider 接口的提供数据的方法。