关于Modifier的其余篇章: Jetpack Compose 之 Modifier(上) Jetpack Compose 之 Modifier(下)
Modifier
PointerInputModifier 的功能介绍和原理简析
PointerInputModifier
是负责拦截和处理原始触摸/鼠标等「指针事件」的底层机制。它并不是你直接拿来用的 API,而是通过一系列高级手势和交互(如 clickable { ... }
、draggable { ... }
、pointerInput { ... }
等)统一落到它头上来执行。下面分两部分:功能简介 & 原理简析。
Modifier 自带的点击事件的触发方式有两种:
kotlin
Modifier.clickable { } // 单击
fun Modifier.combinedClickable(
enabled: Boolean = true, // 是否启用
onClickLabel: String? = null, // 无障碍描述
onLongClickLabel: String? = null, // 无障碍描述
onClick: () -> Unit, // 单击回调
onLongClick: () -> Unit = {}, // 长按回调
onDoubleClick: () -> Unit = {}, // 双击回调
role: Role? = Role.Button, // Accessibility Role
indication: Indication? = rememberRipple(), // 点击时的水波纹效果
interactionSource: MutableInteractionSource // 用于跟踪按下、释放、焦点等交互状态
)
// 使用:
@Composable
fun CombinedClickableSample() {
// 用于指示器(Ripple)的状态追踪
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier
.size(100.dp)
.background(Color.LightGray)
.combinedClickable(
enabled = true,
onClickLabel = "点击",
onLongClickLabel = "长按",
onClick = { Log.d("Demo", "单击了") },
onLongClick = { Log.d("Demo", "长按了") },
onDoubleClick = { Log.d("Demo", "双击了") },
role = Role.Button,
indication = rememberRipple(bounded = true),
interactionSource = interactionSource
),
contentAlignment = Alignment.Center
) {
Text("点我", color = Color.Black)
}
}
combinedClickable
与 clickable
的区别:
clickable
只支持单击;如果同时添加多个手势(如拖拽、滑动)可能会冲突;combinedClickable
同时集成了长按和双击检测,且内部已经做了手势冲突解决,更加健壮。
一、功能简介
-
捕获原始指针事件
PointerInputModifier
能拦截最底层的触摸事件(MotionEvent
)、鼠标事件,甚至滚轮、手写笔等,统一转换成 Compose 内部的PointerInputEvent
。
-
支持自定义手势
-
你可以在 Modifier 链上调用
kotlinModifier.pointerInput(key1, key2, ...) { // 类似于自带GestureDetector的onTouchEvent // 在协程里 awaitPointerEventScope { ... } / detectTapGestures { ... } // 监听与点击有关的事件 } // 还可以这样使用: Modifier.pointerInput(Unit) {// 监听与点击有关的 ,detectTapGestures( onTap = {}, onDoubleTap = {}, onLongPress = {}, onPress = {}) // 摸到屏幕的监听 forEachGesture { // 循环侦测,一般要用,否则只能侦测一次,detectTapGestures 等一系列都是这样实现的。 awaitPointerEventScope { // 监听每一个触摸事件,例如按下抬起。 val down = awaitFirstDown() // 监听抬起 } } }
Modifier.pointerInput(Unit) 与 combinedClickable 有什么区别呢?combinedClickable 的内部其实也是使用 detectTapGestures 来实现的,安卓和Compose 都支持鼠标,鼠标的点击事件不会触发 onTap ,但是会触发 onClick,另外 combinedClickable 也是没有这个回调的,但是内部其实是利用了这个回调。
-
上边写到的代码逻辑,这其实会在背后添加一个 PointerInputModifier
,启动一个协程来不断接收、处理和消费事件。PointerInputModifier
一共有两个实现类,一个是SuspendingPointerInputFilter
, 另一个是 PointerInteropFilter
,后者是用来和android中传统的 View 系统交互的, 我们填写的 PointerInputModifier
去哪里呢?其实和之前的 DrawModifier 一样,也是在androidx.compose.ui.node.LayoutNode#modifier
中处理的,因此可以知道,PointerInputModifier
也是对挨着它最近的右侧 LayoutModifier
生效的。
kotlin
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
}
下边这个代码是如何生效的?
kotlin
Modifier.pointerInput().pointerInput().size(??) // 左侧的pointerInput是右侧pointerInput的父点击
hitTest
就是事件处理的核心机制,
kotlin
internal fun hitTest(
pointerPosition: Offset,
hitTestResult: HitTestResult<PointerInputFilter>,
isTouchEvent: Boolean = false,
isInLayer: Boolean = true
) {
val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
outerLayoutNodeWrapper.hitTest(
LayoutNodeWrapper.PointerInputSource,
positionInWrapped,
hitTestResult,
isTouchEvent,
isInLayer
)
}
fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> hitTest(
hitTestSource: HitTestSource<T, C, M>,
pointerPosition: Offset,
hitTestResult: HitTestResult<C>,
isTouchEvent: Boolean,
isInLayer: Boolean
) {
val head = entities.head(hitTestSource.entityType()) // 为什么不直接通过Index去取,而是使用hitTestSource.entityType(),因为hitTest会被两种Modifier都会去使用的,处理 PointerInputModifier 还有 `SemanticsModifier(使用无障碍功能)` 也会使用。
// 省略其余代码......
head.hit(
hitTestSource,
pointerPosition,
hitTestResult,
isTouchEvent,
isInLayer
)
// 省略其余代码......
}
}
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) { // hitTestSource.contentFrom(this来源于下边的 contentFrom
next.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer) // 递归调用。
}
}
}
val PointerInputSource =
object : HitTestSource<PointerInputEntity, PointerInputFilter, PointerInputModifier> {
override fun entityType() = EntityList.PointerInputEntityType
@Suppress("ModifierFactoryReturnType", "ModifierFactoryExtensionFunction")
override fun contentFrom(entity: PointerInputEntity) =
entity.modifier.pointerInputFilter // entity.modifier 链表中的头节点
override fun interceptOutOfBoundsChildEvents(entity: PointerInputEntity) =
entity.modifier.pointerInputFilter.interceptOutOfBoundsChildEvents
override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) = true
override fun childHitTest(
layoutNode: LayoutNode,
pointerPosition: Offset,
hitTestResult: HitTestResult<PointerInputFilter>,
isTouchEvent: Boolean,
isInLayer: Boolean
) = layoutNode.hitTest(pointerPosition, hitTestResult, isTouchEvent, isInLayer)
}

-
手势冲突 & 事件消费
- 多个
PointerInputModifier
(或更高层的clickable
、draggable
)同时存在时,Compose 会按布局树深度与声明顺序,把事件分发到各个PointerInputModifier
,支持「先消费,后阻止向下/向上传递」的逻辑,从而处理好手势冲突。
- 多个
-
与 Compose 渲染解耦
- 它只关心「事件」,不参与布局测量和绘制,确保手势处理不会阻塞 UI 树的重排或重绘。
二、原理简析
-
Modifier 树中插入
PointerInputModifierNode
- 当你调用
Modifier.pointerInput { ... }
,Compose 会在该节点的Modifier
列表中插入一个PointerInputModifierNode
,并在布局阶段把它装配到对应的LayoutNode
里的pointerInputFilters
列表。
- 当你调用
-
PointerInputDispatcher & Dispatcher Loop
- 每个
Owner
(通常是最顶层的ComposeView
)维护一个PointerInputDispatcher
,它订阅 Android 系统的原始触摸回调(onTouchEvent(MotionEvent)
)。 - 系统一旦有触摸事件到来,
ComposeView
会把它封装成PointerInputEvent
,交给dispatcher.dispatch(event)
。
- 每个
-
事件分发
PointerInputDispatcher
会遍历所有注册在各个LayoutNode
上的PointerInputModifierNode
(按从最里到最外、从深到浅的顺序),依次调用它们的onPointerEvent()
。- 每个
PointerInputModifierNode
内部会把事件发给自身启动的协程(pointerInput
block)里的awaitPointerEventScope
,或调用pointerInputFilter.pointerInputFilter()
方法。
-
协程 & Await 系统
-
pointerInput { ... }
本质上是一个被LaunchedEffect
管理的协程,它在一个forEachGesture
或awaitPointerEventScope
循环中挂起和恢复:kotlinpointerInput(key) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // 对 event.positions 做判定、消费 (event.changes.consume()) } } }
-
当
onPointerEvent
到来时,会resume
相应协程,把新事件交给你。
-
-
事件消费 & 迭代
- 你可以在处理完某些指针变化后,调用
change.consume()
(或更细粒度的consumePositionChange()
、consumeDownChange()
)来告诉框架「这个事件部分已经被我消费,不要再分发给更上层或其他 handler」。
- 你可以在处理完某些指针变化后,调用
-
生命周期管理
- 当对应的 Composable 或
Modifier
离开 Composition,后台会自动取消那个协程并移除PointerInputModifierNode
,PointerInputDispatcher
也就不会再给它分发事件,保证不会泄漏。
- 当对应的 Composable 或
总结
PointerInputModifier
= Compose 最底层的「指针事件过滤器」,所有手势 API 都是建立在它之上。- 事件流 :Android
MotionEvent
→PointerInputEvent
→PointerInputDispatcher
→ 各PointerInputModifierNode.onPointerEvent()
→ 你的pointerInput { ... }
协程。 - 它的核心优势在于:基于协程的等待/恢复模型 、可消费/可拦截 、与布局/绘制解耦,从而让手势逻辑既灵活又高效。
ParentDataModifier
ParentDataModifier
是一种特殊的 Modifier
,它不直接参与子组件的测量或绘制,而是用来将"父布局需要的额外数据"从子组件"上传"到父布局。设置在子组件,但是却是给父组件用的,在布局过程中用于辅助测量的。在经典的 Android View 世界里,这个作用就相当于 LayoutParams
(比如 LinearLayout.LayoutParams
中的 weight
、gravity
等属性);在 Compose 里,就是 ParentDataModifier
。weight
、gravity
这些属性用 LayoutModifier
是实现不了的, 因为LayoutModifier
只是关注自己的实现。
一、为什么需要 ParentDataModifier
很多布局组件(如 Row
、Column
、Box
)在摆放子项时,需要额外的"子项元数据"来决定如何测量或定位它们:
-
Row
/Column
里的weight
-
Box
里的align
或者bias
-
自定义布局里某些子项可能需要提供"占位优先级"或"某个方向上的偏移量"
-
Modifier.layoutId("")
与 传统View 中的 ID 不同,只用用来做辅助测量的,例如下边的自定义 Compose 组件的示例,通过不同的ID触发不同的布局方式。
*kotlinCustomLayout(Modifier.size(40.dp)) { Text(text = "rengwuxian", Modifier.layoutId("big")) Text(text = "扔物线", Modifier.layoutId("small")) Box(Modifier.size(20.dp).background(Color.Red)) } @Composable fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) { Layout(content, modifier) { measurables, constraints -> measurables.forEach { it: Measurable when (it.layoutId) { "big" -> it.measure(constraints.xxxx) "small" -> it.measure(constraints.yyyy) else -> it.measure(constraints) } } } }
-
Layout(content = , measurePolicy = )
用来在最底层做最完整的测量。这是所有 Compose 函数最底层的实现。
这些元数据并不属于子组件本身的绘制或测量逻辑,而是由父布局在布局阶段去读取并据此调整位置/尺寸。ParentDataModifier
就是实现这一传输的桥梁。
二、ParentDataModifier 的写法
写法
-
前提:首先是要用
Layout()
组件去写一个自定义布局,才会用得到它; -
自定义布局的测量和布局的算法里,用
Measurable.parentData
拿到开发者给子组件设置的属性,然后去计算,Measurable.parentData
是用来对多个不同属性进行融合的,而不是对同一个属性做融合。 -
写一个自定义的
Modifier
函数,让它的内部创建一个ParentDataModifier
,并且实现它的modifyParentData()
函数,在里面提供对应的parentData
。modifyParentData()
的函数参数,是下一个ParentDataModifier
所提供的数据,可以和自己的数据进行融合后提供。- 可以把
Modifier
函数写专门的接口或object
对象,来避免 API 污染的问题(参照Row的写法)。
kotlin
// 使用示例:在 CustomLayout2 中,你可以为子元素添加权重(weightData)和"大"标记(bigData)
CustomLayout2 {
// 下面这行,编译器知道这是处在 CustomLayout2Scope 的接收者范围内,
// 可以无冲突地调用 weightData()、bigData()。
Text(
"1",
Modifier
.weightData(1f) // 为这个子项设置权重
.bigData(true) // 标记为"大"布局
)
// 第二个 Text:没有额外的 parentData
Text("2")
// 第三个子元素在一个 Box 中,也可以使用 weightData
Box {
Text("3", Modifier.weightData(1f)) // 报错,weightData 没有直接在 CustomLayout2 中使用,而是在 CustomLayout2 下的 Box 中使用。
}
}
@Composable
fun CustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(content, modifier) { measurables, constraints ->
// 遍历每个可测量项,根据它的 layoutId 决定测量方式
measurables.forEach { measurable ->
when (measurable.layoutId) {
"big" -> measurable.measure(constraints.xxxx) // "big" 专用约束
"small" -> measurable.measure(constraints.yyyy) // "small" 专用约束
else -> measurable.measure(constraints) // 默认约束
}
}
// 最终布局大小与放置逻辑
layout(width = 100, height = 100) {
// 在这里调用 place() 将子元素放入具体位置
...
}
}
}
@Composable
fun CustomLayout2(
modifier: Modifier = Modifier,
content: @Composable CustomLayout2Scope.() -> Unit
) {
// 构造一个受 CustomLayout2Scope 扩展的 content
Layout(
content = { CustomLayout2Scope.content() },
modifier = modifier
) { measurables, constraints ->
// ------ 第一步:遍历所有 measurables
measurables.forEach { measurable ->
// ------ 第二步:从 measurable.parentData 中取出前面设置的 Layout2Data
val data = measurable.parentData as? Layout2Data
// 然后就可以读取出两个自定义属性
val big = data?.big // 是否标记为"大"
val weight = data?.weight // 权重值
// 根据 big、weight 进行测量逻辑
...
}
// 最终布局
layout(width = 100, height = 100) {
...
}
}
}
// 用来承载综合数据的类,可以同时使用多个自定义的属性。
class Layout2Data(var weight: Float = 0f, var big: Boolean = false)
@LayoutScopeMarker // 这个扩展函数只在 `CustomLayout2Scope` 内可用, 且必须是直接在里边,而不能是间接的。
object CustomLayout2Scope {
/**
* 为当前元素添加一个 weightData(parentData),
* 该 parentData 会被 CustomLayout2 在测量阶段读取。
*/
fun Modifier.weightData(weight: Float) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
// ------ 第三步:如果已有 parentData,就更新 weight,否则创建新的 Layout2Data
return if (parentData == null) {
Layout2Data(weight = weight)
} else {
(parentData as Layout2Data).apply {// 参数融合,Modifier.weightData(1f).bigData(true)),因为默认的 bigData 为 false
this.weight = weight
}
}
}
})
/**
* 为当前元素添加一个 bigData(parentData),
* 标记它是否是"大"元素。
*/
fun Modifier.bigData(big: Boolean) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
// 如果已有 Layout2Data,就更新 big;否则新建一个
return ((parentData as? Layout2Data) ?: Layout2Data()).also {// 参数融合,Modifier.weightData(1f).bigData(true)),因为默认的 weight 为 0f
it.big = big
}
}
})
}
Row/Column 的 weight
kotlin
Row(modifier = Modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.weight(1f) // 这里其实就是添加了一个 ParentDataModifier
.height(50.dp)
.background(Color.Red)
)
Box(
modifier = Modifier
.weight(2f)
.height(50.dp)
.background(Color.Blue)
)
}
Modifier.weight(...)
底层插入了一个ParentDataModifier
,把 weight 值"存"在子节点上。- 当
Row
在测量时,会遍历它所有子节点,读取每个 child 的 weight(通过child.parentData as? WeightParentData
),然后根据权重分配可用宽度。
Box 的 align
kotlin
Box(modifier = Modifier.size(200.dp)) {
Text(
"Bottom End",
modifier = Modifier
.align(Alignment.BottomEnd) // 添加了 ParentDataModifier,告诉 Box 在右下角绘制
)
}
Modifier.align()
也是个ParentDataModifier
,它把对齐信息(Alignment
)附加给子布局。Box
在布局时,就会检查每个子项的 parentData(BoxParentData
),据此决定子项的x,y
坐标。
三、原理简析
-
插入节点
当你在 Modifier 链里调用如
weight()
、align()
等 API 时,Compose 会往这个子节点的Modifier
列表中插入一个实现了ParentDataModifier
接口的节点。 -
数据携带
这个 Modifier 内部会保存一份针对当前子节点的"父数据"(例如
weight = 1f
),并重写modifyParentData
方法返回一个相应的ParentData
对象。 -
父布局读取
在父布局的
MeasurePolicy
里(measure
或layout
阶段),它会调用类似child.parentData
(或在 DSL 中通过data = childData()
)来获取ParentData
。- Compose 会自动把子节点的所有
ParentDataModifier
累积起来,形成最终的LayoutNode.parentData
。
- Compose 会自动把子节点的所有
-
依据父数据布局
父布局根据读取到的 parentData 来调整测量规则、分配空间、或者定位坐标。
四、自定义 ParentDataModifier
如果在写自定义布局,也可以定义自己的 ParentDataModifier
:
kotlin
// 1. 定义存储的数据类型
data class MyParentData(val priority: Int)
// 2. 定义 Modifier
fun Modifier.priority(value: Int) = this.then(
object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any {
// parentData 可能是上一个 ParentDataModifier 的结果
return (parentData as? MyParentData)?.copy(priority = value)
?: MyParentData(value)
}
}
)
// 3. 自定义布局读取
@Composable
fun MyCustomLayout(content: @Composable () -> Unit) {
Layout(content) { measurables, constraints ->
// 先测量并获得每个子项的 parentData
val placeablesWithData = measurables.map { measurable ->
val placeable = measurable.measure(constraints)
val pd = measurable.parentData as? MyParentData ?: MyParentData(0)
placeable to pd.priority
}
// 按 priority 排序再布局
val sorted = placeablesWithData.sortedByDescending { it.second }
layout(constraints.maxWidth, constraints.maxHeight) {
var x = 0
sorted.forEach { (placeable, priority) ->
placeable.placeRelative(x, 0)
x += placeable.width
}
}
}
}
这样,我们就通过 Modifier.priority(...)
把一个自定义的"优先级"上传给父布局,并在布局阶段读取并使用它。
ParentDataModifier 的原理
在测量与布局时候起作用,且与右边最近的 LayoutModifier 存在一起,被放进了装入链表的数组:
kotlin
Modifier.then(ParentDataModifier1).then(ParentDataModifier2).then(LayoutModifier1)
.then(ParentDataModifier3).then(ParentDataModifier4).then(LayoutModifier2)
.then(ParentDataModifier5).then(ParentDataModifier6)
// 上边的代码对应的结构:
ModifiedLayoutNode(
LayoutModifier1,
[
...
ParentDataModifier1 -> ParentDataModifier2,
...
],
ModifiedLayoutNode(
LayoutModifier2,
[
...
ParentDataModifier3 -> ParentDataModifier4,
...
],
InnerPlaceable(
[
...
ParentDataModifier5 -> ParentDataModifier6
...
]
)
)
)
// 完整示例:
@Composable
@Preview
fun ParentDataModifierChainExample() {
// 定义 6 个 ParentDataModifier,分别返回不同的标记
val parentDataModifier1 = object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? = "PD1"
}
val parentDataModifier2 = object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? = "PD2"
}
val parentDataModifier3 = object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? = "PD3"
}
val parentDataModifier4 = object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? = "PD4"
}
val parentDataModifier5 = object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? = "PD5"
}
val parentDataModifier6 = object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? = "PD6"
}
// 第一个 LayoutModifier(ModifiedLayoutNode)
val layoutModifier1 = object : LayoutModifier {
override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult {
// 这里 measurabe.parentData 会是 PD1、PD2 串联的结果
Log.d("zxc LM1", "parentData = ${measurable.parentData}") // → "PD3"
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
// 第二个 LayoutModifier(Nested ModifiedLayoutNode)
val layoutModifier2 = object : LayoutModifier {
override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult {
// 这里 measurable.parentData 会是 PD3、PD4 串联的结果
Log.d("zxc LM2", "parentData = ${measurable.parentData}") // → "PD5"
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
Box(
Modifier
// → ModifiedLayoutNode(LM1) 拥有 [PDM1, PDM2]
.then(parentDataModifier1)
.then(parentDataModifier2)
.then(layoutModifier1)
// → ModifiedLayoutNode(LM2) 拥有 [PDM3, PDM4]
.then(parentDataModifier3)
.then(parentDataModifier4)
.then(layoutModifier2)
// → InnerPlaceable 拥有 [PDM5, PDM6]
.then(parentDataModifier5)
.then(parentDataModifier6)
) {
Text("看 Logcat")
}
}
// 结果:
LM1 com...n.coursecomposemodifierlayout D parentData = PD3
LM2 com...n.coursecomposemodifierlayout D parentData = PD5
源码分析
获取数据的起源就在 parentData 中
kotlin
measurables.forEach { measurable ->
// ------ 第二步:从 measurable.parentData 中取出前面设置的 Layout2Data
val data = measurable.parentData as? Layout2Data
// 然后就可以读取出两个自定义属性
val big = data?.big // 是否标记为"大"
val weight = data?.weight // 权重值
// 根据 big、weight 进行测量逻辑
...
}
一切的起源来自:androidx.compose.ui.node.LayoutNodeWrapper#parentData
kotlin
override val parentData: Any?
// 拿到装有 parentData 的链表的表头节点,然后获取 parentData
get() = entities.head(EntityList.ParentDataEntityType).parentData
// SimpleEntity 的一个拓展属性就是 parentData
private val SimpleEntity<ParentDataModifier>?.parentData: Any?
get() = if (this == null) { // 表示这个 ModifiedLayoutNode 的 ParentDataModifier 链表为空
wrapped?.parentData // 返回它的内部的 ModifiedLayoutNode 的 ParentDataModifier ,这个就是一次递归。
} else {
with(modifier) { // 头节点不为空,
/**
* ParentData provided through the parentData node will override the data provided
* through a modifier.
*/
measureScope.modifyParentData(next.parentData) // 这里就是执行自定义的函数的位置, 比如下边代码的 modifyParentData 。
// next.parentData 表示链表中的下一个节点。又是一次递归。
}
}
/* fun Modifier.priority(value: Int) = this.then(
object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any {
// parentData 可能是上一个 ParentDataModifier 的结果
return (parentData as? MyParentData)?.copy(priority = value)
?: MyParentData(value)
}
}
) */
对于这个例子:
kotlin
Modifier.then(ParentDataModifier1).then(ParentDataModifier2).then(LayoutModifier1)
.then(ParentDataModifier3).then(ParentDataModifier4).then(LayoutModifier2)
.then(ParentDataModifier5).then(ParentDataModifier6)
得出的他最终的遍历顺序就是:
kotlin
ModifiedLayoutNode
→ ParentDataModifier1
→ ParentDataModifier2
→ ModifiedLayoutNode
→ ParentDataModifier3
→ ParentDataModifier4
→ InnerPlaceable
→ ParentDataModifier5
→ ParentDataModifier6
因此,即便顺序调整之后,最终的显示效果还是一样的,也正因为是提供个父组件用的,所以下边这种写法是没有效果的:
kotlin
Box(
Modifier
.size(40.dp)
.background(Color.Green)
.padding(20.dp)
.layout { measurable, constraints -> // 无效
measurable.parentData
})
这是因为: Modifier.layout { ... }
并不是在写一个「自定义布局」Composable,它只是给当前这个 单独元素 (这里是 Box
)挂了一个 LayoutModifier
,它的 measure
回调里:
-
只能拿到这个元素自己 (
measurable
就代表Box
本身)的parentData
,而不是你想取的 Box 里子组件 的parentData
-
它的回调签名是
kotlin
MeasureScope.(measurable: Measurable, constraints: Constraints) -> MeasureResult
必须在里面调用
kotlin
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
否则整个布局根本就不会走测量/放置流程,等同于"空操作"。
小结
- 作用:在子组件的 Modifier 上携带"父布局需要的元数据",供父布局在测量/布局阶段读取使用。
- 本质 :Compose 里的 "布局参数" ------ 就像 View 中的
LayoutParams
,但更灵活可组合。 - 使用方式 :内置的
weight
、align
、baselinePadding
、gravity
等;也可以自定义ParentDataModifier
搭配自定义布局。
SemanticsModifier 的作用、写法和原理
SemanticsModifier
是 Compose 中用来为 UI 元素提供可访问性(Accessibility)以及测试标签信息的关键手段。它会在布局树中生成或修改对应的 SemanticsNode,把**语义(role、state、action、contentDescription、testTag 等)**暴露给无障碍服务(TalkBack)、UI 测试框架或其它需要"看懂"界面结构的工具。
写法
1. 使用 semantics
或高阶 API
-
最常见 :用
Modifier.semantics { ... }
kotlinBox(Modifier .size(100.dp) .semantics(/*true*/) { // 改为true之后,会与子组件进行合并,同时让自己不会被合并到外部去。 contentDescription = "用户头像" role = Role.Image stateDescription = if (selected) "已选中" else "未选中" onClick { /* 点击行为 */ true } } )
-
专用便捷 API :
Compose 也提供一些常见语义修饰符,如:
kotlinModifier.clickable { ... } // 自动添加 onClick、role=Button Modifier.toggleable(checked) { ... } // 添加 onClick, role=Switch, stateDescription Modifier.clearAndSetSemantics { // 清空下层语义,设置全新语义,绝大多数时候都不需要设置。 testTag = "LoginButton" }
2. 自定义 SemanticsModifier
如果需要更细粒度控制,可以直接实现 SemanticsModifier
接口,本质上它继承自 Modifier.Element
:
kotlin
// 1) 定义一个 PropertyKey
val CountKey = SemanticsPropertyKey<Int>("Count")
// 2) 给 SemanticsConfiguration 的接收者增加一个委托属性
var SemanticsPropertyReceiver.count by CountKey
// 3) 写个 Modifier 扩展,用官方的 semantics{} DSL
fun Modifier.countSemantics(count: Int): Modifier =
this.then(
semantics {
this.count = count
}
)
// 4) 使用示例
@Composable
fun Demo() {
Box(
Modifier
.size(80.dp)
.countSemantics(42) // 无障碍和测试就能读到 CountKey = 42
) {
Text("Tap me")
}
}
然后在 Modifier
链中使用:
kotlin
Box(Modifier.then(MySemanticsModifier("自定义标签")))
原理
kotlin
val PointerInputSource =
object : HitTestSource<PointerInputEntity, PointerInputFilter, PointerInputModifier> {
override fun entityType() = EntityList.PointerInputEntityType
@Suppress("ModifierFactoryReturnType", "ModifierFactoryExtensionFunction")
override fun contentFrom(entity: PointerInputEntity) =
entity.modifier.pointerInputFilter
override fun interceptOutOfBoundsChildEvents(entity: PointerInputEntity) =
entity.modifier.pointerInputFilter.interceptOutOfBoundsChildEvents
override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) = true
override fun childHitTest(
layoutNode: LayoutNode,
pointerPosition: Offset,
hitTestResult: HitTestResult<PointerInputFilter>,
isTouchEvent: Boolean,
isInLayer: Boolean
) = layoutNode.hitTest(pointerPosition, hitTestResult, isTouchEvent, isInLayer)
}
/**
* Hit testing specifics for semantics.
*/
val SemanticsSource =
object : HitTestSource<SemanticsEntity, SemanticsEntity, SemanticsModifier> {
override fun entityType() = EntityList.SemanticsEntityType
override fun contentFrom(entity: SemanticsEntity) = entity
override fun interceptOutOfBoundsChildEvents(entity: SemanticsEntity) = false
override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) =
parentLayoutNode.outerSemantics?.collapsedSemanticsConfiguration()
?.isClearingSemantics != true
override fun childHitTest(
layoutNode: LayoutNode,
pointerPosition: Offset,
hitTestResult: HitTestResult<SemanticsEntity>,
isTouchEvent: Boolean,
isInLayer: Boolean
) = layoutNode.hitTestSemantics(
pointerPosition,
hitTestResult,
isTouchEvent,
isInLayer
)
}
internal fun hitTest(
pointerPosition: Offset,
hitTestResult: HitTestResult<PointerInputFilter>,
isTouchEvent: Boolean = false,
isInLayer: Boolean = true
) {
val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
outerLayoutNodeWrapper.hitTest(
LayoutNodeWrapper.PointerInputSource,
positionInWrapped,
hitTestResult,
isTouchEvent,
isInLayer
)
}
@Suppress("UNUSED_PARAMETER")
internal fun hitTestSemantics( // 判断触摸的视图点在哪里。
pointerPosition: Offset,
hitSemanticsEntities: HitTestResult<SemanticsEntity>,
isTouchEvent: Boolean = true,
isInLayer: Boolean = true
) {
val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
outerLayoutNodeWrapper.hitTest(
LayoutNodeWrapper.SemanticsSource,
positionInWrapped,
hitSemanticsEntities,
isTouchEvent = true,
isInLayer = isInLayer
)
}
-
语义节点树构建
- Compose 在布局测量与放置阶段,会同时收集所有
SemanticsModifier
(及其它产生语义的 Modifier,如clickable
、toggleable
) - 它们会被包装成一个或多个
SemanticsNode
,构成一棵平行于布局树的"语义树"(AccessibilityNodeInfo
在底层映射自这里)。
- Compose 在布局测量与放置阶段,会同时收集所有
-
SemanticsConfiguration 聚合
- 每个
SemanticsModifier
都提供一个SemanticsConfiguration
,里面是键值对形式的语义属性。 - 当多个修饰符应用在同一个 UI 节点上,系统会 合并 它们的配置:同一 key 后面的会覆盖前面的,action 列表会累加。
- 每个
-
无障碍桥接
- 最终框架会把这棵语义树交给 Android Accessibility Framework(
AccessibilityNodeInfo
),或给测试库(Espresso Compose)解析。 - TalkBack、UI Automator 等工具就可以读取
contentDescription
、role
、stateDescription
、testTag
,甚至触发onClick
、customActions
等。
- 最终框架会把这棵语义树交给 Android Accessibility Framework(
-
优化与合并策略
clearAndSetSemantics { ... }
:可清空所有下层语义,防止父组件意外继承子组件的无障碍信息。- 合并策略 :如果一个容器和子项都定义了语义,默认会向上合并,生成整体的可访问性焦点区域。
小结
- 为什么要用:让 Compose UI 对无障碍读屏和自动化测试友好------提供文字描述、角色信息、状态、操作回调。
- 怎么用 :最常见是
Modifier.semantics { ... }
或者使用框架提供的clickable
、toggleable
、testTag
等便捷语义 Modifier。 - 底层原理 :Compose 会把所有
SemanticsModifier
收集进平行的"语义树",并在渲染时转换成 Android AccessibilityNodeInfo,创建真正的无障碍节点。
addBeforeLayoutModifier-() 和 addAfterLayoutModifier-() 的区别
惟一的区别就是:对于同一个Modifier,当它有多重身份的时候,应该先处理它的哪个身份?例如下边这个例子,这也是目前唯一具有双重身份的Modifier:
kotlin
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) {
但是实际上,
-
addBeforeLayoutModifier
: addBefore-() 会让⾥⾯的 Modifier 身份早于 LayoutModifier 身份被处理,导致被包含在 LayoutModifier 所处的 ModifiedLayoutNode 的更内部的⼀层,从⽽受到更内部的layoutNodeWrapper 的尺⼨限制。 -
addAfterLayoutModifier
:addAfter-() 会让⾥⾯的 Modifier 身份在 LayoutModifier 身份之后被处理,这样就和 LayoutModifier 处于同⼀个 ModifiedLayoutNode ,从⽽和 LayoutModifier 的尺⼨范围是⼀样的。
kotlin
Modifier.then(parentDataModifier1).then(parentDataModifier2).then(onRemeasuredModifier1).then(onRemeasuredModifier2).then(layoutModifier1)
.then(parentDataModifier3).then(parentDataModifier4).then(onRemeasuredModifier3).then(onRemeasuredModifier4).then(layoutModifier2)
.then(parentDataModifier5).then(parentDataModifier6).then(onRemeasuredModifier5)
// 得到的结果:
ModifiedLayoutNode(
LayoutModifier1,
[
ParentDataModifier1 -> ParentDataModifier2,
onRemeasuredModifier1 -> onRemeasuredModifier2
],
ModifiedLayoutNode(
LayoutModifier2,
[
ParentDataModifier3 -> ParentDataModifier4,
onRemeasuredModifier3 -> onRemeasuredModifier4
],
InnerPlaceable(
[
ParentDataModifier5 -> ParentDataModifier6,
onRemeasuredModifier5
]
)
)
)
OnRemeasuredModifier 的作用、写法和原理
作用
OnRemeasuredModifier
对应传统 View 的 onMeasure()
,当它修饰的 最近右侧 那个 LayoutModifier
(或最内层内容)完成测量后就会回调一次 onRemeasured(size: IntSize)
,让我们拿到测量出来的宽高信息,以便做接下来的逻辑(比如驱动动画、收集日志或更新状态)。
写法
-
直接实现
kotlinText( "Hello", Modifier .padding(20.dp) .then(object : OnRemeasuredModifier { override fun onRemeasured(size: IntSize) { // size 就是右侧 padding(...) 或者紧挨它的 LayoutModifier 测量后的最终尺寸 Log.d("Demo", "Measured size: $size") } }) .padding(40.dp) )
在上面例子里,
onRemeasured
会回调 40.dp 这个padding
测量后的大小,因为它总是与它右侧最近的那个LayoutModifier
关联。CSDN博客 -
内置封装
onSizeChanged
kotlinText( "World", Modifier.onSizeChanged { newSize -> // 只有当尺寸发生"变化"时才回调一次 println("Size changed to $newSize") } )
Modifier.onSizeChanged { ... }
底层就是一个OnSizeChangedModifier
,它实现了OnRemeasuredModifier
,并且会在每次测量完成后,只有当newSize != previousSize
时才真正触发。
原理
kotlin
internal val nodes = NodeChain(this)
internal val innerCoordinator: NodeCoordinator // 对应旧版本的InnerPlaceable
get() = nodes.innerCoordinator
internal val layoutDelegate = LayoutNodeLayoutDelegate(this)
internal val outerCoordinator: NodeCoordinator // 对应旧版本的outWrapper,最外层的装着 LayoutModifier 的 用于测量的 LayoutNodeWrapper,它便是最外层的,对应着最左边的 LayoutModifier 的 NodeCoordinator。 在新版本的 Modifier 统一都被放在了 nodes 里边了 ,而不是 放在 NodeCoordinator 的里边了,有了这种存储关系的调整,但是对应关系还是有的。一个 Node 还是对应一个 NodeCoordinator ,NodeCoordinator 依然是布局进行分层的工具。里边会关联着 LayoutModifier
NodeChain 是用来存放Modifier 的, 而且 这些 Modifier 会被封装进 一个个 Node 对象里边 ,这些 Node存放在哪里呢?就是 head 和 tail ,分别是双向链表的头节点和尾结点。下边就是 Modifier 的初始化保存原理的源码:
kotlin
private fun syncCoordinators() {
var coordinator: NodeCoordinator = innerCoordinator
var node: Modifier.Node? = tail.parent
while (node != null) {
if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
val next = if (node.isAttached) {
val c = node.coordinator as LayoutModifierNodeCoordinator
val prevNode = c.layoutModifierNode
c.layoutModifierNode = node
if (prevNode !== node) c.onLayoutModifierNodeChanged()
c
} else {
// 处理多层嵌套,与旧版本一样,处理 LayoutModifier 都需要创建 ModifierLayoutModifier 包裹 LayoutModifier 和 与他相关的 非 LayoutModifier
val c = LayoutModifierNodeCoordinator(layoutNode, node)
node.updateCoordinator(c)
c
}
coordinator.wrappedBy = next
next.wrapped = coordinator
coordinator = next
} else {
// 挂载同一个 NodeCoordinator 下边
node.updateCoordinator(coordinator)
}
node = node.parent
}
coordinator.wrappedBy = layoutNode.parent?.innerCoordinator
outerCoordinator = coordinator
}
大逻辑没有变,只是细节上的调整。syncCoordinators()
就是把
kotlin
InnerPlaceable ← Modifier.Node1 ← Modifier.Node2 ← ... ← Modifier.NodeN
这样一条 Modifier.Node 链,映射到
kotlin
InnerCoordinator ← LMCoordinator1 ← LMCoordinator2 ← ... ← LMCoordinatorM ← OuterCoordinator
这样一条 NodeCoordinator 链上,其中只有那些实现了 LayoutModifier
的节点会真正生成一个新的 LayoutModifierNodeCoordinator
,其它节点则共享它们最近的上层 Coordinator。这样就建立起了「谁在哪层测量/放置/绘制」的清晰对应关系。
onRemeasured 触发的原始逻辑有两处,逻辑几乎相同,他们都是自测量完成之后,再调用内部的onMeasured()
。 Compose的自己测量调用方式是
kotlin
override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
// before rerunning the user's measure block reset previous measuredByParent for children
layoutNode.forEachChild {
it.measuredByParent = LayoutNode.UsageByParent.NotUsed
}
measureResult = with(layoutNode.measurePolicy) {
measure(layoutNode.childMeasurables, constraints)
}
onMeasured()
return this
}
例如下边的代码:
kotlin
@Composable
@Preview
fun MyComposable() {
Text(text = "Hello World!", Modifier.onSizeChanged { })
Text("床前明月光", Modifier.then(object : OnRemeasuredModifier {
override fun onRemeasured(size: IntSize) {
Log.d("zxc1 ", "onRemeasured: size = $size")
}
}))
Text(
text = "Hello World!",
Modifier
.padding(20.dp)
// 它只和右边有关,因此在 padding(40.dp) 调用后就会触发这里。且它的 size 是只包含 padding(40.dp) 的尺寸。
.then(object : OnRemeasuredModifier {
override fun onRemeasured(size: IntSize) {
Log.d("zxc2 ", "onRemeasured: size = $size")
}
})
.padding(40.dp)
)
}
// 输出:
onRemeasured: size = 225 x 61
onRemeasured: size = 513 x 321
kotlin
internal val tail: Modifier.Node = innerCoordinator.tail
internal var head: Modifier.Node = tail
-
Compose 在内部会将每个
LayoutModifier
包装成一个ModifiedLayoutNode
,每遇到一个LayoutModifier
就会划分一个新的"布局节点层"; -
同时,所有非
LayoutModifier
(包括OnRemeasuredModifier
)会分成"Before" 或 "After" 两类链表,分别记录在LayoutNodeWrapper.entities
里。 -
当某层的
LayoutNodeWrapper.measure(...)
完成(无论是最内层的InnerPlaceable
还是某个ModifiedLayoutNode
)时,都会调用它的onMeasured()
:kotlinfun onMeasured() { if (entities.has(RemeasureEntityType)) { Snapshot.withoutReadObservation { entities.forEach(RemeasureEntityType) { it.modifier.onRemeasured(measuredSize) } } } }
这样就保证所有挂在"After LayoutModifier"阶段的 OnRemeasuredModifier
都能准确且及时地拿到 本层 测量结果
OnPlacedModifier 的作用、写法和原理
OnPlacedModifier
是一个 布局"放置"阶段 的回调入口。当你给某个 Composable 加上它,就能在这个节点完成放置(place(...)
)后、绘制前,拿到它的 LayoutCoordinates
------ 包括它在父布局坐标系/根布局坐标系中的位置、尺寸、父子层级关系等。典型用途有:
- 读出最终位置或大小,驱动后续动画;
- 把布局结果暴露给外部状态(比如保存某个控件的屏幕位置);
- 结合
LocalView
做全局坐标映射,触发业务逻辑。
写法
1. 内置扩展:Modifier.onPlaced { }
kotlin
Box(
Modifier
.size(100.dp)
.onPlaced { coords ->
// coords.positionInParent() / positionInWindow() / size / parent etc.
Log.d("Demo", "placed at ${coords.positionInWindow()}")
}
) {
/*...*/
}
2. 自定义 OnPlacedModifier
如果需要更定制化,或想把逻辑封装成复用组件,可以自己实现:
kotlin
class MyOnPlacedModifier(
val onPlacedAction: (LayoutCoordinates) -> Unit
) : OnPlacedModifier {
override fun onPlaced(coordinates: LayoutCoordinates) {
onPlacedAction(coordinates)
}
}
fun Modifier.onMyPlaced(action: (LayoutCoordinates) -> Unit): Modifier =
this.then(MyOnPlacedModifier(action))
// 使用
Box(
Modifier.onMyPlaced { coords ->
// ...
}
)
原理
kotlin
@OptIn(ExperimentalComposeUiApi::class)
fun onPlaced() {
val lookahead = lookaheadDelegate
if (lookahead != null) {
visitNodes(Nodes.LayoutAware) {
it.onLookaheadPlaced(lookahead.lookaheadLayoutCoordinates)
}
}
visitNodes(Nodes.LayoutAware) { // 遍历layoutNode那一根保存好 Node 的双链表,只会去遍历指定的类型(Nodes.LayoutAware)。
it.onPlaced(this)
}
}
-
Modifier 链到
LayoutNodeWrapper
Compose 会把所有实现了
LayoutModifier
、OnPlacedModifier
、OnRemeasuredModifier
等接口的Modifier.Element
,收集到内部的LayoutNodeWrapper
上。 -
测量阶段
每一层
LayoutNodeWrapper.measure(...)
负责给子节点测量尺寸。 -
放置阶段
在最内层完成
place()
并返回后,框架沿着LayoutNodeWrapper
链 从内到外 调用:kotlin// 伪代码示意 wrapper.placeChildren { childPlaceable -> childPlaceable.place(...) } // 放置完本层所有子项后 if (wrapper.entities.has(PositioningEntityType)) { wrapper.entities.forEach(PositioningEntityType) { entity -> (entity.modifier as OnPlacedModifier) .onPlaced(wrapper.coordinates) } }
也就是说:所有挂在这一层的
OnPlacedModifier
(以及它们的扩展接口如onPlacement
,onPositioned
)都会在本层放置结束后被回调一次。
小结
- 何时用 :当你需要知道一个 Composable 最终"被放到哪里"或"占了多大区间"时,用
onPlaced
拦截。 - 用法简单 :
Modifier.onPlaced { coords -> ... }
或自定义OnPlacedModifier
。 - 底层原理 :属于 PositioningModifier 一类,Compose 在放置流程后自动遍历并 invoke。