深入浅出 Compose 测量机制

作者:Pika日期:2025/10/9

自从换了新工作后,好久没有写博客了,今天终于能有时间写点东西,Compose作为Android新一代UI框架,已经得到了很多公司的认可,未来市场对Compose的要求也逐步提高。如果大家对Compose有兴趣,也欢迎后台私信我,字节移动OS招聘Compose框架的二次定制开发的Android小伙伴,一起把Compose做大做强吧!

UI框架的测量流程

对于UI框架来说,测量布局与绘制可谓是非常重要的三个话题,对于Compose来说也不例外,本章我们将从着Compose的原理出发,来聊一下最重要的测量流程。

测量流程决定了一个UI元素的最终大小,在Android View 体系中,开发者可以重写View体系的onMeasure方法,传递限制给子View以及决定自身的大小,最终完成整体的UI树大小确定。在测量流程中,我们涉及到一个关键的概念,限制(Constraint)以及最终大小的测量结果(MeasureResult)。不同的ViewGroup或者View有不同的对于子View与自身的测量方式,Compose也不例外,下面我们一步步看其如何实现这两个关键概念。

Compose的UI树

本文基于Compose最新的1.9.x版本讨论。在进一步深入测量机制之前,我们需要聊一下Compose的UI树结构,大家才能明白测量机制是如何作用的。

在Compose中,如果一个Composable基于Layout方法定义UI的表现时,那么它会生成一个叫做LayoutNode的数据结构

1@Suppress("ComposableLambdaParameterPosition")
2@UiComposable
3@Composable
4inline fun Layout(
5    content: @Composable @UiComposable () -> Unit,
6    modifier: Modifier = Modifier,
7    measurePolicy: MeasurePolicy,
8) {
9    val compositeKeyHash = currentCompositeKeyHashCode.hashCode()
10    val localMap = currentComposer.currentCompositionLocalMap
11    val materialized = currentComposer.materialize(modifier)
12    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
13        factory = ComposeUiNode.Constructor,
14        update = {
15set(measurePolicy, SetMeasurePolicy)
16            set(localMap, SetResolvedCompositionLocals)
17            set(compositeKeyHash, SetCompositeKeyHash)
18            set(materialized, SetModifier)
19        } ,
20        content = content,
21    )
22}
23

以Box举例子,Box内部其实就是通过Layout方法调用的。当然其他的Row或者Column,包括Text等,最终都是调用的Layout方法

1inline fun Box(
2    modifier: Modifier = Modifier,
3    contentAlignment: Alignment = Alignment.TopStart,
4    propagateMinConstraints: Boolean = false,
5    content: @Composable BoxScope.() -> Unit,
6) {
7    val measurePolicy = maybeCachedBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
8    Layout(
9        content = { BoxScopeInstance.content() } ,
10        measurePolicy = measurePolicy,
11        modifier = modifier,
12    )
13}
14

因此我们在写一段Compose代码的时候,最终的产物其实是一颗组织为已LayoutNode为结点的UI树

1Box(xxx) {
2Box(xxx){
3      ....
4    }
5    Text(xxx)
6   
7} 
8

LayoutNode 这个数据结构是Compose中最核心的数据类,它几乎贯穿了整个Compose的运行时机制

1internal class LayoutNode(
2    // Virtual LayoutNode is the temporary concept allows us to a node which is not a real node,
3    // but just a holder for its children - allows us to combine some children into something we
4    // can subcompose in(LayoutNode) without being required to define it as a real layout - we
5    // don't want to define the layout strategy for such nodes, instead the children of the
6    // virtual nodes will be treated as the direct children of the virtual node parent.
7    // This whole concept will be replaced with a proper subcomposition logic which allows to
8    // subcompose multiple times into the same LayoutNode and define offsets.
9    private val isVirtual: Boolean = false,
10    // The unique semantics ID that is used by all semantics modifiers attached to this LayoutNode.
11    // TODO(b/281907968): Implement this with a getter that returns the compositeKeyHash.
12override var semanticsId: Int = generateSemanticsId(),
13) :
14    ComposeNodeLifecycleCallback,
15    Remeasurement,
16    OwnerScope,
17    LayoutInfo,
18    SemanticsInfo,
19    ComposeUiNode,
20    InteroperableComposeUiNode,
21    Owner.OnLayoutCompletedListener
22

与View系统不一样的是,Compose中能够影响测量结果的不仅仅有LayoutNode的测量策略,还有Modifier也能影响测量的结果。能够影响测量结果的Modifier,在Compose中都实现了一个叫LayoutModifierNode的接口

1interface LayoutModifierNode : DelegatableNode {
2   
3fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult
4    ... 
5

比如常见的Padding,最终生成的Modifer会被转换为一个叫Modifier.Node的数据结构,其中PaddingNode就实现了LayoutModifierNode。

1private class PaddingNode(
2    var start: Dp = 0.dp,
3    var top: Dp = 0.dp,
4    var end: Dp = 0.dp,
5    var bottom: Dp = 0.dp,
6    var rtlAware: Boolean,
7) : LayoutModifierNode, Modifier.Node()
8

那么LayoutModifierNode有什么特殊之处呢?它又是怎么影响测量结果的呢?

在Compose中,我们针对每一个Composable函数修饰的Modifier,都理解为一系列的Modifier.Node,至今Modifier有21个Modifier类型,它定义在NodeKind这里类中。这里我们可以理解Compose把一系列UI能力进行了拆分,每一个Modifier都有着自己的类型

1internal object Nodes {
2    @JvmStatic
3    inline val Any
4        get() = NodeKind<Modifier.Node>(0b1 shl 0)
5
6    @JvmStatic
7    inline val Layout
8        get() = NodeKind<LayoutModifierNode>(0b1 shl 1)
9
10    @JvmStatic
11    inline val Draw
12        get() = NodeKind<DrawModifierNode>(0b1 shl 2)
13
14    @JvmStatic
15    inline val Semantics
16        get() = NodeKind<SemanticsModifierNode>(0b1 shl 3)
17
18    @JvmStatic
19    inline val PointerInput
20        get() = NodeKind<PointerInputModifierNode>(0b1 shl 4)
21
22    @JvmStatic
23    inline val Locals
24        get() = NodeKind<ModifierLocalModifierNode>(0b1 shl 5)
25
26    @JvmStatic
27    inline val ParentData
28        get() = NodeKind<ParentDataModifierNode>(0b1 shl 6)
29    ....
30

那么LayoutNode与LayoutNode之间的数据数据结构就变成了下面这个图

这里我们其实会发现,很多Modifier其实并不会对测量流程产生影响,也不会影响最终的布局展示,如果我们想要对整个树的测量相关的能力进行设计,就会发现很多Modifer.Node的遍历其实是无意义的,因此提出了一个叫NodeCoordinator的概念,它非常抽象,中文来说就是Node协同器,它分为两种InnerNodeCoordinator 与LayoutModifierNodeCoordinator。LayoutModifierNodeCoordinator 是遇到一个LayoutModifierNode就会生成一个,而InnerNodeCoordinator则是每个LayoutNode创建时都会创建一个且仅有一个

在多个Node的情况下,Compose构建了一颗如下的UI树:

NodeCoordinator 会负责管理一系列的Node,每个Node都会持有一个NodeCoordinator

1fun syncCoordinators() {
2    var coordinator: NodeCoordinator = innerCoordinator
3    var node: Modifier.Node? = tail.parent
4    while (node != null) {
5        val layoutmod = node.asLayoutModifierNode()
6        if (layoutmod != null) {
7            val next =
8                if (node.coordinator != null) {
9                    val c = node.coordinator as LayoutModifierNodeCoordinator
10                    val prevNode = c.layoutModifierNode
11                    c.layoutModifierNode = layoutmod
12                    if (prevNode !== node) c.onLayoutModifierNodeChanged()
13                    c
14                } else {
15                    val c = LayoutModifierNodeCoordinator(layoutNode, layoutmod)
16                    node.updateCoordinator(c)
17                    c
18                }
19            coordinator.wrappedBy = next
20            next.wrapped = coordinator
21            coordinator = next
22        } else {
23            node.updateCoordinator(coordinator)
24        }
25        node = node.parent
26    }
27    coordinator.wrappedBy = layoutNode.parent?.innerCoordinator
28    outerCoordinator = coordinator
29}
30

明白了这个数据结构之后,我们可以进行测量机制的探索了

测量机制

Compose中,测量的主要流程分为限制(Constraint)传递测量结果(MeasureResult)的产生,在Compose中,父节点(这里父节点角色时是NodeCoordinator)传递限制给子节点,子节点在满足限制的条件下产生最终的测量结果。

这里有一个非常关键的信息,子节点在满足限制的条件下产生最终的测量结果,即子节点自身的结果有可能并非最终的结果,这个限制在NodeCoordinator measure时保证

1 /** The measured size of this Placeable. This might not respect [measurementConstraints]. */
2protected var measuredSize: IntSize = IntSize(0, 0)
3    set(value) {
4        if (field != value) {
5            field = value
6            onMeasuredSizeChanged()
7        }
8    }
9
10private fun onMeasuredSizeChanged() {
11    width =
12       measuredSize. width . coerceIn (
13measurementConstraints. minWidth ,
14measurementConstraints. maxWidth ,
15)
16    height =
17       measuredSize. height . coerceIn (
18measurementConstraints. minHeight ,
19measurementConstraints. maxHeight ,
20)
21    apparentToRealOffset =
22        IntOffset((width - measuredSize.width) / 2, (height - measuredSize.height) / 2)
23}
24

比如下面这个Modifier链条(假如父节点传递的限制为0 - Infinity),最终的结果是300.dp,而不是150.dp,因为width传递的限制为(min:300.dp,max:300.dp),因此第二个width设置了150.dp其实也不满足父Constraint的限制从而被强制设置为300.dp

1Modifer.width(300.dp).width(150.dp)
2

因此,如果我们想要强制修改某个Modifer的具体数值,比如希望width为150生效,最好的方式就是修改传递给当前节点的子节点的限制,让其不跟随父节点的限制即可,比如requiredWidth的实现如下:

1fun Modifier.requiredWidth(width: Dp) =
2    this.then(
3        SizeElement(
4            minWidth = width,
5            maxWidth = width,
6           enforceIncoming = false, 
7            inspectorInfo =
8                debugInspectorInfo {
9name = "requiredWidth"
10                    value = width
11                } ,
12        )
13    )
14
1override fun MeasureScope.measure(
2    measurable: Measurable,
3    constraints: Constraints,
4): MeasureResult {
5    val wrappedConstraints =
6        targetConstraints.let { targetConstraints ->
7 if (enforceIncoming) { 
8 constraints. constrain (targetConstraints) 
9 } else { 
10 val resolvedMinWidth =
11 if (minWidth. isSpecified ) { 
12 targetConstraints.minWidth
13 } else { 
14 constraints.minWidth. fastCoerceAtMost (targetConstraints.maxWidth) 
15                    }
16                val resolvedMaxWidth =
17                    if (maxWidth.isSpecified) {
18                        targetConstraints.maxWidth
19                    } else {
20                        constraints.maxWidth.fastCoerceAtLeast(targetConstraints.minWidth)
21                    }
22                val resolvedMinHeight =
23                    if (minHeight.isSpecified) {
24                        targetConstraints.minHeight
25                    } else {
26                        constraints.minHeight.fastCoerceAtMost(targetConstraints.maxHeight)
27                    }
28                val resolvedMaxHeight =
29                    if (maxHeight.isSpecified) {
30                        targetConstraints.maxHeight
31                    } else {
32                        constraints.maxHeight.fastCoerceAtLeast(targetConstraints.minHeight)
33                    }
34                Constraints(
35                    resolvedMinWidth,
36                    resolvedMaxWidth,
37                    resolvedMinHeight,
38                    resolvedMaxHeight,
39                )
40            }
41        } 
42

这里我们可以总结一下,Compose中子节点一定会在满足 父节点 的限制下决定自身的测量结果大小,因此如果想要修改某个固定的大小,唯一的做法就是在限制传递过程之中修改限制本身

在大部分情况下,比如Box或者Column的MeasurePolicy中,都会将父节点传递的最小限制修改为0,从而让子节点可以确定自身的限制

1 /**
2* Copies the existing [Constraints], setting [minWidth] and [minHeight] to 0, and preserving
3* [maxWidth] and [maxHeight] as-is.
4*/
5inline fun copyMaxDimensions() = Constraints(value and MaxDimensionsAndFocusMask)
6
7private data class BoxMeasurePolicy(
8    private val alignment: Alignment,
9    private val propagateMinConstraints: Boolean,
10) : MeasurePolicy {
11    override fun MeasureScope.measure(
12        measurables: List<Measurable>,
13        constraints: Constraints,
14    ): MeasureResult {
15        if (measurables.isEmpty()) {
16            return layout(constraints.minWidth, constraints.minHeight) {}
17}
18
19        val contentConstraints =
20            if (propagateMinConstraints) {
21                constraints
22            } else {
23                constraints. copyMaxDimensions () 
24            }
25      
26

限制(Constraint)传递

在Compose中,最开始的限制来自View系统,我们可以在AndroidComposeView中获取到由View系统传递给Compose LayoutNode的限制,流程如下:

1override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
2    trace("AndroidOwner:onMeasure") {
3if (!isAttachedToWindow) {
4            invalidateLayoutNodeMeasurement(root)
5        }
6        val (minWidth, maxWidth) = convertMeasureSpec(widthMeasureSpec)
7        val (minHeight, maxHeight) = convertMeasureSpec(heightMeasureSpec)
8
9        val constraints =
10            Constraints.fitPrioritizingHeight(
11                minWidth = minWidth,
12                maxWidth = maxWidth,
13                minHeight = minHeight,
14                maxHeight = maxHeight,
15            )
16        if (onMeasureConstraints == null) {
17            // first onMeasure after last onLayout
18            onMeasureConstraints = constraints
19            wasMeasuredWithMultipleConstraints = false
20        } else if (onMeasureConstraints != constraints) {
21            // we were remeasured twice with different constraints after last onLayout
22            wasMeasuredWithMultipleConstraints = true
23        }
24        measureAndLayoutDelegate.updateRootConstraints(constraints)
25        measureAndLayoutDelegate.measureOnly()
26        ....
27        
28

每个LayoutNode都会有一个MeasurePassDelegate对象,由它来代理单个LayoutNode的测量派发

1override fun measure(constraints: Constraints): Placeable {
2    if (layoutNode.intrinsicsUsageByParent == LayoutNode.UsageByParent.NotUsed) {
3        // This LayoutNode may have asked children for intrinsics. If so, we should
4        // clear the intrinsics usage for everything that was requested previously.
5        layoutNode.clearSubtreeIntrinsicsUsage()
6    }
7    // If we are at the lookahead root of the tree, do both the lookahead measure and
8    // regular measure. Otherwise, we'll be consistent with parent's lookahead measure
9    // and regular measure stages. This avoids producing exponential amount of
10    // lookahead when LookaheadLayouts are nested.
11    if (layoutNode.isOutMostLookaheadRoot) {
12        lookaheadPassDelegate!!.run {
13measuredByParent = LayoutNode.UsageByParent.NotUsed
14measure(constraints)
15        }
16}
17    trackMeasurementByParent(layoutNode)
18    remeasure(constraints)
19    return this
20}
21

这里我们就进入了真正的测量流程了,测量的限制传递分为单个LayoutNode的限制传递与LayoutNode之间的传递,我们知道LayoutNode上可能会有多个LayoutModifierNodeCoordinator,它都需要对最终的测量限制进行修改,下面我们看一下如果在LayoutModifierNodeCoordinator之前传递限制

LayoutModifierNodeCoordinator之间传递限制

在LayoutNode中,有两个关键的对象,outerCoordinator与innerCoordinator,其中outerCoordinator其实都是LayoutModifierNodeCoordinator,

1internal class LayoutNode
2
3internal val innerCoordinator: NodeCoordinator
4    get() = nodes.innerCoordinator
5
6internal val layoutDelegate = LayoutNodeLayoutDelegate(this)
7internal val outerCoordinator: NodeCoordinator
8    get() = nodes.outerCoordinator
9

默认情况下outerCoordinator == innerCoordinator,如果存在一个或者多个LayoutModifierNodeCoordinator,那么outerCoordinator则指向最左边的LayoutModifierNodeCoordinator

LayoutModifierNodeCoordinator 下的measure方法就是测量的流程,在这里我们做了一些lookahead流程的删减,我们可以看到,这里会回调layoutModifierNode下的measure方法,即我们在layoutModifierNode自定义的_measure_方法,而_measure_方法又会继续回调measurable的measure方法

1override fun measure(constraints: Constraints): Placeable {
2    @Suppress("NAME_SHADOWING")
3    val constraints =
4        if (forceMeasureWithLookaheadConstraints) {
5            requireNotNull(lookaheadConstraints) {
6"Lookahead constraints cannot be null in approach pass."
7            }
8} else {
9            constraints
10        }
11    performingMeasure(constraints) {
12measureResult =
13         // 忽略lookahead过程
14         with ( layoutModifierNode ) { measure (wrappedNonNull, constraints) }
15this@LayoutModifierNodeCoordinator
16    }
17onMeasured()
18    return this
19}
20

比如Padding,measure方法下又会将限制传递给下一份NodeCoordinator,此时_measure方法传递的是_wrappedNonNull,即它的子NodeCoordinator

1private class PaddingNode(
2    var start: Dp = 0.dp,
3    var top: Dp = 0.dp,
4    var end: Dp = 0.dp,
5    var bottom: Dp = 0.dp,
6    var rtlAware: Boolean,
7) : LayoutModifierNode, Modifier.Node() {
8
9    override fun MeasureScope.measure(
10        measurable: Measurable,
11        constraints: Constraints,
12    ): MeasureResult {
13
14        val horizontal = start.roundToPx() + end.roundToPx()
15        val vertical = top.roundToPx() + bottom.roundToPx()
16
17        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
18
19        val width = constraints.constrainWidth(placeable.width + horizontal)
20        val height = constraints.constrainHeight(placeable.height + vertical)
21        return layout(width, height) {
22if (rtlAware) {
23                placeable.placeRelative(start.roundToPx(), top.roundToPx())
24            } else {
25                placeable.place(start.roundToPx(), top.roundToPx())
26            }
27        }
28}
29}
30

InnerNodeCoordinator传递限制

在同一个LayoutNode中,限制最终传递到InnerNodeCoordinator,InnerNodeCoordinator会调用在LayoutNode声明的MeasurePolicy中的measure方法,从而把限制进一步传递给LayoutNode的子LayoutNode中,此时_measure方法传递的是**layoutNode.childMeasurables_,即它的子layoutNode的MeasurePassDelegate。

1override fun measure(constraints: Constraints): Placeable {
2    @Suppress("NAME_SHADOWING")
3    val constraints =
4        if (forceMeasureWithLookaheadConstraints) {
5            lookaheadDelegate!!.constraints
6        } else {
7            constraints
8        }
9    return performingMeasure(constraints) {
10// before rerunning the user's measure block reset previous measuredByParent for
11        // children
12        layoutNode.forEachChild {
13it.measurePassDelegate.measuredByParent = LayoutNode.UsageByParent.NotUsed
14 }
15
16measureResult =
17            with(layoutNode.measurePolicy)  { measure(layoutNode.childMeasurables, constraints)  }
18onMeasured()
19        this
20    } 
21

InnerNodeCoordinator会把限制传递给LayoutNode的子LayoutNode中,可能会存在多个。而LayoutModifierNodeCoordinator只会把限制传递给下一个LayoutModifierNodeCoordinator或者InnerNodeCoordinator,它的子节点只有一个或者为0个(outerCoordinator == innerCoordinator的情况),这里需要注意

测量结果(MeasureResult)的产生

测量的结果通过MeasureResult类产生

1interface MeasureResult {
2    /** The measured width of the layout, in pixels. */
3val width: Int
4
5    /** The measured height of the layout, in pixels. */
6val height: Int
7
8    /**
9* Alignment lines that can be used by parents to align this layout. This only includes the
10* alignment lines of this layout and not children.
11*/
12val alignmentLines: Map<AlignmentLine, Int>
13
14    /**
15* An optional lambda function used to create [Ruler]s for child layout. This may be
16* reevealuated when the layout's position moves.
17*/
18val rulers: (RulerScope.() -> Unit)?
19        get() = null
20
21    /**
22* A method used to place children of this layout. It may also be used to measure children that
23* were not needed for determining the size of this layout.
24*/
25fun placeChildren()
26}
27

在InnerNodeCoordinator 与LayoutModifierNodeCoordinator中,最终都会拿到最终的结果,这个结果通过由layout方法返回

1override fun MeasureScope.measure(
2    measurable: Measurable,
3    constraints: Constraints,
4): MeasureResult {
5
6    val horizontal = start.roundToPx() + end.roundToPx()
7    val vertical = top.roundToPx() + bottom.roundToPx()
8
9    val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
10
11    val width = constraints.constrainWidth(placeable.width + horizontal)
12    val height = constraints.constrainHeight(placeable.height + vertical)
13    return layout(width, height) {
14 if (rtlAware) {
15placeable. placeRelative (start. roundToPx (), top. roundToPx ())
16} else {
17placeable. place (start. roundToPx (), top. roundToPx ())
18}
19}
20}
21
1fun layout(
2    width: Int,
3    height: Int,
4    alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
5    rulers: (RulerScope.() -> Unit)? = null,
6    placementBlock: Placeable.PlacementScope.() -> Unit,
7): MeasureResult {
8    checkMeasuredSize(width, height)
9    return object : MeasureResult {
10        override val width = width
11        override val height = height
12        override val alignmentLines = alignmentLines
13        override val rulers = rulers
14
15        override fun placeChildren() {
16            // This isn't called from anywhere inside the compose framework. This might
17            // be called by tests or external frameworks.
18            if (this@MeasureScope is LookaheadCapablePlaceable) {
19                placementScope.placementBlock()
20            } else {
21                SimplePlacementScope(width, layoutDirection, density, fontScale)
22                    .placementBlock()
23            }
24        }
25    }
26}
27

当然,自身测量的结果还需要被矫正在 父节点 传递的限制中才是合法的结果

1protected open fun onMeasureResultChanged(width: Int, height: Int) {
2     ...
3    measuredSize = IntSize(width, height)
4    if (layerBlock != null) {
5        updateLayerParameters(invokeOnLayoutChange = false)
6    }
7    visitNodes(Nodes.Draw) { it.onMeasureResultChanged() }
8layoutNode.owner?.onLayoutChange(layoutNode)
9}
10
11protected var measuredSize: IntSize = IntSize(0, 0)
12    set(value) {
13        if (field != value) {
14            field = value
15            onMeasuredSizeChanged()
16        }
17    }
18
19private fun onMeasuredSizeChanged() {
20    width =
21        measuredSize.width.coerceIn(
22            measurementConstraints.minWidth,
23            measurementConstraints.maxWidth,
24        )
25    height =
26        measuredSize.height.coerceIn(
27            measurementConstraints.minHeight,
28            measurementConstraints.maxHeight,
29        )
30    apparentToRealOffset =
31        IntOffset((width - measuredSize.width) / 2, (height - measuredSize.height) / 2)
32}
33

这里我们可以知道,Compose测量其实按测量形态分为两种:NodeCoordinator与NodeCoordinator之间的测量以及跨LayoutNode的测量(NodeCoordinator与MeasurePassDelegate)

二次测量是什么?Compose真的没有二次测量吗

传统意义来说,对于measure的二次调用就属于二次测量,在一些特殊场景下二次测量是有意义的或者说是不可或缺的。

在传统的 Android View 系统中,当父布局的宽度被设置成wrap_content,且内部有子 View 的宽度是match_parent时,会出现二次测量的情况。

父控件大小未决定,而子控件需要根据父控件的大小决定自己的大小的情况下,就需要对子控件进行二次测量,

在Compose中,只有MeasurePassDelegate被限定为只能进行一次测量

1MeasurePassDelegate.kt
2
3private fun trackMeasurementByParent(node: LayoutNode) {
4    val parent = node.parent
5    if (parent != null) {
6        checkPrecondition ( 
7 measuredByParent == LayoutNode.UsageByParent. NotUsed  || 
8  @Suppress (  "DEPRECATION"  ) node.canMultiMeasure
9  )  {
10 MeasuredTwiceErrorMessage
11 }
12        
13        ...
14        
15internal const val MeasuredTwiceErrorMessage: String =
16    "measure() may not be called multiple times on the same Measurable. If you want to " +
17        "get the content size of the Measurable before calculating the final constraints, " +
18        "please use methods like minIntrinsicWidth()/maxIntrinsicWidth() and " +
19        "minIntrinsicHeight()/maxIntrinsicHeight()"
20

在其他测量流程中,比如NodeCoordinator与NodeCoordinator之间的测量,测量次数可以不被限定,比如LayoutModifierNodeCoordinator下可以进行多次测量

1Box(
2    Modifier
3        .layout {
4measurable,constraint->
5            // 多次测量是OK的
6val firstMeasured = measurable.measure(constraint)
7            val secondMeasured =  measurable.measure(constraint)
8            layout(firstMeasured.width,secondMeasured.height) {
9
10}
11} 
12

所以回到标题,Compose真的没有二次测量,其实是有的!

固有测量

IntrinsicSizeModifier也是实现了LayoutModifierNode的抽象类,在measure方法中,它其实也算是进行了两轮测量,第一轮测量是被称为固有测量,这个时候父节点会对子节点进行一次测量,只是这次的测量并不会涉及到MeasurePassDelegate的测量。

1private abstract class IntrinsicSizeModifier : LayoutModifierNode, Modifier.Node() {
2
3    abstract val enforceIncoming: Boolean
4
5    abstract fun MeasureScope.calculateContentConstraints(
6        measurable: Measurable,
7        constraints: Constraints,
8    ): Constraints
9
10    final override fun MeasureScope.measure(
11        measurable: Measurable,
12        constraints: Constraints,
13    ): MeasureResult {
14        val contentConstraints = calculateContentConstraints(measurable, constraints)
15        val placeable =
16            measurable.measure(
17                if (enforceIncoming) constraints.constrain(contentConstraints)
18                else contentConstraints
19            )
20        return layout(placeable.width, placeable.height) { placeable.placeRelative(IntOffset.Zero) }
21}
22

calculateContentConstraints 方法中,通常会调用对应的IntrinsicMeasurable方法进行宽高的确定

1interface IntrinsicMeasurable {
2    /** Data provided by the [ParentDataModifier]. */
3val parentData: Any?
4
5    /**
6* Calculates the minimum width that the layout can be such that the content of the layout will
7* be painted correctly. There should be no side-effects from a call to [minIntrinsicWidth].
8*/
9fun minIntrinsicWidth(height: Int): Int
10
11    /**
12* Calculates the smallest width beyond which increasing the width never decreases the height.
13* There should be no side-effects from a call to [maxIntrinsicWidth].
14*/
15fun maxIntrinsicWidth(height: Int): Int
16
17    /**
18* Calculates the minimum height that the layout can be such that the content of the layout will
19* be painted correctly. There should be no side-effects from a call to [minIntrinsicHeight].
20*/
21fun minIntrinsicHeight(width: Int): Int
22
23    /**
24* Calculates the smallest height beyond which increasing the height never decreases the width.
25* There should be no side-effects from a call to [maxIntrinsicHeight].
26*/
27fun maxIntrinsicHeight(width: Int): Int
28}
29

最终的结果跟普通测量结果流程一样,通过LayoutModifierNodeCoordinator 与InnerNodeCoordinator得出最终的结果

1InnerNodeCoordinator
2
3override fun minIntrinsicWidth(height: Int) = layoutNode.minIntrinsicWidth(height)
4
5override fun minIntrinsicHeight(width: Int) = layoutNode.minIntrinsicHeight(width)
6
7override fun maxIntrinsicWidth(height: Int) = layoutNode.maxIntrinsicWidth(height)
8
9override fun maxIntrinsicHeight(width: Int) = layoutNode.maxIntrinsicHeight(width)
10
1LayoutModifierNodeCoordinator
2
3
4override fun minIntrinsicWidth(height: Int): Int =
5    with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
6 minIntrinsicWidth(
7            this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
8            height,
9        )
10    }
11
12override fun maxIntrinsicWidth(height: Int): Int =
13    with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
14 maxIntrinsicWidth(
15            this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
16            height,
17        )
18    }
19
20override fun minIntrinsicHeight(width: Int): Int =
21    with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
22 minIntrinsicHeight(
23            this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
24            width,
25        )
26    }
27
28override fun maxIntrinsicHeight(width: Int): Int =
29    with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
30 maxIntrinsicHeight(
31            this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
32            width,
33        )
34    } 
35

当第二次measure方法时就会利用固有测量得到的信息修改Constraint发起另一次测量

Lookahead测量

Lookahead测量也算是Compose中对于测量流程的信息补充,通常用于在涉及位置大小的动画场景中,在满足条件下,也会发起两次测量

1fun measureOnly() {
2    if (relayoutNodes.isNotEmpty()) {
3        performMeasureAndLayout(fullPass = false) {
4if (relayoutNodes.affectsLookaheadMeasure) {
5                if (root.lookaheadRoot != null) {
6                    // This call will walk the tree to look for lookaheadMeasurePending nodes
7                    // and
8                    // do a lookahead remeasure for those nodes only.
9                    remeasureOnly(root, affectsLookahead = true)
10                } else {
11                    // First do a lookahead remeasure pass for all the lookaheadMeasurePending
12                    // nodes,
13                    // followed by a remeasure pass for the rest of the tree.
14                    remeasureLookaheadRootsInSubtree(root)
15                }
16            }
17            remeasureOnly(root, affectsLookahead = false)
18        }
19}
20}
21

此时都会调用到Measurable的measure方法,这里的第一次测量我们称为lookahead测量,跟正式测量不一样的是,传入measure方法的measurable也不涉及MeasurePassDelegate, 而有专门的LookaheadDelegate对象来负责

1private inner class LookaheadDelegateImpl : LookaheadDelegate(this@InnerNodeCoordinator) {
2
3    // Lookahead measure
4    override fun measure(constraints: Constraints): Placeable =
5        performingMeasure(constraints) {
6// before rerunning the user's measure block reset previous measuredByParent for
7            // children
8            layoutNode.forEachChild {
9it.lookaheadPassDelegate!!.measuredByParent = LayoutNode.UsageByParent.NotUsed
10 }
11val measureResult =
12                with ( layoutNode.measurePolicy )  {
13measure ( layoutNode.childLookaheadMeasurables, constraints)
14                }
15measureResult
16        } 
17
1private inner class LookaheadDelegateForLayoutModifierNode :
2    LookaheadDelegate(this@LayoutModifierNodeCoordinator) {
3    // LookaheadMeasure
4    override fun measure(constraints: Constraints): Placeable =
5        performingMeasure(constraints) {
6this@LayoutModifierNodeCoordinator.lookaheadConstraints = constraints
7            with ( this @LayoutModifierNodeCoordinator .layoutModifierNode )  {
8measure(
9                    // This allows `measure` calls in the modifier to be redirected to
10                    // calling lookaheadMeasure in wrapped.
11                    this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
12                    constraints,
13                )
14            }
15} 
16

总结

对于Compose的测量,总结出三个特点:

  1. 测量结果必定需要满足父节点传递的限制,改变传递的限制能够影响最终的结果
  2. NodeCoordinator与NodeCoordinator之间的测量以及跨LayoutNode的测量(NodeCoordinator与MeasurePassDelegate)
  3. 二次测量的限定在MeasurePassDelegate,如果测量流程不涉及MeasurePassDelegate,则可以测量多次

深入浅出 Compose 测量机制》 是转载文章,点击查看原文


相关推荐


大数据毕业设计选题推荐-基于大数据的全球产品库存数据分析与可视化系统-大数据-Spark-Hadoop-Bigdata
IT研究室2025/10/8

✨作者主页:IT研究室✨ 个人简介:曾从事计算机专业培训教学,擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python项目 安卓项目 微信小程序项目 文章目录 一、前言二、开发环境三、系统界面展示四、代码参考五、系统视频结语 一、前言 系统介绍 本系统是一个基于大数据技术的全球产品库存数据分析与可视化系统,采用Hado


【Linux】线程的互斥
羚羊角uou2025/10/6

因为线程是共享地址空间的,就会共享大部分资源,这种共享资源就是公共资源,当多执行流访问公共资源的时候,就会出现各种情况的数据不一致问题。为了解决这种问题,我们就需要学习线程的同步与互斥,本篇将介绍线程的互斥。 1.相关概念 临界资源:多线程执⾏流被保护的共享资源就叫做临界资源 临界区:每个线程内部,访问临界资源的代码,就叫做临界区 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起保护作⽤ 原⼦性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,


MySQL Performance Schema详解与实战应用
IT橘子皮2025/10/5

Performance Schema是MySQL内置的性能监控系统,自5.5版本引入以来已成为数据库性能分析与优化的核心工具。本文将全面解析其架构原理、配置方法及典型应用场景,帮助您掌握这一强大的性能诊断利器。 一、Performance Schema核心架构 Performance Schema采用插桩-消费者模型构建,通过轻量级的内存表存储性能数据,对数据库性能影响通常控制在5%以内。其核心组件包括: ​插桩点(Instruments)​​:嵌入MySQL代码的探测点,按层级命名如wai


【Unity笔记】Unity XR 模式下 Point Light 不生效的原因与解决方法
EQ-雪梨蛋花汤2025/10/4

Unity XR 模式下 Point Light 不生效的原因与解决方法 在 Unity 中开发 VR 应用时,经常会遇到一个让人疑惑的现象: 在 编辑器 Game 模式下,场景中的 Point Light(点光源) 可以正常照亮物体。但当启用 Initialize XR on Startup 并通过 VR 设备运行时,Point Light 不再生效,只有 Directional Light(平行光) 仍然有效。 这让很多开发者误以为“材质只支持 Directional Light,而不支持


XYplorer(多标签文件管理器) 多语便携版
东风西巷2025/10/2

XYplorer中文版是一款多标签文件管理器及增强资源管理器的工具,XYplorer文件管理器支持多标签页栏,管理文件时跟使用Chrome之类浏览器一样,从浏览方便性,和切换滑顺程度,要比Windows系统自带的Explorer资源管理器便捷得多.可以大部分程度上替代系统自带的文件管理器.同时,有浏览器快捷键和鼠标快捷. 软件功能 双窗口浏览:支持双窗口浏览,可以同时浏览两个文件夹,方便文件的复制、移动和比较。 高级搜索:支持高级搜索功能,可以根据文件名、大小、日期、属性等多种条件进


什么是 Apache Ignite?
悟能不能悟2025/10/2

首先需要明确一点:“Ignite”这个名字在技术领域可能指代不同的事物,但最著名和广泛使用的是 ​Apache Ignite。它是一个功能强大的、分布式内存计算平台。除此之外,还有例如 ​Couchbase Ignite​(一个会议)等。本文将重点介绍 ​Apache Ignite。 什么是 Apache Ignite? Apache Ignite 是一个以内存为中心的分布式数据库、缓存和处理平台,设计用于在横向扩展的架构上提供极高的性能和吞吐量。你可以把它理解为一个“内存数据网格”,但其


Python零基础入门:30分钟掌握核心语法与实战应用
做运维的阿瑞2025/10/2

Python基础入门指南 5分钟掌握核心概念,15分钟上手实战项目 你将学到什么 核心技能实际应用学习时间🔢 数据类型处理文本、数字、列表10分钟🔄 控制流程循环、判断、函数15分钟📊 数据处理文件操作、数据分析20分钟🎮 实战项目猜数字游戏30分钟 适合人群 零基础新手 | 转语言开发者 | 在校学生 | 职场提升 快速开始 三个核心场景 数据处理 # 处理学生成绩 scores = [85, 92, 78, 96, 88, 76, 94, 82] #


软件工程实践团队作业——团队组建与实践选题
Funny Valentine-js10/1/2025

吴彦组。


Qt Widgets 应用程序核心类 - QApplication 详解
会飞的胖达喵9/30/2025

摘要:QApplication是Qt Widgets应用的核心类,负责GUI应用程序的控制流和全局设置。它继承自QGuiApplication和QCoreApplication,提供样式管理、调色板/字体设置、用户交互参数配置以及窗口管理等功能。通过qApp宏可全局访问应用实例,支持运行时动态调整界面风格。示例展示了QApplication的初始化、属性设置、样式更改和事件循环管理,以及高级功能如字体大小控制和样式表应用,体现了其在Qt Widgets开发中的核心作用。


C/C++黑客帝国代码雨
Want5952025/10/10

写在前面 数字雨,又被称为“黑客帝国雨”,是一种经典的视觉效果,常用于表现科幻、科技感十足的场景。这种效果最初在电影《黑客帝国》中出现,以绿色字符从屏幕顶端不断下落的方式,营造出一种神秘而充满未来感的氛围。本文将介绍如何使用C语言在Windows控制台中实现一个简易的数字雨效果。通过这篇文章,你不仅能了解如何利用控制台API进行绘图操作,还能体会到字符动画背后的技术逻辑与美感。 系列文章 序号直达链接1C/C++李峋同款跳动的爱心2C/C++跳动的爱心3C/C++经典爱心4C/C++满

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0