自从换了新工作后,好久没有写博客了,今天终于能有时间写点东西,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的测量,总结出三个特点:
- 测量结果必定需要满足父节点传递的限制,改变传递的限制能够影响最终的结果
- NodeCoordinator与NodeCoordinator之间的测量以及跨LayoutNode的测量(NodeCoordinator与MeasurePassDelegate)
- 二次测量的限定在MeasurePassDelegate,如果测量流程不涉及MeasurePassDelegate,则可以测量多次