一、前言
kotlin
Column(modifier = Modifier.background(Color.Red).padding(10.dp).offset(5.dp)) {
Text("1")
Text("2")
}
本文将以上面的代码为例,详细详解组件和修饰符的测量和布局。组件包含Column、Text,修饰符包括background、padding、offset。
示例代码会生成下面的结构,可以看到,修饰符也包含在结构中。这也就意味着,修饰符也是树中的节点。
text
├── LayoutModifierNodeCoordinator (padding + background Modifier)
│ └── LayoutModifierNodeCoordinator (offset Modifier)
│ └── InnerNodeCoordinator (Column 本身)
│ ├── LayoutNode (Text("1"))
│ └── LayoutNode (Text("2"))
二、修饰符
在Compose原理八之修饰符一文中,我们介绍了修饰符的构建过程,再来回顾下。
- 修饰符的链式调用实际是生成了一棵树,对象不可变,保证线程安全。
- 将树形结构展平成双向链表。
- 将链表变成Coordinator。
示例代码的修饰符就会变成这样:
scss
Modifier
.background(Red) // DrawModifier -> 依附最近的 Coordinator
.padding(10.dp) // LayoutModifier -> 产生 Coordinator
.offset(5.dp) // LayoutModifier -> 产生 Coordinator
生成的链表 :
rust
Head -> BackgroundNode -> PaddingNode -> OffsetNode -> Tail
生成的 Coordinator :
scss
LayoutNode (整体)
|
+-- PaddingCoordinator (最外层)
|
+-- OffsetCoordinator (中间层)
|
+-- InnerCoordinator (最内层)
绑定关系 (谁依附谁) :
- BackgroundNode 没有自己的 Coordinator,依附于
PaddingCoordinator(因为它是画在 Padding 层里的)。 - PaddingNode 绑定的 Coordinator 是
PaddingCoordinator。 - OffsetNode 绑定的 Coordinator 是
OffsetCoordinator。
注意:PaddingCoordinator、OffsetCoordinator并不是源码中的类,其实它们是LayoutModifierNodeCoordinator对象,只是为了区分,这里取了不同的名字。
三、测量
测量流程是一个深度优先遍历的过程。请求从 AndroidComposeView.onMeasure发出,在onMeasure里面创建Compose需要的约束对象,通过MeasureAndLayoutDelegate 传递给LayoutNode ,通过 LayoutNode 传递给 MeasurePassDelegate,再进入 NodeCoordinator 链。
3、1 原生onMeasure
当安卓系统对 View 树进行测量时,会调用 AndroidComposeView.onMeasure。Compose测量的约束对象就是在onMeasure里面创建的。
kotlin
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
trace("AndroidOwner:onMeasure") {
if (!isAttachedToWindow) {
invalidateLayoutNodeMeasurement(root)
}
// 1. 将 Android 的 MeasureSpec 转换为 min/max 尺寸
val (minWidth, maxWidth) = convertMeasureSpec(widthMeasureSpec)
val (minHeight, maxHeight) = convertMeasureSpec(heightMeasureSpec)
// 2. 创建 Compose 的 Constraints 对象
// fitPrioritizingHeight 是一种优先满足高度约束的策略
val constraints = Constraints.fitPrioritizingHeight(
minWidth = minWidth,
maxWidth = maxWidth,
minHeight = minHeight,
maxHeight = maxHeight,
)
// ... (处理 onMeasureConstraints 逻辑,判断是否多次测量)
// 3. 更新根节点的约束并触发测量
measureAndLayoutDelegate.updateRootConstraints(constraints)
measureAndLayoutDelegate.measureOnly()
// 4. 设置 View 自身的尺寸(反馈给 Android 系统)
setMeasuredDimension(root.width, root.height)
// ... (测量 AndroidViewsHandler,如果有的话)
}
}
3、2 原生 onLayout
当系统对View树进行布局时,会调用AndroidComposeView.onLayout。
kotlin
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
lastMatrixRecalculationAnimationTime = 0L
// 1. 触发 Compose 的布局流程(测量和放置)
measureAndLayoutDelegate.measureAndLayout(resendMotionEventOnLayout)
onMeasureConstraints = null
// 2. 更新位置缓存并分发 OnGloballyPositioned 回调
updatePositionCacheAndDispatch()
// 3. 布局 AndroidView 子视图
if (_androidViewsHandler != null) {
androidViewsHandler.layout(0, 0, r - l, b - t)
}
}
3、3 measureAndLayoutDelegate.measureAndLayout
MeasureAndLayoutDelegate 负责管理所有 LayoutNode 的测量和布局请求。
这是整个布局流程的核心驱动方法。它会循环处理待测量和待布局的节点,直到所有节点都处于"干净"状态。
kotlin
// MeasureAndLayoutDelegate.kt
fun measureAndLayout(onLayout: (() -> Unit)? = null): Boolean {
var rootNodeResized = false
performMeasureAndLayout(fullPass = true) {
if (relayoutNodes.isNotEmpty()) {
relayoutNodes.popEach { layoutNode, affectsLookahead, relayoutNeeded ->
val sizeChanged =
remeasureAndRelayoutIfNeeded(layoutNode, affectsLookahead, relayoutNeeded)
if (!relayoutNeeded) {
if (layoutNode.lookaheadLayoutPending) {
relayoutNodes.add(layoutNode, Invalidation.LookaheadPlacement)
}
if (layoutNode.layoutPending) {
relayoutNodes.add(layoutNode, Invalidation.Placement)
}
}
if (layoutNode === root && sizeChanged) {
rootNodeResized = true
}
}
onLayout?.invoke()
}
}
callOnLayoutCompletedListeners()
return rootNodeResized
}
3、4 remeasureAndRelayoutIfNeeded
kotlin
private fun remeasureAndRelayoutIfNeeded(
layoutNode: LayoutNode,
affectsLookahead: Boolean = true,
relayoutNeeded: Boolean = true,
): Boolean {
var sizeChanged = false
if (layoutNode.isDeactivated) {
// we don't remeasure or relayout deactivated nodes.
return false
}
if (
layoutNode.isPlaced || // the root node doesn't have isPlacedByParent = true
layoutNode.isPlacedByParent ||
layoutNode.canAffectPlacedParent ||
layoutNode.isPlacedInLookahead == true ||
layoutNode.canAffectParentInLookahead ||
layoutNode.alignmentLinesRequired
) {
val constraints = if (layoutNode === root) rootConstraints!! else null
if (affectsLookahead) {
if (layoutNode.lookaheadMeasurePending) {
sizeChanged = doLookaheadRemeasure(layoutNode, constraints)
}
if (relayoutNeeded) {
if (
(sizeChanged || layoutNode.lookaheadLayoutPending) &&
layoutNode.isPlacedInLookahead == true
) {
layoutNode.lookaheadReplace()
}
}
} else {
if (layoutNode.measurePending) {
// 测量
sizeChanged = doRemeasure(layoutNode, constraints)
}
if (relayoutNeeded) {
if (layoutNode.layoutPending) {
val isPlacedByPlacedParent =
layoutNode === root ||
(layoutNode.parent?.isPlaced == true && layoutNode.isPlacedByParent)
if (isPlacedByPlacedParent) {
if (layoutNode === root) {
// 布局
layoutNode.place(0, 0)
} else {
layoutNode.replace()
}
onPositionedDispatcher.onNodePositioned(layoutNode)
layoutNode.requireOwner().rectManager.invalidateCallbacksFor(layoutNode)
consistencyChecker?.assertConsistent()
}
}
}
}
drainPostponedMeasureRequests()
}
return sizeChanged
}
3、5 layoutNode.remeasure
当一个节点需要测量时,MeasureAndLayoutDelegate 会调用LayoutNode.remeasure(constraints)。
kotlin
// LayoutNode.kt
internal fun remeasure(constraints: Constraints? = layoutDelegate.lastConstraints): Boolean {
return if (constraints != null) {
if (intrinsicsUsageByParent == UsageByParent.NotUsed) {
// This LayoutNode may have asked children for intrinsics. If so, we should
// clear the intrinsics usage for everything that was requested previously.
clearSubtreeIntrinsicsUsage()
}
measurePassDelegate.remeasure(constraints)
} else {
false
}
}
3、6 MeasurePassDelegate.remeasure
kotlin
// MeasurePassDelegate.kt
fun remeasure(constraints: Constraints): Boolean {
withComposeStackTrace(layoutNode) {
// ... (前置检查)
// 检查是否需要重新测量
if (layoutNode.measurePending || measurementConstraints != constraints) {
// ... (状态重置)
measuredOnce = true
val outerPreviousMeasuredSize = outerCoordinator.size
measurementConstraints = constraints
// 1. 执行具体的测量逻辑
performMeasure(constraints)
// 2. 检查尺寸是否发生变化
val sizeChanged =
outerCoordinator.size != outerPreviousMeasuredSize ||
outerCoordinator.width != width ||
outerCoordinator.height != height
// 更新缓存的尺寸
measuredSize = IntSize(outerCoordinator.width, outerCoordinator.height)
return sizeChanged
} else {
// ... (强制子树测量逻辑)
}
return false
}
}
3、7 MeasurePassDelegate.performMeasure
kotlin
// MeasurePassDelegate.kt
internal fun performMeasure(constraints: Constraints) {
// ... (状态检查)
performMeasureConstraints = constraints
layoutState = LayoutState.Measuring
measurePending = false
// 使用 snapshotObserver 观察测量过程中的状态读取
// 这意味着在 measure 块中读取的任何 State 发生变化时,都会触发重新测量
layoutNode
.requireOwner()
.snapshotObserver
.observeMeasureSnapshotReads(layoutNode, affectsLookahead = false, performMeasureBlock)
// 3. 测量后的状态处理
// 如果测量过程没有改变 layoutState (仍为 Measuring),说明测量正常完成
// 将其标记为 LayoutPending,准备进入布局阶段
if (layoutState == LayoutState.Measuring) {
markLayoutPending()
layoutState = LayoutState.Idle
}
}
// 这里的 performMeasureBlock 定义为:
private val performMeasureBlock: () -> Unit = {
// 核心调用:启动 Coordinator 链的测量
outerCoordinator.measure(performMeasureConstraints)
}
outerCoordinator.measure启动 Coordinator 链的测量。
3、8 outerCoordinator.measure
outerCoordinator是谁?outerCoordinator是LayoutModifierNodeCoordinator对象,在开头的示例代码中,padding和offset都生成了各自的LayoutModifierNodeCoordinator对象,outerCoordinator其实就是padding的LayoutModifierNodeCoordinator对象。
不管哪个LayoutModifierNodeCoordinator对象都是执行下面的方法,只不过this@LayoutModifierNodeCoordinator.wrappedNonNull对象不一样。
kotlin
// LayoutModifierNodeCoordinator.kt (源码简化)
override fun measure(constraints: Constraints): Placeable {
// 1. 调用 performingMeasure 进行测量上下文设置
performingMeasure(constraints) {
// 2. 委托给对应的 Modifier Node 进行测量
with(layoutModifierNode) {
// 这里调用的是 PaddingNode.measure
measure(
// 参数1: measurable (即下一个 Coordinator, OffsetCoordinator)
this@LayoutModifierNodeCoordinator.wrappedNonNull,
// 参数2: constraints (父容器传入的约束)
constraints
)
}
}
// 3. 测量完成,返回自己 (Coordinator 也是 Placeable)
return this
}
3、9 Padding的测量
measurable参数其实是Offset的LayoutModifierNodeCoordinator对象。PaddingNode 调用 measurable.measure,实际上是调用 OffsetCoordinator.measure。OffsetCoordinator拿到的是被Padding减小后的约束。
BackgroundNode 是 DrawModifierNode,不参与测量过程。
kotlin
// PaddingModifier.kt (源码简化)
override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult {
val horizontal = start.roundToPx() + end.roundToPx() // 10dp + 10dp = 20dp
val vertical = top.roundToPx() + bottom.roundToPx() // 10dp + 10dp = 20dp
// 1. 修改约束:减去 padding 的大小
val contentConstraints = constraints.offset(-horizontal, -vertical)
// 2. 递归调用:测量下一个节点 (OffsetCoordinator),placeable存储的是Column的宽高
val placeable = measurable.measure(contentConstraints)
// 当所有的修饰符和组件都测量完成后,才会执行下面的代码
// 3. 确定自己的大小:内容大小 + padding
val width = placeable.width + horizontal
val height = placeable.height + vertical
// 4. 返回 MeasureResult,定义放置逻辑
return layout(width, height) {
// 这里的逻辑会在放置阶段执行
placeable.place(x = paddingStart, y = paddingTop)
}
}
3、10 OffsetNode
measurable参数其实是的InnerNodeCoordinator对象。OffsetNode 调用 measurable.measure,实际上是调用 InnerNodeCoordinator.measure。
kotlin
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
// 不做任何修改,直接调用,placeable存储的是Column的宽高
val placeable = measurable.measure(constraints)
// 返回 MeasureResult,定义放置逻辑
return layout(placeable.width, placeable.height) {
// 这里的逻辑会在放置阶段执行
if (rtlAware) {
placeable.placeRelative(x.roundToPx(), y.roundToPx())
} else {
placeable.place(x.roundToPx(), y.roundToPx())
}
}
}
3、11 Column的测量
layoutNode就是Column,调用 ColumnMeasurePolicy.measure(),ColumnMeasurePolicy.measure()
kotlin
// InnerNodeCoordinator.kt
override fun measure(constraints: Constraints): Placeable {
@Suppress("NAME_SHADOWING")
val constraints =
if (forceMeasureWithLookaheadConstraints) {
lookaheadDelegate!!.constraints
} else {
constraints
}
return performingMeasure(constraints) {
// before rerunning the user's measure block reset previous measuredByParent for
// children
layoutNode.forEachChild {
it.measurePassDelegate.measuredByParent = LayoutNode.UsageByParent.NotUsed
}
// layoutNode就是Column
measureResult =
with(layoutNode.measurePolicy) { measure(layoutNode.childMeasurables, constraints) }
onMeasured()
this
}
}
ColumnMeasurePolicy.measure() 会遍历其子项(Text("1") 和 Text("2")),并调用它们的 measure() 方法。
kotlin
// RowColumnMeasurePolicy.kt
internal fun RowColumnMeasurePolicy.measure(...) {
// ...
// First measure children with zero weight.
for (i in startIndex until endIndex) {
val child = measurables[i]
val parentData = child.rowColumnParentData
val weight = parentData.weight
if (weight > 0f) {
// ...
} else {
// ...
val placeable =
placeables[i]
?: child.measure(
// Ask for preferred main axis size.
createConstraints(
mainAxisMin = 0,
crossAxisMin = crossAxisDesiredSize ?: 0,
mainAxisMax = ...,
crossAxisMax = crossAxisDesiredSize ?: crossAxisMax,
)
)
// ...
placeables[i] = placeable
}
}
// ...
}
3、12 Text的测量
Text 的测量过程与 Column 类似,最终会确定其尺寸。
先调用修饰符的测量方法,后调用组件的测量方法,由于是递归调用,所以最终组件先拿到宽高,修饰符后拿到宽高,修饰符的宽高其实就是组件的宽高。
3、13 layout
测量完成后,会调用小写的layout方法,该方法返回MeasureResult对象,MeasureResult记录了组件的宽高。当调用placeChildren,就会进行摆放。
kotlin
fun layout(
width: Int,
height: Int,
alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
rulers: (RulerScope.() -> Unit)? = null,
placementBlock: Placeable.PlacementScope.() -> Unit,
): MeasureResult {
checkMeasuredSize(width, height)
return object : MeasureResult {
override val width = width
override val height = height
override val alignmentLines = alignmentLines
override val rulers = rulers
override fun placeChildren() {
// 摆放子组件
if (this@MeasureScope is LookaheadCapablePlaceable) {
placementScope.placementBlock()
} else {
SimplePlacementScope(width, layoutDirection, density, fontScale)
.placementBlock()
}
}
}
}
四、布局
4、1 Padding的布局
padding 的 LayoutModifierNodeCoordinator.placeAt() 会调用 offset 的 LayoutModifierNodeCoordinator.placeAt()。在 PaddingNode 的 measure 方法中,我们看到它返回的 layout 块中调用了 placeable.placeRelative(start.roundToPx(), top.roundToPx()),即 (10dp, 10dp)。这意味着 padding 会将 offset 放置在相对于自己的 (10dp, 10dp) 位置。
kotlin
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val horizontal = start.roundToPx() + end.roundToPx()
val vertical = top.roundToPx() + bottom.roundToPx()
// 测量完成后,placeable其实是offset的LayoutModifierNodeCoordinator对象
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
return layout(width, height) {
if (rtlAware) {
// 调用 offset的LayoutModifierNodeCoordinator.placeAt()。
placeable.placeRelative(start.roundToPx(), top.roundToPx())
} else {
placeable.place(start.roundToPx(), top.roundToPx())
}
}
}
}
4、2 Padding的布局
offset 的 LayoutModifierNodeCoordinator.placeAt() 与 padding 的类似,会调用 Column 的 InnerNodeCoordinator.placeAt()。
在 OffsetNode 的 measure 方法中,我们看到它返回的 layout 块中调用了 placeable.placeRelative(x.roundToPx(), y.roundToPx()),即 (5dp, 5dp)。这意味着 offset 会将 Column 放置在相对于自己的 (5dp, 5dp) 位置。
15dp 坐标的由来:
- padding 将 offset 放置在 (10dp, 10dp)
- offset 将 Column 放置在 (5dp, 5dp)
- 最终 Column 相对于最外层 padding 的位置是 (10dp + 5dp, 10dp + 5dp) = (15dp, 15dp)
kotlin
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
if (rtlAware) {
placeable.placeRelative(x.roundToPx(), y.roundToPx())
} else {
placeable.place(x.roundToPx(), y.roundToPx())
}
}
}
4、3 Column 的布局
Column 的 InnerNodeCoordinator.placeAt() 会调用 onAfterPlaceAt()。
kotlin
// InnerNodeCoordinator.kt
private fun onAfterPlaceAt() {
// ...
layoutNode.measurePassDelegate.onNodePlaced()
}
onNodePlaced() 会调用 layoutChildren()。
kotlin
// MeasurePassDelegate.kt
internal fun onNodePlaced() {
// ...
if (!layingOutChildren) {
layoutChildren()
}
}
layoutChildren() 会调用 measureResult.placeChildren(),即 ColumnMeasurePolicy.placeHelper()。
kotlin
// Column.kt
override fun placeHelper(...) {
return with(measureScope) {
layout(crossAxisLayoutSize, mainAxisLayoutSize) {
placeables.forEachIndexed { i, placeable ->
val crossAxisPosition = ...
placeable.place(crossAxisPosition, mainAxisPositions[i])
}
}
}
}
ColumnMeasurePolicy.placeHelper() 会遍历其子项,并调用它们的 place() 方法。
4、4 Text的布局
Text 的布局过程与 Column 类似,最终会确定其在屏幕上的位置。
五、自定义布局
- 自定义布局需要调用大写的layout函数,遍历所有的子组件,测量每个子组件,子组件的宽高受到父组件约束,测量子组件的时候需要传入父组件的约束。
- 拿到所有子组件的宽高,就能知道父组件的宽高了。
- 调用小写的layout函数,放置子组件。
kotlin
@Composable
fun CustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val padding = Modifier.padding(16.dp).background(Color.Red)
Layout(
content = content,
modifier = modifier,
measurePolicy = { measurables, constraints ->
// 1. 测量所有子组件
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 2. 计算自己的尺寸
val width = constraints.maxWidth
val height = placeables.sumOf { it.height }
// 3. 返回测量结果和放置逻辑
layout(width, height) {
// 4. 放置子组件
var y = 0
placeables.forEach { placeable ->
placeable.place(0, y)
y += placeable.height
}
}
}
)
}
六、总结
- Compose的测量布局还是由原生的onMeasure、onLayou来触发。
- 先调用修饰符的测量方法,后调用组件的测量方法,由于是递归调用,最终组件先拿到宽高,修饰符后拿到宽高,修饰符的宽高其实就是组件的宽高。
- 布局的时候,先放置修饰符,然后放置组件。