Compose原理九之测量布局

一、前言

kotlin 复制代码
Column(modifier = Modifier.background(Color.Red).padding(10.dp).offset(5.dp)) {
    Text("1")
    Text("2")
}

本文将以上面的代码为例,详细详解组件和修饰符的测量和布局。组件包含ColumnText,修饰符包括backgroundpaddingoffset

示例代码会生成下面的结构,可以看到,修饰符也包含在结构中。这也就意味着,修饰符也是树中的节点。

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

注意:PaddingCoordinatorOffsetCoordinator并不是源码中的类,其实它们是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是谁?outerCoordinatorLayoutModifierNodeCoordinator对象,在开头的示例代码中,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.measureOffsetCoordinator拿到的是被Padding减小后的约束。

BackgroundNodeDrawModifierNode,不参与测量过程。

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()。在 PaddingNodemeasure 方法中,我们看到它返回的 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()

OffsetNodemeasure 方法中,我们看到它返回的 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来触发。
  • 先调用修饰符的测量方法,后调用组件的测量方法,由于是递归调用,最终组件先拿到宽高,修饰符后拿到宽高,修饰符的宽高其实就是组件的宽高。
  • 布局的时候,先放置修饰符,然后放置组件。
相关推荐
想用offer打牌2 小时前
一站式了解接口防刷(限流)的基本操作
java·后端·架构
张二森2 小时前
分布式存储的战争(四)AI的咆哮-GPFS/Deepseek 3FS 并行文件系统
架构
白太岁6 小时前
Muduo:(3) 线程的封装,线程 ID 的获取、分支预测优化与信号量同步
c++·网络协议·架构·tcp
AxureMost6 小时前
产品经理:业务架构、应用架构与数据架构
架构·产品经理
白太岁6 小时前
Muduo:(0) 架构与接口总览
c++·架构·tcp
小程故事多_807 小时前
深度解析个人AI助手OpenClaw:从消息处理到定时任务的全流程架构
人工智能·架构
Coder_Boy_8 小时前
Java高级_资深_架构岗 核心知识点——高并发模块(底层+实践+最佳实践)
java·开发语言·人工智能·spring boot·分布式·微服务·架构
AC赳赳老秦8 小时前
2026 AI原生开发工具链趋势:DeepSeek与主流IDE深度联动实践指南
运维·ide·人工智能·架构·prometheus·ai-native·deepseek
2501_926978339 小时前
大模型“脱敏--加密”--“本地轻头尾运算--模型重运算”
人工智能·经验分享·架构