深入浅出 Compose 测量机制

自从换了新工作后,好久没有写博客了,今天终于能有时间写点东西,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的数据结构

less 复制代码
@Suppress("ComposableLambdaParameterPosition")
@UiComposable
@Composable
inline fun Layout(
    content: @Composable @UiComposable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy,
) {
    val compositeKeyHash = currentCompositeKeyHashCode.hashCode()
    val localMap = currentComposer.currentCompositionLocalMap
    val materialized = currentComposer.materialize(modifier)
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
set(measurePolicy, SetMeasurePolicy)
            set(localMap, SetResolvedCompositionLocals)
            set(compositeKeyHash, SetCompositeKeyHash)
            set(materialized, SetModifier)
        } ,
        content = content,
    )
}

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

kotlin 复制代码
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit,
) {
    val measurePolicy = maybeCachedBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
    Layout(
        content = { BoxScopeInstance.content() } ,
        measurePolicy = measurePolicy,
        modifier = modifier,
    )
}

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

scss 复制代码
Box(xxx) {
Box(xxx){
      ....
    }
    Text(xxx)
   
} 

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

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

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

kotlin 复制代码
interface LayoutModifierNode : DelegatableNode {
   
fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult
    ... 

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

kotlin 复制代码
private class PaddingNode(
    var start: Dp = 0.dp,
    var top: Dp = 0.dp,
    var end: Dp = 0.dp,
    var bottom: Dp = 0.dp,
    var rtlAware: Boolean,
) : LayoutModifierNode, Modifier.Node()

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

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

less 复制代码
internal object Nodes {
    @JvmStatic
    inline val Any
        get() = NodeKind<Modifier.Node>(0b1 shl 0)

    @JvmStatic
    inline val Layout
        get() = NodeKind<LayoutModifierNode>(0b1 shl 1)

    @JvmStatic
    inline val Draw
        get() = NodeKind<DrawModifierNode>(0b1 shl 2)

    @JvmStatic
    inline val Semantics
        get() = NodeKind<SemanticsModifierNode>(0b1 shl 3)

    @JvmStatic
    inline val PointerInput
        get() = NodeKind<PointerInputModifierNode>(0b1 shl 4)

    @JvmStatic
    inline val Locals
        get() = NodeKind<ModifierLocalModifierNode>(0b1 shl 5)

    @JvmStatic
    inline val ParentData
        get() = NodeKind<ParentDataModifierNode>(0b1 shl 6)
    ....

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

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

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

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

ini 复制代码
fun syncCoordinators() {
    var coordinator: NodeCoordinator = innerCoordinator
    var node: Modifier.Node? = tail.parent
    while (node != null) {
        val layoutmod = node.asLayoutModifierNode()
        if (layoutmod != null) {
            val next =
                if (node.coordinator != null) {
                    val c = node.coordinator as LayoutModifierNodeCoordinator
                    val prevNode = c.layoutModifierNode
                    c.layoutModifierNode = layoutmod
                    if (prevNode !== node) c.onLayoutModifierNodeChanged()
                    c
                } else {
                    val c = LayoutModifierNodeCoordinator(layoutNode, layoutmod)
                    node.updateCoordinator(c)
                    c
                }
            coordinator.wrappedBy = next
            next.wrapped = coordinator
            coordinator = next
        } else {
            node.updateCoordinator(coordinator)
        }
        node = node.parent
    }
    coordinator.wrappedBy = layoutNode.parent?.innerCoordinator
    outerCoordinator = coordinator
}

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

测量机制

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

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

scss 复制代码
 /** The measured size of this Placeable. This might not respect [measurementConstraints]. */
protected var measuredSize: IntSize = IntSize(0, 0)
    set(value) {
        if (field != value) {
            field = value
            onMeasuredSizeChanged()
        }
    }

private fun onMeasuredSizeChanged() {
    width =
       measuredSize. width . coerceIn (
measurementConstraints. minWidth ,
measurementConstraints. maxWidth ,
)
    height =
       measuredSize. height . coerceIn (
measurementConstraints. minHeight ,
measurementConstraints. maxHeight ,
)
    apparentToRealOffset =
        IntOffset((width - measuredSize.width) / 2, (height - measuredSize.height) / 2)
}

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

scss 复制代码
Modifer.width(300.dp).width(150.dp)

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

ini 复制代码
fun Modifier.requiredWidth(width: Dp) =
    this.then(
        SizeElement(
            minWidth = width,
            maxWidth = width,
           enforceIncoming = false, 
            inspectorInfo =
                debugInspectorInfo {
name = "requiredWidth"
                    value = width
                } ,
        )
    )
kotlin 复制代码
override fun MeasureScope.measure(
    measurable: Measurable,
    constraints: Constraints,
): MeasureResult {
    val wrappedConstraints =
        targetConstraints.let { targetConstraints ->
 if (enforceIncoming) { 
 constraints. constrain (targetConstraints) 
 } else { 
 val resolvedMinWidth =
 if (minWidth. isSpecified ) { 
 targetConstraints.minWidth
 } else { 
 constraints.minWidth. fastCoerceAtMost (targetConstraints.maxWidth) 
                    }
                val resolvedMaxWidth =
                    if (maxWidth.isSpecified) {
                        targetConstraints.maxWidth
                    } else {
                        constraints.maxWidth.fastCoerceAtLeast(targetConstraints.minWidth)
                    }
                val resolvedMinHeight =
                    if (minHeight.isSpecified) {
                        targetConstraints.minHeight
                    } else {
                        constraints.minHeight.fastCoerceAtMost(targetConstraints.maxHeight)
                    }
                val resolvedMaxHeight =
                    if (maxHeight.isSpecified) {
                        targetConstraints.maxHeight
                    } else {
                        constraints.maxHeight.fastCoerceAtLeast(targetConstraints.minHeight)
                    }
                Constraints(
                    resolvedMinWidth,
                    resolvedMaxWidth,
                    resolvedMinHeight,
                    resolvedMaxHeight,
                )
            }
        } 

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

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

kotlin 复制代码
 /**
* Copies the existing [Constraints], setting [minWidth] and [minHeight] to 0, and preserving
* [maxWidth] and [maxHeight] as-is.
*/
inline fun copyMaxDimensions() = Constraints(value and MaxDimensionsAndFocusMask)

private data class BoxMeasurePolicy(
    private val alignment: Alignment,
    private val propagateMinConstraints: Boolean,
) : MeasurePolicy {
    override fun MeasureScope.measure(
        measurables: List<Measurable>,
        constraints: Constraints,
    ): MeasureResult {
        if (measurables.isEmpty()) {
            return layout(constraints.minWidth, constraints.minHeight) {}
}

        val contentConstraints =
            if (propagateMinConstraints) {
                constraints
            } else {
                constraints. copyMaxDimensions () 
            }
            
            
      

限制(Constraint)传递

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

scss 复制代码
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    trace("AndroidOwner:onMeasure") {
if (!isAttachedToWindow) {
            invalidateLayoutNodeMeasurement(root)
        }
        val (minWidth, maxWidth) = convertMeasureSpec(widthMeasureSpec)
        val (minHeight, maxHeight) = convertMeasureSpec(heightMeasureSpec)

        val constraints =
            Constraints.fitPrioritizingHeight(
                minWidth = minWidth,
                maxWidth = maxWidth,
                minHeight = minHeight,
                maxHeight = maxHeight,
            )
        if (onMeasureConstraints == null) {
            // first onMeasure after last onLayout
            onMeasureConstraints = constraints
            wasMeasuredWithMultipleConstraints = false
        } else if (onMeasureConstraints != constraints) {
            // we were remeasured twice with different constraints after last onLayout
            wasMeasuredWithMultipleConstraints = true
        }
        measureAndLayoutDelegate.updateRootConstraints(constraints)
        measureAndLayoutDelegate.measureOnly()
        ....
        

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

scss 复制代码
override fun measure(constraints: Constraints): Placeable {
    if (layoutNode.intrinsicsUsageByParent == LayoutNode.UsageByParent.NotUsed) {
        // This LayoutNode may have asked children for intrinsics. If so, we should
        // clear the intrinsics usage for everything that was requested previously.
        layoutNode.clearSubtreeIntrinsicsUsage()
    }
    // If we are at the lookahead root of the tree, do both the lookahead measure and
    // regular measure. Otherwise, we'll be consistent with parent's lookahead measure
    // and regular measure stages. This avoids producing exponential amount of
    // lookahead when LookaheadLayouts are nested.
    if (layoutNode.isOutMostLookaheadRoot) {
        lookaheadPassDelegate!!.run {
measuredByParent = LayoutNode.UsageByParent.NotUsed
measure(constraints)
        }
}
    trackMeasurementByParent(layoutNode)
    remeasure(constraints)
    return this
}

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

LayoutModifierNodeCoordinator之间传递限制

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

kotlin 复制代码
internal class LayoutNode

internal val innerCoordinator: NodeCoordinator
    get() = nodes.innerCoordinator

internal val layoutDelegate = LayoutNodeLayoutDelegate(this)
internal val outerCoordinator: NodeCoordinator
    get() = nodes.outerCoordinator

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

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

kotlin 复制代码
override fun measure(constraints: Constraints): Placeable {
    @Suppress("NAME_SHADOWING")
    val constraints =
        if (forceMeasureWithLookaheadConstraints) {
            requireNotNull(lookaheadConstraints) {
"Lookahead constraints cannot be null in approach pass."
            }
} else {
            constraints
        }
    performingMeasure(constraints) {
measureResult =
         // 忽略lookahead过程
         with ( layoutModifierNode ) { measure (wrappedNonNull, constraints) }
this@LayoutModifierNodeCoordinator
    }
onMeasured()
    return this
}

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

kotlin 复制代码
private class PaddingNode(
    var start: Dp = 0.dp,
    var top: Dp = 0.dp,
    var end: Dp = 0.dp,
    var bottom: Dp = 0.dp,
    var rtlAware: Boolean,
) : LayoutModifierNode, Modifier.Node() {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {

        val horizontal = start.roundToPx() + end.roundToPx()
        val vertical = top.roundToPx() + bottom.roundToPx()

        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) {
                placeable.placeRelative(start.roundToPx(), top.roundToPx())
            } else {
                placeable.place(start.roundToPx(), top.roundToPx())
            }
        }
}
}

InnerNodeCoordinator传递限制

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

kotlin 复制代码
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
 }

measureResult =
            with(layoutNode.measurePolicy)  { measure(layoutNode.childMeasurables, constraints)  }
onMeasured()
        this
    } 

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

测量结果(MeasureResult)的产生

测量的结果通过MeasureResult类产生

kotlin 复制代码
interface MeasureResult {
    /** The measured width of the layout, in pixels. */
val width: Int

    /** The measured height of the layout, in pixels. */
val height: Int

    /**
* Alignment lines that can be used by parents to align this layout. This only includes the
* alignment lines of this layout and not children.
*/
val alignmentLines: Map<AlignmentLine, Int>

    /**
* An optional lambda function used to create [Ruler]s for child layout. This may be
* reevealuated when the layout's position moves.
*/
val rulers: (RulerScope.() -> Unit)?
        get() = null

    /**
* A method used to place children of this layout. It may also be used to measure children that
* were not needed for determining the size of this layout.
*/
fun placeChildren()
}

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

scss 复制代码
override fun MeasureScope.measure(
    measurable: Measurable,
    constraints: Constraints,
): MeasureResult {

    val horizontal = start.roundToPx() + end.roundToPx()
    val vertical = top.roundToPx() + bottom.roundToPx()

    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) {
placeable. placeRelative (start. roundToPx (), top. roundToPx ())
} else {
placeable. place (start. roundToPx (), top. roundToPx ())
}
}
}
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() {
            // This isn't called from anywhere inside the compose framework. This might
            // be called by tests or external frameworks.
            if (this@MeasureScope is LookaheadCapablePlaceable) {
                placementScope.placementBlock()
            } else {
                SimplePlacementScope(width, layoutDirection, density, fontScale)
                    .placementBlock()
            }
        }
    }
}

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

scss 复制代码
protected open fun onMeasureResultChanged(width: Int, height: Int) {
     ...
    measuredSize = IntSize(width, height)
    if (layerBlock != null) {
        updateLayerParameters(invokeOnLayoutChange = false)
    }
    visitNodes(Nodes.Draw) { it.onMeasureResultChanged() }
layoutNode.owner?.onLayoutChange(layoutNode)
}

protected var measuredSize: IntSize = IntSize(0, 0)
    set(value) {
        if (field != value) {
            field = value
            onMeasuredSizeChanged()
        }
    }

private fun onMeasuredSizeChanged() {
    width =
        measuredSize.width.coerceIn(
            measurementConstraints.minWidth,
            measurementConstraints.maxWidth,
        )
    height =
        measuredSize.height.coerceIn(
            measurementConstraints.minHeight,
            measurementConstraints.maxHeight,
        )
    apparentToRealOffset =
        IntOffset((width - measuredSize.width) / 2, (height - measuredSize.height) / 2)
}

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

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

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

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

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

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

kotlin 复制代码
MeasurePassDelegate.kt

private fun trackMeasurementByParent(node: LayoutNode) {
    val parent = node.parent
    if (parent != null) {
        checkPrecondition ( 
 measuredByParent == LayoutNode.UsageByParent. NotUsed  || 
  @Suppress (  "DEPRECATION"  ) node.canMultiMeasure
  )  {
 MeasuredTwiceErrorMessage
 }
        
        ...
        
internal const val MeasuredTwiceErrorMessage: String =
    "measure() may not be called multiple times on the same Measurable. If you want to " +
        "get the content size of the Measurable before calculating the final constraints, " +
        "please use methods like minIntrinsicWidth()/maxIntrinsicWidth() and " +
        "minIntrinsicHeight()/maxIntrinsicHeight()"

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

scss 复制代码
Box(
    Modifier
        .layout {
measurable,constraint->
            // 多次测量是OK的
val firstMeasured = measurable.measure(constraint)
            val secondMeasured =  measurable.measure(constraint)
            layout(firstMeasured.width,secondMeasured.height) {

}
} 

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

固有测量

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

kotlin 复制代码
private abstract class IntrinsicSizeModifier : LayoutModifierNode, Modifier.Node() {

    abstract val enforceIncoming: Boolean

    abstract fun MeasureScope.calculateContentConstraints(
        measurable: Measurable,
        constraints: Constraints,
    ): Constraints

    final override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        val contentConstraints = calculateContentConstraints(measurable, constraints)
        val placeable =
            measurable.measure(
                if (enforceIncoming) constraints.constrain(contentConstraints)
                else contentConstraints
            )
        return layout(placeable.width, placeable.height) { placeable.placeRelative(IntOffset.Zero) }
}

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

kotlin 复制代码
interface IntrinsicMeasurable {
    /** Data provided by the [ParentDataModifier]. */
val parentData: Any?

    /**
* Calculates the minimum width that the layout can be such that the content of the layout will
* be painted correctly. There should be no side-effects from a call to [minIntrinsicWidth].
*/
fun minIntrinsicWidth(height: Int): Int

    /**
* Calculates the smallest width beyond which increasing the width never decreases the height.
* There should be no side-effects from a call to [maxIntrinsicWidth].
*/
fun maxIntrinsicWidth(height: Int): Int

    /**
* Calculates the minimum height that the layout can be such that the content of the layout will
* be painted correctly. There should be no side-effects from a call to [minIntrinsicHeight].
*/
fun minIntrinsicHeight(width: Int): Int

    /**
* Calculates the smallest height beyond which increasing the height never decreases the width.
* There should be no side-effects from a call to [maxIntrinsicHeight].
*/
fun maxIntrinsicHeight(width: Int): Int
}

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

kotlin 复制代码
InnerNodeCoordinator

override fun minIntrinsicWidth(height: Int) = layoutNode.minIntrinsicWidth(height)

override fun minIntrinsicHeight(width: Int) = layoutNode.minIntrinsicHeight(width)

override fun maxIntrinsicWidth(height: Int) = layoutNode.maxIntrinsicWidth(height)

override fun maxIntrinsicHeight(width: Int) = layoutNode.maxIntrinsicHeight(width)
less 复制代码
LayoutModifierNodeCoordinator


override fun minIntrinsicWidth(height: Int): Int =
    with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
 minIntrinsicWidth(
            this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
            height,
        )
    }

override fun maxIntrinsicWidth(height: Int): Int =
    with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
 maxIntrinsicWidth(
            this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
            height,
        )
    }

override fun minIntrinsicHeight(width: Int): Int =
    with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
 minIntrinsicHeight(
            this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
            width,
        )
    }

override fun maxIntrinsicHeight(width: Int): Int =
    with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
 maxIntrinsicHeight(
            this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
            width,
        )
    } 

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

Lookahead测量

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

scss 复制代码
fun measureOnly() {
    if (relayoutNodes.isNotEmpty()) {
        performMeasureAndLayout(fullPass = false) {
if (relayoutNodes.affectsLookaheadMeasure) {
                if (root.lookaheadRoot != null) {
                    // This call will walk the tree to look for lookaheadMeasurePending nodes
                    // and
                    // do a lookahead remeasure for those nodes only.
                    remeasureOnly(root, affectsLookahead = true)
                } else {
                    // First do a lookahead remeasure pass for all the lookaheadMeasurePending
                    // nodes,
                    // followed by a remeasure pass for the rest of the tree.
                    remeasureLookaheadRootsInSubtree(root)
                }
            }
            remeasureOnly(root, affectsLookahead = false)
        }
}
}

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

kotlin 复制代码
private inner class LookaheadDelegateImpl : LookaheadDelegate(this@InnerNodeCoordinator) {

    // Lookahead measure
    override fun measure(constraints: Constraints): Placeable =
        performingMeasure(constraints) {
// before rerunning the user's measure block reset previous measuredByParent for
            // children
            layoutNode.forEachChild {
it.lookaheadPassDelegate!!.measuredByParent = LayoutNode.UsageByParent.NotUsed
 }
val measureResult =
                with ( layoutNode.measurePolicy )  {
measure ( layoutNode.childLookaheadMeasurables, constraints)
                }
measureResult
        } 
less 复制代码
private inner class LookaheadDelegateForLayoutModifierNode :
    LookaheadDelegate(this@LayoutModifierNodeCoordinator) {
    // LookaheadMeasure
    override fun measure(constraints: Constraints): Placeable =
        performingMeasure(constraints) {
this@LayoutModifierNodeCoordinator.lookaheadConstraints = constraints
            with ( this @LayoutModifierNodeCoordinator .layoutModifierNode )  {
measure(
                    // This allows `measure` calls in the modifier to be redirected to
                    // calling lookaheadMeasure in wrapped.
                    this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
                    constraints,
                )
            }
} 

总结

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

  1. 测量结果必定需要满足父节点传递的限制,改变传递的限制能够影响最终的结果
  2. NodeCoordinator与NodeCoordinator之间的测量以及跨LayoutNode的测量(NodeCoordinator与MeasurePassDelegate)
  3. 二次测量的限定在MeasurePassDelegate,如果测量流程不涉及MeasurePassDelegate,则可以测量多次
相关推荐
pedestrian_h2 小时前
操作系统-线程
android·java·开发语言
gfdgd xi3 小时前
GXDE 内核管理器 1.0.1——修复bug、支持loong64
android·linux·运维·python·ubuntu·bug
美狐美颜sdk3 小时前
跨平台直播美颜sdk集成攻略:Android、iOS与Web的统一方案
android·前端·ios
❀͜͡傀儡师4 小时前
二维码/条码识别、身份证识别、银行卡识别、车牌识别、图片文字识别、黄图识别、驾驶证(驾照)识别
android·scanner
生莫甲鲁浪戴5 小时前
Android Studio新手开发第三十五天
android·ide·android studio
qq_717410016 小时前
FAQ20472:相机录像镜像功能实现
android
非专业程序员Ping7 小时前
HarfBuzz 实战:五大核心API 实例详解【附iOS/Swift实战示例】
android·ios·swift
流星魂小七7 小时前
颜色选择器
android·着色器·环形颜色选择器·圆形颜色选择器·colorpicker·colorwheelview
cdming8 小时前
LIUNX 与手机安卓的文件互传 的常用方法
android·智能手机
雨白10 小时前
Flow 的异常处理与执行控制
android·kotlin