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