1、addBeforeLayoutModifier() 与 addAfterLayoutModifier()
addBeforeLayoutModifier() 与 addAfterLayoutModifier() 并不是指在 Modifier 链上先处理哪个或者后处理哪个具体的 Modifier,而是指同一个 Modifier 具有多重“身份”时,先/后处理哪个“身份”。具体点说,就是因为一个 Modifier 可以实现多个 Modifier 接口,比如既可以实现 DrawModifier,也可以实现 OnRemeasuredModifier,此时将该 Modifier 视为有多重身份,那么它作为 DrawModifier 时,就会在 addBeforeLayoutModifier() 内先于 LayoutModifier 被处理,作为 OnRemeasuredModifier 时,就在 addAfterLayoutModifier() 内后于 LayoutModifier 被处理。
下面来详细看这段代码:
override var modifier: Modifier = Modifierset(value) {...// Create a new chain of LayoutNodeWrappers, reusing existing ones from wrappers// when possible.val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->...// 先添加到链表中的 Modifier toWrap.entities.addBeforeLayoutModifier(toWrap, mod)...val wrapper = if (mod is LayoutModifier) {// Re-use the layoutNodeWrapper if possible.(reuseLayoutNodeWrapper(toWrap, mod)?: ModifiedLayoutNode(toWrap, mod)).apply {onInitialize()updateLookaheadScope(mLookaheadScope)}} else {toWrap}// 后添加到链表中的 Modifier wrapper.entities.addAfterLayoutModifier(wrapper, mod)wrapper}setModifierLocals(value)outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapperlayoutDelegate.outerWrapper = outerWrapper...}
通过 addBeforeLayoutModifier() 被添加的 mod 是添加到 toWrap 中,经过 LayoutModifier 的处理时,toWrap 与 mod 被包装到一个 ModifiedLayoutNode 类型的变量 wrapper 中,后面调用 addAfterLayoutModifier() 添加 mod 时,是添加到 wrapper 中。以 LayoutModifier 为基准视角的话,先添加的 Modifier 是被添加到表示它的 ModifiedLayoutNode 的内层的 LayoutNodeWrapper 中,而后添加的 Modifier 是被添加到 ModifiedLayoutNode 本身的链表中:
ModifiedLayoutNode(entities = [后添加的 Modifier 的链表数组],modifier = LayoutModifier,// 这个 LayoutNodeWrapper 的实际类型要根据 Modifier 链的具体情况,// 它可能是最内层的 InnerPlaceable,也可能还是一个 ModifiedLayoutNodewrapped = LayoutNodeWrapper(entities = [先添加的 Modifier 的链表数组])
)
在当前版本为止,唯一一个实现了多个 Modifier 的是 PainterModifier,它实现了 LayoutModifier 与 DrawModifier:
private class PainterModifier(val painter: Painter,val sizeToIntrinsics: Boolean,val alignment: Alignment = Alignment.Center,val contentScale: ContentScale = ContentScale.Inside,val alpha: Float = DefaultAlpha,val colorFilter: ColorFilter? = null,inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, DrawModifier, InspectorValueInfo(inspectorInfo)
根据上面的处理原则,DrawModifier 要放在 LayoutModifier 所形成的 ModifiedLayoutNode 的内层节点上:
ModifiedLayoutNode(entities = [],modifier = LayoutModifier,// 这个 LayoutNodeWrapper 的实际类型要根据 Modifier 链的具体情况,// 它可能是最内层的 InnerPlaceable,也可能还是一个 ModifiedLayoutNodewrapped = LayoutNodeWrapper(entities = [DrawModifier])
)
这种设计在 Compose 的 1.3.0-beta1 版本中被修改了,不再区分同一个 Modifier 的多个身份了,都当成一个变量存储,改成了 LayoutModifier 与 DrawModifier 都在 ModifiedLayoutNode 的同一层了。
从这一节开始,课程使用的就是 1.3.0-beta1 的源码了。
2、OnRemeasuredModifier
2.1 基本使用
作用类似于 View 体系下的 onMeasure(),OnRemeasuredModifier 在它修饰的组件,或者说它修饰的 LayoutModifier 在测量完成时会回调其内部的唯一函数 onRemeasured() 通知测量已经完成:
/**
* A modifier whose onRemeasured is called when the layout content is remeasured.
* The most common usage is onSizeChanged.
*/
@JvmDefaultWithCompatibility
interface OnRemeasuredModifier : Modifier.Element {/*** Called after a layout's contents have been remeasured.*/fun onRemeasured(size: IntSize)
}
Compose 提供的 OnRemeasuredModifier 的唯一实现类是 OnSizeChangedModifier,它相对于 OnRemeasuredModifier 的功能做出了一些优化,并不会在每一次组件测量完成时都进行回调,只会在测量完成后,尺寸发生变化的情况下回调。使用方法是调用 Modifier.onSizeChanged():
@Stable
fun Modifier.onSizeChanged(onSizeChanged: (IntSize) -> Unit
) = this.then(OnSizeChangedModifier(onSizeChanged = onSizeChanged,inspectorInfo = debugInspectorInfo {name = "onSizeChanged"properties["onSizeChanged"] = onSizeChanged})
)
OnSizeChangedModifier 实现 onRemeasured() 时,只有在新测量的 size 与测量前的 previousSize 不同时,才回调参数上的 onSizeChanged:
private class OnSizeChangedModifier(val onSizeChanged: (IntSize) -> Unit,inspectorInfo: InspectorInfo.() -> Unit
) : OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {private var previousSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)override fun onRemeasured(size: IntSize) {if (previousSize != size) {onSizeChanged(size)previousSize = size}}
}
假如你就是想在每次测量完成后都回调,那么就使用匿名对象的方式实现 OnRemeasuredModifier 即可:
Modifier.then(object : OnRemeasuredModifier {override fun onRemeasured(size: IntSize) {TODO("Not yet implemented")}})
2.2 原理
课程是把当时较新的 1.3.0-beta1 版本的源码拿出来讲了,在 LayoutNode 的 modifier 属性的 set() 中,添加各个 Modifier 到存储结构中的代码完全重构了,改进了一些细节,但大的逻辑没变。暂时先不跟进新版本的流程,用此前一直使用的 1.3.0-alpha1 来讲解原理,看看课程后续是用新版本还是老版本再决定要不要把新版本的流程更新到笔记中。
OnRemeasuredModifier 接口的 onRemeasured() 会在 LayoutNodeWrapper 进行测量时被调用。这个测量过程也是实现的接口函数:
/*** 可以被测量的组合体(composition)的一部分,它表示一个布局(layout),其实例不应被存储*/
interface Measurable : IntrinsicMeasurable {/*** 使用 [constraints] 进行测量,返回一个拥有新尺寸的 [Placeable] 布局,一个 [Measurable]* 在布局过程中只能被测量一次。*/fun measure(constraints: Constraints): Placeable
}
LayoutNodeWrapper 实现了该接口,就具备了对布局进行测量的功能:
internal abstract class LayoutNodeWrapper(internal val layoutNode: LayoutNode
) : LookaheadCapablePlaceable(), Measurable, LayoutCoordinates, OwnerScope,(Canvas) -> Unit {
}
由于 LayoutNodeWrapper 是个抽象类,并没有直接实现 Measurable,而是交给了它的两个子类 InnerPlaceable 与 ModifiedLayoutNode,所以我们来看子类的实现:
// InnerPlaceable.kt:override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {// before rerunning the user's measure block reset previous measuredByParent for childrenlayoutNode.forEachChild {it.measuredByParent = LayoutNode.UsageByParent.NotUsed}measureResult = with(layoutNode.measurePolicy) {layoutNode.measureScope.measure(layoutNode.childMeasurables, constraints)}onMeasured()return this}// ModifiedLayoutNode.kt:override fun measure(constraints: Constraints): Placeable {performingMeasure(constraints) {with(modifier) {measureResult = measureScope.measure(wrapped, constraints)this@ModifiedLayoutNode}}onMeasured()return this}
两个子类都是在测量完成,得到测量结果 measureResult 后,调用了 onMeasured(),该函数由父类 LayoutNodeWrapper 实现:
fun onMeasured() {// 如果当前 LayoutNodeWrapper 的 entities 数组中 RemeasureEntity 的链表不为空if (entities.has(EntityList.RemeasureEntityType)) {Snapshot.withoutReadObservation {// 调用 RemeasureEntity 链表上每一个 RemeasureEntity 内封装的// OnRemeasuredModifier 的 onRemeasured() 函数entities.forEach(EntityList.RemeasureEntityType) {it.modifier.onRemeasured(measuredSize)}}}}
注释中已经写明,onMeasured() 就是执行 RemeasureEntity 链表上所有 OnRemeasuredModifier 的 onRemeasured(),参数上的 measuredSize 来自于测量结果的 Placeable 的测量尺寸。
结合具体例子来说:
Modifier.padding(20.dp).then(object : OnRemeasuredModifier {override fun onRemeasured(size: IntSize) {TODO("Not yet implemented")}}).padding(40.dp)
在最右侧的 padding(40.dp) 所隐含的 LayoutModifier 在视图树中所形成的 ModifiedLayoutNode 完成测量后,调用 onMeasured() 时,会执行该 ModifiedLayoutNode 的 RemeasureEntity 链表上的 OnRemeasuredModifier 的 onRemeasured(),then() 内的匿名 OnRemeasuredModifier 对象刚好在适用范围内,因此 onRemeasured() 上的参数 size 是其右侧的 padding(40.dp) 测量到的尺寸。该匿名对象与其左侧的 padding(20.dp) 没有关联。
3、OnPlacedModifier
传统的 View 体系下,View 中有两个回调函数 onMeasure() 和 onLayout(),它们分别在测量和布局时回调。前面提到过,重写 onMeasure() 有两个作用:获得测量出来的尺寸、定制自身测量过程,而重写 onLayout() 可以获取 View 的最终尺寸以及它在父 View 中的相对位置,并且对于 ViewGroup 来说,还可以通过调用每个子 View 的 layout() 摆放自己内部的子 View。
与这两个函数相对应的,Compose 中有两个 Modifier,分别是 OnRemeasuredModifier 与 OnPlacedModifier,这两个 Modifier 只是在测量完成/布局完成时做一个通知回调。
本节我们来介绍 OnPlacedModifier。
3.1 作用
3.2 写法
3.3 原理
4、LookaheadOnPlacedModifier
LookaheadOnPlacedModifier 相比于 OnPlacedModifier 会多提供 Lookahead 的信息,look ahead 可以译为“预测未来,计划未来”,总之是要表达一种前瞻以及未雨绸缪的意思。比如你要做一个编译器,在读代码的时候可能会遇到一个 “for”,但是你不能马上就认为代码要进入一个 for 循环,而是应该再向前多看几个字符,它也有可能是变量名 “forest”,这个向前看的动作就是 look ahead。
look ahead 以往是常用于算法领域中的,Compose 将它用于界面尺寸与位置的算法上,也就是 LookaheadLayout。它的特殊之处在于它会进行两次测量和布局的流程,在正式的测量和布局之前,会有一个前瞻(lookahead)测量和布局过程。在正式的测量和布局过程中,可以加一个额外的 LayoutModifier,对前瞻测量和布局的结果加以修正。
LookaheadLayout 的主要功能可能还是在页面进行状态切换的时候,可以在不同状态之间可以进行相对平滑渐进式的切换,此处暂时先不详细介绍。
LookaheadOnPlacedModifier 的作用与 OnPlacedModifier 相当,只不过前者能在 LookaheadLayout 的内部使用,并且提供一个额外信息,在正式测量与布局阶段得以回调,并提供前瞻测量与布局结果,该结果会被 LookaheadLayout 加以利用。
在写法和原理上,LookaheadOnPlacedModifier 也与 OnPlacedModifier 相似。在 Modifier 上可以调用两种 onPlaced():
Modifier.onPlaced { lookaheadScopeCoordinates -> {} }
Modifier.onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> {} }
其中第一个 onPlaced() 的函数参数只有一个 LayoutCoordinates 类型的参数:
/**
* 在父 [LayoutModifier] 被放置之后、子 [LayoutModifier] 被放置之前调用 [onPlaced]。
* 这允许子 [LayoutModifier] 根据其父布局来调整自身的放置位置。
*/
@Stable
fun Modifier.onPlaced(onPlaced: (LayoutCoordinates) -> Unit
) = this.then(OnPlacedModifierImpl(callback = onPlaced,inspectorInfo = debugInspectorInfo {name = "onPlaced"properties["onPlaced"] = onPlaced})
)
而第二个 onPlaced() 的函数参数有两个,并且该 Modifier 的扩展函数是定义在 LookaheadLayoutScope 接口中的:
@ExperimentalComposeUiApi
interface LookaheadLayoutScope {/*** 在父 [LayoutModifier] 被放置之后、子 [LayoutModifier] 被放置之前调用 [onPlaced]。* 这允许子 [LayoutModifier] 根据其父布局来调整自身的放置位置。** [onPlaced] 回调将以 [LookaheadLayout] 发出的 [LayoutNode] 的 [LookaheadLayoutCoordinates]* 作为第一个参数,以及此修饰符的 [LookaheadLayoutCoordinates] 作为第二个参数被调用。通过* [LookaheadLayoutCoordinates.localLookaheadPositionOf] 和* [LookaheadLayoutCoordinates.localPositionOf],可以分别计算出该修饰符在 * [LookaheadLayout] 坐标系中的前瞻位置和当前位置。*/fun Modifier.onPlaced(onPlaced: (lookaheadScopeCoordinates: LookaheadLayoutCoordinates,layoutCoordinates: LookaheadLayoutCoordinates) -> Unit): Modifier
}
因此这个 onPlaced() 需要在 LookaheadLayout() 中使用,因为 LookaheadLayout() 的第一个参数 content 指定接收者为 LookaheadLayoutScope,提供了调用 onPlaced() 的环境:
@Suppress("ComposableLambdaParameterPosition")
@ExperimentalComposeUiApi
@UiComposable
@Composable
fun LookaheadLayout(content: @Composable @UiComposable LookaheadLayoutScope.() -> Unit,modifier: Modifier = Modifier,measurePolicy: MeasurePolicy
)
大致的使用方式如下:
LookaheadLayout(content = { Row(Modifier.onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> }) {} },measurePolicy = // 这个位置暂时不知道填什么,后续会讲吧,因为本节不会讲 LookaheadLayout 的具体用法
)
原理与 OnPlacedModifier 几乎一样。
OnPlacedModifier、LookaheadOnPlacedModifier 与 OnRemeasuredModifier 被归类为 LayoutAware,指对于测量与布局过程有感知的,测量与布局过程会导致它们的回调函数被调用。
5、OnGloballyPositionedModifier
/**
* 一个修饰符,当内容的全局位置可能已经改变时,使用布局的最终 LayoutCoordinates 调用
* [onGloballyPositioned]。注意,它将在合成完成后、坐标最终确定时被调用。
*/
@JvmDefaultWithCompatibility
interface OnGloballyPositionedModifier : Modifier.Element {/*** 在测量后,使用布局最终的 LayoutCoordinates 调用本函数。注意,它是在* 一次组合(composition)完成后,当 coordinates 最终确定后被调用。* 在 Modifier 链中的位置对 [LayoutCoordinates] 参数或 [onGloballyPositioned]* 的调用时机没有影响。*/fun onGloballyPositioned(coordinates: LayoutCoordinates)
}
GloballyPositioned 取 GPS(Global Positioning System,全球定位系统)之意,可以将 OnGloballyPositionedModifier 理解为一个全局定位的 Modifier。意思是当管理 OnGloballyPositionedModifier 的那个 LayoutNodeWrapper 的尺寸或位置被刷新了,就会回调 onGloballyPositioned()。
管理的关系,我们前面已经说过多次,这里再举例说一下:
// 规 Text() 对应的 InnerPlaceable 管理
Text("haha", Modifier.onGloballyPositioned { })
Text("haha",Modifier.onGloballyPositioned { } // 归右侧的 size() 管理.size(40.dp))
Text("haha",Modifier.onGloballyPositioned { } // 归右侧的 size() 管理.size(40.dp).onGloballyPositioned { } // 归右侧的 padding() 管理.padding(20.dp)
)
下面关注接口函数的参数类型 LayoutCoordinates,实际上在前面的 OnPlacedModifier 与 LookaheadOnPlacedModifier 接口的函数参数中,也传入了 LayoutCoordinates。该接口表示一个存储布局测量边界(MeasureBox)的持有者,LayoutNodeWrapper 就实现了这个接口:
internal abstract class LayoutNodeWrapper(internal val layoutNode: LayoutNode
) : LookaheadCapablePlaceable(), Measurable, LayoutCoordinates, OwnerScope,(Canvas) -> Unit {...
}
而这些接口的函数参数传入的实际上就是它们的 Modifier 所在的、管理它们的 LayoutNodeWrapper 对象。那为什么参数不直接规定一个 LayoutNodeWrapper 类型,而是使用父接口 LayoutCoordinates 呢?这要看一下 LayoutCoordinates 的继承关系。
LayoutCoordinates 有两个直接的子类或子接口:
- 抽象类 LayoutNodeWrapper,有两个具体子类 InnerPlaceable 和 ModifiedLayoutNode
- 子接口 LookaheadLayoutCoordinates,有一个实现类 LookaheadLayoutCoordinatesImpl
然后再来看具体原因:
-
接口隔离原则(ISP):
- 抽象与实现分离:LayoutCoordinates 是公开接口,定义了跨层访问布局坐标的通用能力(如
localToRoot
/positionInRoot
等坐标转换方法),LayoutNodeWrapper 是 internal 修饰的模块内部的实现类。使用接口作为参数可以将能力声明与具体实现解耦。具体说来,在新版中 LayoutNodeWrapper 被替换为 NodeCoordinator 了,如果接口的 onGloballyPositioned() 参数定义的类型不是 LayoutCoordinates 接口而是实现类 LayoutNodeWrapper,那么需要修改接口参数为 NodeCoordinator,这是不对的 - 最小暴露原则:Compose 框架仅暴露 LayoutCoordinates 接口的 7 个核心函数,覆盖 95% 的布局交互需求,隐藏了 LayoutNodeWrapper 内 20+ 细节实现函数
- 抽象与实现分离:LayoutCoordinates 是公开接口,定义了跨层访问布局坐标的通用能力(如
-
实现保护机制:
- 防止非法向下转型:LayoutNodeWrapper 被 internal 修饰,无法在模块外访问该类型,强行通过反射获取实例会导致 IDE 报错(“Cannot access class ‘LayoutNodeWrapper’”),这是框架设计的主动防御
-
框架演进性:
- 实现可替换性:Compose 团队在 1.2 版本中重构了布局系统,将原有的
LayoutNodeWrapper
拆分为多个专项实现类(如MeasuredLayoutNodeWrapper
),但保持LayoutCoordinates
接口不变。若 API 直接暴露具体类型,会导致所有使用方代码在框架升级时崩溃 - 跨平台统一性:
LayoutCoordinates
接口在 Compose Multiplatform 中需要兼容不同平台的布局引擎(如 Android/iOS/Desktop),而各平台底层可能有不同的实现类,接口抽象可抹平平台差异。
- 实现可替换性:Compose 团队在 1.2 版本中重构了布局系统,将原有的
-
实际使用场景:开发者无需知道底层是
LayoutNodeWrapper
还是其他实现类,只需通过接口方法操作布局坐标系:Box(modifier = Modifier.onGloballyPositioned { coordinates ->val rootPosition = coordinates.positionInRoot() // 仅通过接口方法获取位置val width = coordinates.size.width // 获取布局尺寸if (coordinates.isAttached()) { // 检查布局状态// 处理坐标...} })
-
设计模式映射:这种设计完美体现了“依赖倒置原则”,高层模块通过抽象接口与底层实现交互,完全解耦。
-
若强行暴露实现类会怎样,假设框架错误地使用具体类:
// 伪代码:错误设计 fun onGloballyPositioned(coordinates: LayoutNodeWrapper)
会导致:
- 框架升级时修改
LayoutNodeWrapper
内部结构 → 所有调用方代码崩溃 - 开发者可能误用内部方法(如调用
innerCoordinator.modifyParentData()
) - 多平台代码需要为每个平台重复声明相同方法
- 单元测试必须 mock 整个
LayoutNodeWrapper
(而实际上只需验证接口方法)
- 框架升级时修改
结论:接口设计的必要性
Jetpack Compose 通过 LayoutCoordinates
接口实现了:
- 技术隐蔽性:隐藏 20+ 个内部方法
- 二进制兼容:保证 1.0-1.6 版本间 ABI 兼容
- 跨平台一致性:统一 Android/Desktop/iOS 的坐标系统
- 开发者体验:通过有限的 7 个方法降低学习成本
这种设计模式在 Compose 中广泛应用(如 Modifier
/Composable
等接口),是框架保持高扩展性和稳定性的核心机制。
秃头说:把接口缩小到恰好够用的范围,可以减少不小心写错代码而发生错误的机会。
当我们想得到一个区域(OnGloballyPositionedModifier 右侧的 LayoutModifier 所掌管的区域)尺寸或位置发生变化的回调时,可以使用 onGloballyPositioned()。看似与 onPlaced() 功能类似,两个接口内函数的参数都是它们内部的 Modifier 所属的 LayoutModifier(有边有设置 LayoutModifier 的衍生 Modifier)或者所在的 Composable 函数(Modifier 中没有显式设置 LayoutModifier)所对应的 LayoutNodeWrapper。
与 onPlaced() 的区别是回调时机不同。OnPlacedModifier 是在测量和布局过程中,在 LayoutModifier 或 Composable 函数完成测量,进行摆放时回调其内部的 onPlaced(),但是它们内层的 LayoutModifier 或 Composable 函数还没有摆放,此时可以通过设置 OnPlacedModifier 来影响内层的布局摆放过程。
而 OnGloballyPositionedModifier 则是在自己所对应的 LayoutNodeWrapper 相对于窗口的位置和尺寸被更新时回调。比如 List 中的 Item 可能会有图标,在 List 滑动时,图标相对于该 Item 是没有尺寸与位置的变化的,因此 OnPlacedModifier 不会被触发,但是图标相对于整个窗口的位置是发生了变化的,因此会触发 OnGloballyPositionedModifier。
但实际使用时,OnGloballyPositionedModifier 往往会在有可能发生尺寸与位置变化时就被回调。它被触发的场景与次数会远远多于 OnPlacedModifier,也就更重量级。所以在对二者进行选择时,往往能用 OnPlacedModifier 就尽量用 OnPlacedModifier,OnPlacedModifier 不够了才用 OnGloballyPositionedModifier。
原理上,还是两个部分,存与取出使用,取出时需要注意一个特殊点。在 AndroidComposeView 进行测量和布局时:
override fun measureAndLayout(sendPointerUpdate: Boolean) {trace("AndroidOwner:measureAndLayout") {val resend = if (sendPointerUpdate) resendMotionEventOnLayout else nullval rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)if (rootNodeResized) {requestLayout()}// 分发 OnPositioned 回调measureAndLayoutDelegate.dispatchOnPositionedCallbacks()}}
dispatchOnPositionedCallbacks() 最终会调用到 OnGloballyPositionedModifier 内的接口函数:
// MeasureAndLayoutDelegate:fun dispatchOnPositionedCallbacks(forceDispatch: Boolean = false) {if (forceDispatch) {onPositionedDispatcher.onRootNodePositioned(root)}// 向下分发onPositionedDispatcher.dispatch()}// OnPositionedDispatcher:fun dispatch() {// sort layoutNodes so that the root is at the end and leaves are at the frontlayoutNodes.sortWith(DepthComparator)layoutNodes.forEachReversed { layoutNode ->if (layoutNode.needsOnPositionedDispatch) {dispatchHierarchy(layoutNode)}}layoutNodes.clear()}private fun dispatchHierarchy(layoutNode: LayoutNode) {layoutNode.dispatchOnPositionedCallbacks()layoutNode.needsOnPositionedDispatch = falselayoutNode.forEachChild { child ->dispatchHierarchy(child)}}// LayoutNode:internal fun dispatchOnPositionedCallbacks() {if (layoutState != Idle || layoutPending || measurePending) {return // it hasn't yet been properly positioned, so don't make a call}if (!isPlaced) {return // it hasn't been placed, so don't make a call}onPositionedCallbacks?.forEach {// 回调 OnGloballyPositionedModifier 的接口函数it.second.onGloballyPositioned(it.first)}}
说明,只要进行测量和布局,就会触发 OnGloballyPositionedModifier 的 onGloballyPositioned() 被回调。
6、ModifierLocal
ModifierLocal 是那种写 SDK 或第三方库会经常用到,但是写界面几乎永远都用不到的知识。
涉及到 ModifierLocal、ModifierLocalProvider 与 ModifierLocalConsumer。
与 ThreadLocal、CompositionLocal 中的 Local 含义一样,都是它们的前缀范围内的局部变量:
- ThreadLocal 会分配给每个线程一个专属的对象,它们只在各自线程内有效,出了线程范围就无效
- CompositionLocal 是某个被指定的 Composition 或 Composable 函数内的局部变量
6.1 作用与基本使用
ModifierLocal 用于穿透 Modifier,因为多个 Modifier 之间无法共享数据。比如说我给 Modifier 的伴生对象设置两个 LayoutModifier:
Modifier.layout { measurable, constraints ->val placeable = measurable.measure(constraints)val widthString = placeable.width.toString()layout(placeable.width, placeable.height) {placeable.placeRelative(0, 0)}}.layout { measurable, constraints ->val placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {placeable.placeRelative(0, 0)}}
第二个 layout() 内是访问不到第一个 layout() 内的 widthString 变量的,因为 widthString 是第一个 layout() 的函数类型参数的函数体内的局部变量。
现在我使用 modifierLocalProvider() 构造一个 ModifierLocal:
@ExperimentalComposeUiApi
fun <T> Modifier.modifierLocalProvider(key: ProvidableModifierLocal<T>, value: () -> T): Modifier {
}
该函数需要两个参数,一是 key,类型是 ModifierLocal 的子类 ProvidableModifierLocal,二是提供值的函数。简单写一个例子,Modifier 达到了穿透效果:
val sharedKey = modifierLocalOf { "default value" }Modifier.modifierLocalProvider(sharedKey) {"100"}.modifierLocalConsumer { sharedKey.current }
首先通过 modifierLocalOf() 创建一个 ProvidableModifierLocal 作为 key,在参数中指定一个生产默认值的工厂,以便在 ModifierLocal 没通过 Provider 消费时使用:
fun <T> modifierLocalOf(defaultFactory: () -> T): ProvidableModifierLocal<T> =ProvidableModifierLocal(defaultFactory)
接下来调用 modifierLocalProvider() 时传入 key 和生产 value 的函数,注意由于使用 modifierLocalOf() 时已经指定了初始值是 String 类型,因此这里生产的 value 也应该是 String 类型。
最后通过 modifierLocalConsumer() 指定函数参数的接收者类型为 ModifierLocalReadScope,在参数的函数中可以直接通过 key 的 current 属性访问到 key 的最新值:
@Stable
@ExperimentalComposeUiApi
fun Modifier.modifierLocalConsumer(consumer: ModifierLocalReadScope.() -> Unit): Modifier {return this.then(ModifierLocalConsumerImpl(consumer,debugInspectorInfo {name = "modifierLocalConsumer"properties["consumer"] = consumer}))
}
这样看下来呢,虽然名字起的是 ModifierLocal 看起来像一个 Modifier 的局部变量,但实际上,是通过键值对的方式来更新与获取 Modifier 的值的。
6.2 实际应用
利用上面讲的 modifierLocalProvider() 和 modifierLocalConsumer() 两个函数可以解决 Modifier 数据无法在多个 Modifier 间共享的问题吗?答案是不可以,比如:
Modifier.modifierLocalProvider(sharedWidthKey) {// 提供 Modifier 宽度数据的代码,比如结果是 100100}.layout { measurable, constraints ->val placeable = measurable.measure(constraints)val widthString = placeable.width.toString()layout(placeable.width, placeable.height) {placeable.placeRelative(0, 0)}}.modifierLocalConsumer {// 打印 Modifier 的共享数据println(sharedWidthKey.current)}.layout { measurable, constraints ->val placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {placeable.placeRelative(0, 0)}}
这样做很明显达不到我们的目的。我们是想让第一个 layout() 内的变量 width 可以共享给第二个 layout(),但使用 modifierLocalProvider() 和 modifierLocalConsumer() 之后,只是实现了 modifierLocalProvider() 提供的数据共享给 modifierLocalConsumer()。
先尝试一种解决方法,就是绕过两个简便函数,直接实现它们背后的接口 ModifierLocalProvider 和 ModifierLocalConsumer:
val sharedWidthKey = modifierLocalOf { "0" }Modifier.then(object : LayoutModifier, ModifierLocalProvider<String> {lateinit var widthString: Stringoverride fun MeasureScope.measure(measurable: Measurable,constraints: Constraints): MeasureResult {val placeable = measurable.measure(constraints)// 测量过程中被赋值,但是无法保证获取 value 值的操作一定在赋值之后widthString = placeable.width.toString()return layout(placeable.width, placeable.height) {placeable.placeRelative(0, 0)}}override val key: ProvidableModifierLocal<String>get() = sharedWidthKeyoverride val value: String// 不能在 get() 之外初始化 value 值get() = widthString}).then(object : LayoutModifier, ModifierLocalConsumer {lateinit var sharedWidthString: Stringoverride fun MeasureScope.measure(measurable: Measurable,constraints: Constraints): MeasureResult {// 可以使用共享的 sharedWidthString 用于定制测量过程了sharedWidthStringval placeable = measurable.measure(constraints)return layout(placeable.width, placeable.height) {placeable.placeRelative(0, 0)}}override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {// 保存上游共享的 ModifierLocal 数据供测试过程使用,由于本函数的调用会早于// 测量的 measure(),所以在 measure() 中使用 sharedWidthString 是安全的,// 这种用法也是比较常规的用法sharedWidthString = sharedWidthKey.current}})
这样做确实能共享 widthString,但问题是不能确保对 widthString 的访问一定在测量过程为 widthString 初始化赋值之后。一旦在 widthString 初始化之前访问它,由于 lateinit var 的影响,就会抛出异常。为了解决这个问题,可以将共享对象声明为数组,在数组中存放要共享的对象,这样下游虽然有可能会先于测量过程拿到数组,但是因为它拿到的是数组的引用,这样在测量过程进行时,为数组内的元素,也就是要共享的数据进行赋值之后,下游还是可以在要使用数据之前,去取数组内对应的元素拿到共享数据:
// 将要共享的数据放入 Array<String> 数组中val sharedWidthKey = modifierLocalOf { arrayOf("0") }Modifier.then(object : LayoutModifier, ModifierLocalProvider<Array<String>> {val widthString = arrayOf("")override fun MeasureScope.measure(measurable: Measurable,constraints: Constraints): MeasureResult {val placeable = measurable.measure(constraints)// 测量过程中被赋值,但是无法保证获取 value 值的操作一定在赋值之后widthString[0] = placeable.width.toString()return layout(placeable.width, placeable.height) {placeable.placeRelative(0, 0)}}override val key: ProvidableModifierLocal<Array<String>>get() = sharedWidthKeyoverride val value: Array<String>// 不能在 get() 之外初始化 value 值get() = widthString})
实际开发中,还常常会同时实现 ModifierLocalProvider 和 ModifierLocalConsumer 两个接口,实现多级的连续消费的效果:
// InsetsPaddingModifier 实现了上述两个接口以及 LayoutModifier,一级处理完将数据交给下一级处理
Modifier.windowInsetsPadding(WindowInsets(4.dp, 4.dp, 4.dp, 4.dp)).windowInsetsPadding(WindowInsets(4.dp, 6.dp, 4.dp, 6.dp)).windowInsetsPadding(WindowInsets(4.dp, 2.dp, 4.dp, 2.dp))
6.3 原理
作用和用法是关键,原理不用背,是辅助理解作用和用法的。
还是从存储和回调/生效两个方面。
存储与以往大部分 Modifier 的存储类似,还是在 LayoutNode 的 modifier 属性的 set() 中:
override var modifier: Modifier = Modifierset(value) {...// Create a new chain of LayoutNodeWrappers, reusing existing ones from wrappers// when possible.val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->if (mod is RemeasurementModifier) {mod.onRemeasurementAvailable(this)}toWrap.entities.addBeforeLayoutModifier(toWrap, mod)if (mod is OnGloballyPositionedModifier) {getOrCreateOnPositionedCallbacks() += toWrap to mod}val wrapper = if (mod is LayoutModifier) {// Re-use the layoutNodeWrapper if possible.(reuseLayoutNodeWrapper(toWrap, mod)?: ModifiedLayoutNode(toWrap, mod)).apply {onInitialize()updateLookaheadScope(mLookaheadScope)}} else {toWrap}wrapper.entities.addAfterLayoutModifier(wrapper, mod)wrapper}// 在这里处理 ModifierLocalsetModifierLocals(value)outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapperlayoutDelegate.outerWrapper = outerWrapperif (isAttached) {// call detach() on all removed LayoutNodeWrapperswrapperCache.forEach {it.detach()}// attach() all new LayoutNodeWrappersforEachDelegateIncludingInner { layoutNodeWrapper ->if (!layoutNodeWrapper.isAttached) {layoutNodeWrapper.attach()} else {layoutNodeWrapper.entities.forEach { it.onAttach() }}}}wrapperCache.clear()// call onModifierChanged() on all LayoutNodeWrappersforEachDelegateIncludingInner { it.onModifierChanged() }// Optimize the case where the layout itself is not modified. A common reason for// this is if no wrapping actually occurs above because no LayoutModifiers are// present in the modifier chain.if (oldOuterWrapper != innerLayoutNodeWrapper ||outerWrapper != innerLayoutNodeWrapper) {invalidateMeasurements()} else if (layoutState == Idle && !measurePending && addedCallback) {// We need to notify the callbacks of a change in position since there's// a new one.invalidateMeasurements()} else if (innerLayoutNodeWrapper.entities.has(EntityList.OnPlacedEntityType)) {// We need to be sure that OnPlacedModifiers are called, even if we don't// have a relayout.owner?.registerOnLayoutCompletedListener(this)}// If the parent data has changed, the parent needs remeasurement.layoutDelegate.updateParentData()if (invalidateParentLayer || shouldInvalidateParentLayer()) {parent?.invalidateLayer()}}
setModifierLocals():
private fun setModifierLocals(modifier: Modifier) {// Collect existing consumers and providersval consumers = mutableVectorOf<ModifierLocalConsumerEntity>()var node: ModifierLocalProviderEntity? = modifierLocalsHeadwhile (node != null) {consumers.addAll(node.consumers)node.consumers.clear()node = node.next}// Create the chainmodifierLocalsTail = modifier.foldIn(modifierLocalsHead) { lastProvider, mod ->// Ensure that ModifierLocalConsumers come before ModifierLocalProviders// so that consumers don't consume values from their own providers.var provider = lastProvider// Special handling for FocusOrderModifier -- we have to use modifier local// consumers and providers for it.@Suppress("DEPRECATION")if (mod is androidx.compose.ui.focus.FocusOrderModifier) {val focusPropertiesModifier = findFocusPropertiesModifier(mod, consumers)?: run {// Have to create a new consumer/providerval scope = FocusOrderModifierToProperties(mod)FocusPropertiesModifier(focusPropertiesScope = scope,inspectorInfo = debugInspectorInfo {name = "focusProperties"properties["scope"] = scope})}addModifierLocalConsumer(focusPropertiesModifier, provider, consumers)provider = addModifierLocalProvider(focusPropertiesModifier, provider)}// 这里添加 if (mod is ModifierLocalConsumer) {addModifierLocalConsumer(mod, provider, consumers)}if (mod is ModifierLocalProvider<*>) {provider = addModifierLocalProvider(mod, provider)}provider}// Capture the value after the tail. Anything after the tail can be removed.node = modifierLocalsTail.next// Terminate the linked list at the tail.modifierLocalsTail.next = nullif (isAttached) {// These have been removed and should be detachedconsumers.forEach { it.detach() }// detach all removed providerswhile (node != null) {node.detach()node = node.next}// Attach or invalidate all providers and consumersforEachModifierLocalProvider { it.attachDelayed() }}}