Modifier
书接上篇 Jetpack Compose 之 Modifier(上)
OnRemeasuredModifier
和 OnPlacedModifier
的对比
OnRemeasuredModifier
和 OnPlacedModifier
都是在自定义布局流程里"插钩"用的接口,但它们跑的时机、能拿到的数据,以及典型用途都不一样:
1. 触发时机不同
Modifier | 触发时机 |
---|---|
OnRemeasuredModifier |
测量阶段 结束之后,measure(...) 完成时 |
OnPlacedModifier |
放置阶段 结束之后,place(...) 完成时 |
- OnRemeasured :在给定的
Constraints
下,子节点测量出width×height
后回调一次。 - OnPlaced :在父布局里把子节点定位放置(
place(x,y)
)后回调一次,可以拿到最终的LayoutCoordinates
。
2. 拿到的数据不同
Modifier | 回调参数 | 能做什么 |
---|---|---|
OnRemeasuredModifier |
IntSize(size) |
读取测量出的尺寸 |
OnPlacedModifier |
LayoutCoordinates(coords) |
读取放置后的位置、尺寸、父子关系、全局/本地坐标等 |
-
onRemeasured(size)
只给一个尺寸:size.width
和size.height
。 -
onPlaced(coords)
则给一个完整的LayoutCoordinates
,能查询:coords.positionInParent()
、positionInWindow()
;coords.size
;coords.parent
、coords.children
;coords.isAttachedToRoot()
、coords.globalPosition()
等。
3. 典型用例对比
-
测量驱动动画(OnRemeasured)
kotlinModifier.onRemeasured { size -> launch { // 根据 size.width 启动一次宽度适配动画 animateDpAsState(targetValue = size.width.toDp()) } }
-
获取绝对屏幕位置(OnPlaced)
kotlinModifier.onPlaced { coords -> val screenPos = coords.positionInWindow() // 把 screenPos 传给外部,做拖拽放置或弹窗定位 }
4. 底层实现区别
-
OnRemeasuredModifier:
- 属于
RemeasureEntityType
,当某层LayoutNodeWrapper
的measure()
完成并生成Placeable
后,框架会遍历所有挂在该层的OnRemeasuredModifier
实例,按添加顺序依次调用onRemeasured(size)
。
- 属于
-
OnPlacedModifier:
- 属于
PositioningEntityType
,当该层LayoutNodeWrapper
的所有子Placeable.place()
都调用完毕后,框架会遍历所有挂在该层的OnPlacedModifier
实例,依次调用onPlaced(coordinates)
。
- 属于
总结
- 测量结束想拿到「尺寸」就用
OnRemeasuredModifier
; - 放置结束想拿到「位置/坐标/层级」就用
OnPlacedModifier
。
LookaheadOnPlacedModifier 的作用、写法和原理
LookaheadOnPlacedModifier
是在 LookaheadScope
内对组件"被摆放"(placed)完成时的回调,它不仅会在正常布局完成时触发,还会把预先测量(lookahead pass)阶段得到的布局信息一并传给你,从而可以拿到未来 布局状态下的位置信息与当前布局状态下的位置信息,常用于基于前瞻信息做平滑过渡动画(例如共享元素动画) ,其写法与原理与 OnPlacedModifier
基本一致,只是多了一个参数,以及必须要在 LookaheadScope 中调用
典型写法
在 LookaheadScope
里,我们直接给子组件加上一个双参数的 onPlaced
回调,例如:
kotlin
LookaheadScope {
Row(
Modifier
.onPlaced { lookaheadCoordinates, actualCoordinates ->
// lookaheadCoordinates:预测完成后的 LayoutCoordinates
// actualCoordinates:正式布局完成后的 LayoutCoordinates
// 你可以基于两者的差值来驱动动画
}
) {
// ...子组件...
}
}
这个 API 与普通的 Modifier.onPlaced { coordinates -> ... }
用法几乎一模一样,区别在于回调签名多了一个 lookaheadCoordinates
参数。
底层原理
-
两次测量与布局
- Lookahead Pass :
LookaheadLayout
会先做一次"前瞻"测量和摆放,将目标状态下的尺寸和位置缓存起来; - Normal Pass :接着按常规测量和摆放,这时组件在布局过程中就能拿到那次前瞻的结果。
这两轮过程由LookaheadDelegate
和MeasurePassDelegate
分别驱动,前者只对LookaheadLayout
子树执行一次,后者执行真正的布局
- Lookahead Pass :
-
Modifier 存储与回调触发
- 在
LayoutNode.modifier
的 setter 中,所有实现了LookaheadOnPlacedModifier
的元素都会通过addAfterLayoutModifier
附加到相应的ModifiedLayoutNode
(或LayoutNodeWrapper
)的entities
列表中; - 布局完成后,Compose 会依次调用每层 wrapper 的
onLayoutComplete()
,其中会遍历entities
,针对 LookaheadOnPlacedModifier 类型调用回调,并把预先缓存的"lookahead"坐标和真实坐标都传给我们
- 在
-
为何与普通 OnPlacedModifier 同时机
LookaheadOnPlacedModifier
的触发时机与普通的OnPlacedModifier
完全一致------都在布局流程结束后调用,只是因为它是"后置"(addAfterLayoutModifier
)存储的,所以能够见到所有LayoutModifier
已经处理完的最终位置;不同点仅在于它回调时额外携带了第一次前瞻布局的结果。
kotlin
// androidx.compose.ui.layout.LookaheadOnPlacedModifier
@OptIn(ExperimentalComposeUiApi::class)
internal class LookaheadOnPlacedModifier(
// callback 就是我们实现的回调。
val callback: (
lookaheadScopeRootCoordinates: LookaheadLayoutCoordinates,
coordinates: LookaheadLayoutCoordinates
) -> Unit,
val rootCoordinates: () -> LookaheadLayoutCoordinates,
) : Modifier.Element {
fun onPlaced(coordinates: LookaheadLayoutCoordinates) {
callback(rootCoordinates(), coordinates)
}
}
// androidx.compose.ui.node.BackwardsCompatNode#onLookaheadPlaced
@OptIn(ExperimentalComposeUiApi::class)
override fun onLookaheadPlaced(coordinates: LookaheadLayoutCoordinates) {
val element = element
if (element is LookaheadOnPlacedModifier) {
element.onPlaced(coordinates)
}
}
// androidx.compose.ui.node.NodeCoordinator#onPlaced
@OptIn(ExperimentalComposeUiApi::class)
fun onPlaced() {
val lookahead = lookaheadDelegate
if (lookahead != null) {
visitNodes(Nodes.LayoutAware) {
// LookaheadOnPlacedModifier调用时机。
it.onLookaheadPlaced(lookahead.lookaheadLayoutCoordinates)
}
}
visitNodes(Nodes.LayoutAware) {
// OnPlacedModifier 调用时机
it.onPlaced(this)
}
}
综上,LookaheadOnPlacedModifier
就是专门为 LookaheadLayout
设计的双参数 placed
回调,能让你在做布局过渡动画时,既能拿到"未来"布局信息,也能拿到当前布局信息,大幅简化动画构造的工作。另外,可以看到,OnPlacedModifier(布局) OnRemeasuredModifier(测量) LookaheadOnPlacedModifier(布局)
都被归为了 Nodes.LayoutAware
类型。LayoutAware
代表的是对于布局过程有感知的,布局过程包含测量与布局。
kotlin
@OptIn(ExperimentalComposeUiApi::class)
internal fun calculateNodeKindSetFrom(element: Modifier.Element): Long {
// 省略其他代码......
if (
element is OnPlacedModifier ||
element is OnRemeasuredModifier ||
element is LookaheadOnPlacedModifier
) {
mask = mask or Nodes.LayoutAware
}
return mask
}
OnGloballyPositionedModifier 的作用、写法和原理
Modifier.onGloballyPositioned {layoutCoordinates -> ... }
会在该组件及所有其上层布局完成 放置 (placement)之后、在每次重新布局后立即 回调一次,给我们一个完整的 LayoutCoordinates
对象(这里没有使用 NodeCoordinates
是为了避免一些api 污染的问题。)。有点类似于android.view.View#addOnLayoutChangeListener
。我们可以通过它:
-
拿到该组件在 窗口 坐标系(
coords.positionInWindow()
)或 屏幕 坐标系(coords.localToWindow(Offset.Zero)
)中的绝对位置; -
查询它的大小(
coords.size
)、父子层级(coords.parent
/coords.children
)等; -
以此驱动后续的动画定位、弹窗对齐、测试断言或其他依赖"视图真实位置"的逻辑。
-
触发的方式:当它右边的LayoutModifier所控制的区域 size 之类的发生变化,当它所在的 Composable 函数所控制的区域的位置或者尺寸发生变化,会回调。并且它的回调函数中的 layoutCoordinates对象 就是 它所在区域的 LayoutCoordinates 的对象,也就是 NodeCoordinator。
kotlinModifier.onGloballyPositioned {}.size(currrentSize) // 当它右边的LayoutModifier所控制的区域size之类的发生变化 Modifier.onGloballyPositioned {} // 当它所在的 Composable 函数所控制的区域的位置或者尺寸发生变化
当我们想得到一个区域的位置或者尺寸的回调的时候,就可以已使用这个函数。
写法
kotlin
Box(
Modifier
.size(120.dp)
.onGloballyPositioned { layoutCoordinates ->// 这个参数其实就是它所属的 NodeCoordinator 的对象,与 Modifier.onPlaced{} 的参数是一样的
// 组件已经被放到了父布局中,这里可以拿到它的全局位置与大小
val topLeft = coords.positionInWindow() // 相对于 Window 左上角
val bounds = coords.boundsInWindow() // Rect(Offset, Size)
Log.d("Demo", "Placed at $topLeft; size=${coords.size}")
}
) {
Text("Hello")
}
- 任何实现了
LayoutModifier
(如padding
、size
)的放置完成后,都会触发靠它最"靠内" 的onGloballyPositioned
回调。 - 它会在 每次布局(首次、尺寸/位置变化后)都执行一次。
原理
onGloballyPositioned
原理也是分为两部分,一是存储, 二是回调,存储与其余大部分的 Modifier 一样,都是在 LayoutNode 里边的nodes.updateFrom(value)
完成的。

回调 则是从MeasureAndLayoutDelegate#dispatchOnPositionedCallbacks
这里发起的:


-
PositioningModifier
onGloballyPositioned
底层就是一个实现了OnPlacedModifier
接口的包装器(OnGloballyPositionedModifier
)。Compose 在把我们的Modifier
链拆成LayoutNodeWrapper
时,会把这个包装器注册到相应的布局节点上。 -
插入到放置阶段
由于它是一个 后置 布局修饰符(
addAfterLayoutModifier
),Compose 会在每层LayoutNodeWrapper
完成所有子节点的place(x,y)
之后 ,调用它的onPlaced(coordinates)
:kotlin// 伪代码:在每个 wrapper 完放置子项后 wrapper.entities.forEach(PositioningEntityType) { entity -> (entity.modifier as OnPlacedModifier).onPlaced(wrapper.coordinates) }
-
LayoutCoordinates 提供全局映射
回调给你的
coords: LayoutCoordinates
不仅包含子树的测量结果,也持有:coords.localToWindow(Offset.Zero)
:把本地(0,0)映射到 Window 坐标coords.parent
/coords.children
:节点树层级coords.size
:测量出的最终IntSize
因此你可以直接通过它计算"视图在屏幕上到底在哪里"。
小结
- 什么时候用 :当你需要确切知道一个 Compose 元素在屏幕/窗口中的位置与大小时,如做拖拽、定位弹窗、或 UI 自动测试。
- 怎么用 :在 Modifier 链中调用
.onGloballyPositioned { coords -> ... }
,每次布局完成后都会回调。 - 底层实现 :它是一个
OnPlacedModifier
,Compose 在放置阶段自动触发,并把完整的LayoutCoordinates
交给你。 - 与
onPlaced
的区别:两者回调的实际不同。onPlaced
是在测量和布局的过程中,每一个 LayoutModifier 或者说是 Composable 函数 在 它的外层去摆放它的时候,这个函数就会被调用。它的内层是还没有被调用的,例如我们可以在此时 调用.offset { IntOffset(offsetX, offsetY) }
(注意这个是一个LayoutMoidifer 函数,所以和其他的LayoutModifier有先后顺序。) 去影响内部的 布局。而onGloballyPositioned
则是在自己所对应的 NodeCoordinator 的相对整个窗口,对于窗口的位置被更新的时候,以及尺寸改变的时候,会等到整个 Compose 完成放置、节点附着到 Window才会触发。虽然理论上onPlaced
回调的时候onGloballyPositioned
可能不回调,但是实际上,一般的onPlaced
触发后onGloballyPositioned
也会被触发。-
onPlaced { coords -> ... }
- 回调给你的
coords: LayoutCoordinates
是 相对于它的父布局 的局部坐标和尺寸。 - 只能通过
coords.positionInParent()
、coords.size
、coords.parent
/children
等 API 得到局部信息。 - 如果你调用
positionInWindow()
,它会抛异常,因为它并不保证已经附着到 Window。
- 回调给你的
-
onGloballyPositioned { coords -> ... }
- 底层也是一个
OnPlacedModifier
,但它附着的是 全局 (在AndroidComposeView
最外层)的位置监听器。 coords.positionInWindow()
、coords.boundsInWindow()
、coords.windowToLocal()
都可用,你拿到的是组件在整个 窗口 / 屏幕 中的绝对位置和大小。
- 底层也是一个
-
ModifierLocal、ModifierLocalProvider、ModifierLocalConsumer 的原理与运用
1. 原理与机制
原来与其余的Modifier类似,也是两部分,存储和回调。存储也是在 LayoutNode#updateFrom(value)
中做的。

回调流程代码:
kotlin
// NodeChain#updateFrom
internal fun updateFrom(m: Modifier) {
attach()
}
// androidx.compose.ui.node.NodeChain#attach
fun attach() {
headToTail {
if (!it.isAttached) it.attach()
}
}
// Modifier#attach
internal fun attach() {
check(!isAttached)
check(coordinator != null)
isAttached = true
onAttach()
// TODO(lmr): run side effects?
// androidx.compose.ui.node.BackwardsCompatNode#onAttach
override fun onAttach() {
onModifierUpdated(true)
}
最终是发生在 BackwardsCompatNode#onModifierUpdated
中:

-
ModifierLocal 的本质
Compose 为了在一条 Modifier 链内传递上下文信息,引入了与 CompositionLocal 类似的机制------ModifierLocal。底层维护了一组节点(ProviderNode、ConsumerNode),在链合并(merge)阶段,这些节点会注册到一个"ModifierLocal 栈"中,保证每个 Consumer 都能在组合时读到距离自己最近的 Provider 值,否则回退到默认值 droidconAndroid Developers。
-
作用域
- CompositionLocal:作用于整个 Composition 树,可跨多个组件传递信息。
- ModifierLocal:仅限于单条 Modifier 链,更加轻量、局部化,适合与特定 UI 行为或布局属性绑定。
2. 核心 API
kotlin
// 1. 定义一个 ModifierLocal,提供默认值
val MyLocal = modifierLocalOf { defaultValue }
// 2. 在 Modifier 链上"提供"新值(Provider)
Modifier.modifierLocalProvider(MyLocal) { newValue }
// 3. 在后续同一链上的某处"消费"该值(Consumer)
Modifier.modifierLocalConsumer {
// 这里可以通过 MyLocal.current 读取到 newValue
}
modifierLocalOf { ... }
定义一个可提供(ProvidableModifierLocal)Local,并指定默认值;modifierLocalProvider
会在链上插入 ProviderNode,将MyLocal.current
设为value()
;modifierLocalConsumer
会插入 ConsumerNode,组合阶读取时在ModifierLocalReadScope
中执行 lambda,可安全访问所有ProvidableModifierLocal.current
Android Developers。
3. Provider & Consumer 的工作流程
-
定义
kotlin// 在文件顶层或 Companion 对象中定义 val LocalFoo = modifierLocalOf { Foo() }
-
注入(Provider)
kotlinBox( Modifier .modifierLocalProvider(LocalFoo) { Foo(config) } .then(/* 其他 Modifier */) )
- 系统将创建一个 ProviderNode 并记录在当前链的上下文中。
-
消费(Consumer)
kotlinModifier.modifierLocalConsumer { val foo = LocalFoo.current // 在此处可以基于 foo 做测量、布局或绘制 }
- 当框架 merge Modifier 链并在组合阶段应用时,ConsumerNode 会回调你提供的 lambda,此时
LocalFoo.current
已被解析为最近 Provider 提供的值或默认值。
- 当框架 merge Modifier 链并在组合阶段应用时,ConsumerNode 会回调你提供的 lambda,此时
4. 与 CompositionLocal 的比较
特性 | CompositionLocal | ModifierLocal |
---|---|---|
定义方法 | compositionLocalOf { ... } |
modifierLocalOf { ... } |
提供/消费 | CompositionLocalProvider + Local.current |
modifierLocalProvider + modifierLocalConsumer + Local.current |
作用域 | 整棵组合树 | 单条 Modifier 链 |
典型用途 | 主题、布局方向、语言环境等跨组件状态 | 局部触摸反馈、测量参数、布局配置、分析埋点等与 Modifier 强相关的场景 |
5. 典型场景示例
-
统一内边距
kotlinval LocalPadding = modifierLocalOf { 0.dp } Modifier .modifierLocalProvider(LocalPadding) { 16.dp } .modifierLocalConsumer { val pad = LocalPadding.current then(Modifier.padding(pad)) }
-
点击埋点
kotlinprivate val ScreenNameLocal = modifierLocalOf<String?> { null } @Composable fun Modifier.analyticsScreen(screenName: String): Modifier { // 在链上写入 screenName,供后续 Consumer 获取 return this .then( Modifier.modifierLocalProvider(ScreenNameLocal) { screenName } ) .then( Modifier.modifierLocalConsumer { val name = ScreenNameLocal.current // 这里可以上报埋点:LaunchEvent(name) } ) }
这样,无需在每个点击事件里手动传递 screenName,就能在自定义的 Consumer 中拿到当前页面标识并上报。
-
两个LayoutModifier传递
sharedString
kotlinval sharedWidthKey = modifierLocalOf { "0" } Modifier .then(object : LayoutModifier, ModifierLocalProvider<String> { // 同时实现连个Modifier lateinit var widthString: String override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val placeable = measurable.measure(constraints) return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } override val key: ProvidableModifierLocal<String> get() = sharedWidthKey override val value: String get() = widthString }) .then(object : LayoutModifier, ModifierLocalConsumer {// 同时实现连个Modifier lateinit var sharedString: String override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { sharedString // 可以使用该变量了。 val placeable = measurable.measure(constraints) return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // 开始测量和布局之前调用,并且是整个Modifier链条从上到下每一个ModifierLocalConsumer 依次调用这个函数, // 它并不会主动把上游共享给我们的数据都传递给我们,但是我们函数的内部可以手动的去获取那些数据。 override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) { sharedString = sharedWidthKey.current } }) .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) } }
-
实现多级连续消费的效果
kotlin
// windowInsetsPadding 给界面的组件加上边距,让我们组件不会被系统组件盖住。
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))
@Stable
fun Modifier.windowInsetsPadding(insets: WindowInsets): Modifier = this.then(
InsetsPaddingModifier(insets, debugInspectorInfo { // 同时实现了ModifierLocalConsumer与ModifierLocalProvider接口
name = "windowInsetsPadding"
properties["insets"] = insets
})
)
internal class InsetsPaddingModifier(
private val insets: WindowInsets,
inspectorInfo: InspectorInfo.() -> Unit = debugInspectorInfo {
name = "InsetsPaddingModifier"
properties["insets"] = insets
}
) : InspectorValueInfo(inspectorInfo), LayoutModifier,
ModifierLocalConsumer, ModifierLocalProvider<WindowInsets> {
小结
ModifierLocal
为我们提供了一条"轻量级"从父级 modifier 向子 modifier 传递数据的途径。modifierLocalOf
→ 定义键;Modifier.modifierLocalProvider
→ 提供值;Modifier.modifierLocalConsumer
→ 消费值。- 适用于需要在 layout 或 draw 逻辑里读取上游提供的参数,而不想新增一大堆函数参数的场景。
通过这种方式,我们可以让自定义 Modifier
之间灵活协作,实现更可扩展、可复用的 UI 构建组件。