Compose 实践与探索八 —— LayoutModifier 解析

前面几节讲的 Modifier 都是起辅助作用的,比如 Modifier 的伴生对象、CombinedModifier、 ComposedModifier 以及几乎所有 Modifier 的父接口 Modifier.Element。本篇我们开始讲具有直接功效的 Modifier,分为几个大类:LayoutModifier、DrawModifier 等。

1、LayoutModifier 与 Modifier.layout()

本节内容属于“自定义 View 在 Compose 中的做法”,由于 Compose 中没有 View 这个概念了,因此只能说是与自定义 View 在 Compose 中的等价概念,View 的布局在 Compose 中也能做,并且 Layout 属于这部分内容中比较入门的知识。

LayoutModifier 非常重要,它会修改组件的尺寸和位置,像 Modifier 的 padding() 和 size() 等尺寸与位置相关函数创建出的 Modifier 都是实现了 LayoutModifier 接口的。

1.1 Modifier.layout() 用法

我们举一个简单的例子说明它的用法,然后再结合示例代码介绍 Modifier.layout() 中涉及到的各种参数与接口,这样叙述起来会有具体的代码参照,方便理解。

以下是一个非常简单的 Modifier.layout() 的用例,修饰一个 Text 组件:

@Composable
fun LayoutModifierSample() {// Box Box(Modifier.background(Color.Yellow)) {Text("Compose", Modifier.layout { measurable, constraints ->val placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {// 单位是像素,placeRelative() 自适应 RTL 布局,如果不需要 RTL 就使用 place// 多数时候用 placeRelative(),少数时候用 place(),比如绘制国旗时,与 RTL 无关,// 不管是不是 RTL,国旗都是一个方向的,不会反向placeable.placeRelative(0, 0)}})}
}

它的效果如下:

请添加图片描述

就是以默认方式显示了一个 Text,没有做特殊的处理。Box 只是为了让 Text 只使用 layout() 不再引入更多 Modifier 的基础上,让 Text 能有个背景,以便观察。

接下来看 Modifier.layout() 的参数,它只有一个函数类型的参数 measure:

/**
* 创建一个 LayoutModifier,允许更改包装元素的测量和布局方式。
* 这是一个便利的 API,用于创建一个自定义的 LayoutModifier 修饰符,而无需创建实现 
* LayoutModifier 接口的类或对象。内在的测量遵循 LayoutModifier 提供的默认逻辑。
*/
fun Modifier.layout(measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this.then(LayoutModifierImpl(measureBlock = measure,inspectorInfo = debugInspectorInfo {name = "layout"properties["measure"] = measure})
)

measure 会提供 Measurable 与 Constraints 两个参数,返回值类型为 MeasureResult,我们需要对这三个类型进行简单了解。

Measurable

Measurable 接口只有一个负责测量被修饰组件的函数 measure():

/**
* 一个可以被测量的组合(composition)的一部分。这代表一个布局。该实例不应该被存储。
*/
interface Measurable : IntrinsicMeasurable {/*** 使用 [constraints] 测量布局,返回一个具有新尺寸的 [Placeable] 布局。在布局过程中,* 一个 [Measurable] 只能被测量一次。*/fun measure(constraints: Constraints): Placeable
}

Measurable 表示被修饰组件及其之前所有修饰符的聚合状态,在示例中,就是经过 Modifier.background(Color.Yellow) 和自定义 Modifier.layout() 之前的修饰符处理后的 Text 组件。由于我们对 Compose 的机制还没有深入了解,因此这里可以暂时粗略地认为, Modifier.layout() 的 measure 参数提供的 Measurable 对象,就是 Modifier.layout() 所修饰的组件 —— Text。

Constraints

Modifier.layout() 的 measure 还提供了一个 Constraints 参数,用于调用 Measurable 的 measure() 时传参对组件进行测量。这个 Constraints 是外层组件对被修饰组件的原始尺寸限制,当使用 Modifier.layout() 修饰组件后,限制的传递链就变为外层组件 -> LayoutModifier -> 被修饰组件,因此,原本外层组件对被修饰组件的限制,由于 LayoutModifier 在中间插了一层,就变成了对 LayoutModifier 的限制。

Constraints 限制的具体内容,我们当前会用到最大/最小宽高,单位是像素:

/**
* 不可变的约束用于测量布局,被布局或布局修饰符用来测量它们的布局子元素。父级选择定义一个范围的
* 约束,即在像素范围内,被测量的布局应该选择一个尺寸:
* minWidth <= chosenWidth <= maxWidth
* minHeight <= chosenHeight <= maxHeight
*/
@Immutable
@kotlin.jvm.JvmInline
value class Constraints(@PublishedApi internal val value: Long
) {val minWidth: Intget() {val mask = WidthMask[focusIndex]return ((value shr 2).toInt() and mask)}val maxWidth: Intget() {val mask = WidthMask[focusIndex]val width = ((value shr 33).toInt() and mask)return if (width == 0) Infinity else width - 1}val minHeight: Intget() {val focus = focusIndexval mask = HeightMask[focus]val offset = MinHeightOffsets[focus]return (value shr offset).toInt() and mask}val maxHeight: Intget() {val focus = focusIndexval mask = HeightMask[focus]val offset = MinHeightOffsets[focus] + 31val height = (value shr offset).toInt() and maskreturn if (height == 0) Infinity else height - 1}fun copy(minWidth: Int = this.minWidth,maxWidth: Int = this.maxWidth,minHeight: Int = this.minHeight,maxHeight: Int = this.maxHeight): Constraints {require(minHeight >= 0 && minWidth >= 0) {"minHeight($minHeight) and minWidth($minWidth) must be >= 0"}require(maxWidth >= minWidth || maxWidth == Infinity) {"maxWidth($maxWidth) must be >= minWidth($minWidth)"}require(maxHeight >= minHeight || maxHeight == Infinity) {"maxHeight($maxHeight) must be >= minHeight($minHeight)"}return createConstraints(minWidth, maxWidth, minHeight, maxHeight)}
}

MeasureResult

了解了 Measurable 与 Constraints 的含义后,可以调用 Measurable 的 measure() 传入 Constraints 即可对 Modifier.layout() 修饰的组件(示例代码中就是 Text)组件进行测量,测量结果是一个 Placeable,通过它可以获取到测量后的组件宽高用于返回结果。

说到返回结果,measure 参数要求返回结果类型为 MeasureResult:

/**
* 接口保存测量布局的大小和对齐线,以及子元素定位逻辑。placeChildren 是用于定位子元素的函数。在 
* placeChildren 中应该调用 Placeable.placeAt 来放置子元素。对齐线可以被父布局用来决定布局,并且
* 可以使用 Placeable.get 操作符进行查询。请注意,对齐线将被父布局继承,因此间接父级也能够查询它们。
*/
interface MeasureResult {val width: Intval height: Intval alignmentLines: Map<AlignmentLine, Int>fun placeChildren()
}

通常我们只需要指定组件的宽高,不需要定制对齐线以及子元素的布局策略,所以可以直接使用 MeasureScope 接口的 layout(),该函数实现了 MeasureResult 并提供了 alignmentLines 和 placeChildren 的实现:

/**
* 布局的测量 lambda 的接收者作用域(receiver scope)。测量 lambda 的返回值是 MeasureResult,
* 应该被布局返回。
*/
@JvmDefaultWithCompatibility
interface MeasureScope : IntrinsicMeasureScope {fun layout(width: Int,height: Int,alignmentLines: Map<AlignmentLine, Int> = emptyMap(),placementBlock: Placeable.PlacementScope.() -> Unit) = object : MeasureResult {override val width = widthoverride val height = heightoverride val alignmentLines = alignmentLinesoverride fun placeChildren() {Placeable.PlacementScope.executeWithRtlMirroringValues(width,layoutDirection,this@MeasureScope as? LookaheadCapablePlaceable,placementBlock)}}
}

调用 MeasureScope 的 layout() 可以只传宽高,然后给出 placementBlock 的实现,内容是如何摆放组件内容,像示例中使用的 placeable.placeRelative(0, 0) 就是摆放在原始位置。假如设置了 placeable.placeRelative(20, 20),那么文字内容就会在两个方向上偏移 20 个像素:

请添加图片描述

1.2 对测量与布局的思考

是否觉得通过 Modifier.layout() 来控制组件的测量与布局的代码有些不够简单明了?既要传入 lambda 进行测量,又要调用另一个 layout() 进行布局,不像 View 体系下重写 onMeasure() 和 onLayout() 那样直观:

class SquareImageView(context: Context, attrs: AttributeSet?) : ImageView(context, attrs) {override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {// 先让其自己测量大小是多少super.onMeasure(widthMeasureSpec, heightMeasureSpec)// 选择较小值作为新的正方形边长val size = min(measuredWidth, measuredHeight)// 保存结果setMeasuredDimension(size, size)}override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {super.onLayout(changed, left, top, right, bottom)}
}

这是因为 Modifier.layout() 实际上是将测量与布局两个任务合到一起做了,自然就没有 onMeasure() 只负责测量,onLayout() 只负责布局那样清晰,这是“既要又要”所需付出的代价。假如使用我们的例子实现上述 View 体系下相同的测量与布局效果,需要写成这样:

@Composable
fun LayoutModifierSample1() {Box(Modifier.background(Color.Yellow)) {Text("Compose", Modifier.layout { measurable, constraints ->val placeable = measurable.measure(constraints)// 修改尺寸让 Text 变成正方形val size = min(placeable.width, placeable.height)// layout() 内传入最终宽高layout(size, size) {placeable.placeRelative(0, 0)}})}
}

这样就实现了一个正方形的 Text。

此外,还需着重注意的是,Modifier.layout() 的 placementBlock 这个函数参数实现的对组件的测量与布局,与 onMeasure() 的测量、onLayout() 的布局的功能不等价。

具体说来,在测量角度,二者只是在“对自身测量出的尺寸进行修改”这个功能上等价。onMeasure() 通常是先测量自己内部的所有子组件的尺寸,然后再测量自身尺寸,而 Modifier.layout() 只能测量自身尺寸然后附加到所服务的组件上,它不像 onMeasure() 那样在组件内部,可以拿到组件的内部属性进而进行内部组件的计算以及辅助测量。Modifier.layout() 由于这部分的信息缺失,导致在测量时不像 onMeasure() 那样自由,使得它的功能受限,只能做这种上面 LayoutModifierSample1() 那样,先测量自身的尺寸,然后用这个结果进行计算,再保存最终结果这个非常受限的场景。

在布局角度,placementBlock 只是对单个组件做整体偏移的,它不能像 onLayout() 那样对组件内部的子组件做精细化摆放。任何 Modifier 都是做不到的,只能使用 Layout() 这个可组合函数。

总结起来,Modifier.layout() 就是用来修改被修饰的组件的尺寸和(整体)位置偏移的。

1.3 使用场景

何时才会用到 Modifier.layout() 修改组件的尺寸与位置呢?由于 Modifier.layout() 是一个比较通用的 API,大多数时候会有比它更直接、好用的选择,因此容易被遗忘。但在一些不常见的场景中,当常用选择不适用时,就需要它登场了。

Modifier.layout() 的本质作用是给组件在位置和尺寸方面增加装饰效果。不干涉组件内部的测量和布局规则(前面说了,因为它拿不到组件内部属性,因此无法干涉内部),只从外部增加一些额外规定。

比如,假如想用 Modifier.layout() 增加被修饰组件的 padding:

@Composable
fun LayoutModifierSample2() {Box(Modifier.background(Color.Yellow)) {Text("Compose", Modifier.layout { measurable, constraints ->// 为 Text 在四个方向增加 10dp 的 Paddingval paddingPx = 10.dp.roundToPx()val placeable = measurable.measure(// 不要修改原 constraints,copy 一份,更新最大宽度与高度constraints.copy(maxWidth = constraints.maxWidth - paddingPx * 2,maxHeight = constraints.maxHeight - paddingPx * 2))// layout() 保存测量结果时,需要用测量结果加上内边距才是该组件的总尺寸layout(placeable.width + paddingPx * 2, placeable.height + paddingPx * 2) {placeable.placeRelative(paddingPx, paddingPx)}})}
}

这个代码与 Modifier.padding() 内的 PaddingModifier 的核心代码是类似的,了解原理后,你不止可以实现 Padding,实现装饰效果的原理都是类似的。

总结:Modifier.layout() 是干嘛的?定制被修饰组件的尺寸与位置,且不会干涉组件内部的测量与布局。

2、LayoutModifier 的工作原理和对布局的精细影响

Modifier.layout() 内部实际上是通过 then() 拼接了一个 LayoutModifierImpl,它是 LayoutModifier 接口的实现类,LayoutModifier 是 Modifier.Element 的子接口:

/**
* 一个 [Modifier.Element],用于改变其包裹内容的测量和布局方式。
* 由于它是一个修饰符,它具有与 [androidx.compose.ui.layout.Layout] 组件相同的测量和布局功能,同时
* 包裹了一个布局。相比之下,[androidx.compose.ui.layout.Layout] 组件用于定义多个子元素的布局行为。
*/
@JvmDefaultWithCompatibility
interface LayoutModifier : Modifier.Element {/*** 用于测量修饰符的函数。[measurable] 对应于被包裹的内容,可以根据[LayoutModifier] 的逻辑* 使用所需的约束进行测量。修饰符需要选择自己的大小,这个大小可以取决于被包裹内容*(获取的 [Placeable])选择的大小,如果被包裹的内容已经被测量。大小需要作为 [MeasureResult] * 的一部分返回,同时还包括 [Placeable] 的放置逻辑,定义了被包裹内容应该如何在 [LayoutModifier] * 中定位。创建 [MeasureResult] 的一种便捷方式是使用 [MeasureScope.layout] 工厂函数。*/fun MeasureScope.measure(measurable: Measurable,constraints: Constraints): MeasureResult
}

与组件尺寸、位置相关的 Modifier 都实现了这个接口,比如 Modifier.padding() 内的 PaddingModifier、Modifier.width()、Modifier.height()、Modifier.size() 内的 SizeModifier。

我们在描述界面时使用的 Composable 函数,如 Box()、Text() 等,它们对应组件在运行时不是以函数形式保存在内存中的,而是以 LayoutNode 的形式,由 LayoutNode 进行实际的测量、布局、绘制、触摸反馈等等工作。接下来我们要先看 LayoutNode 是如何进行测量与布局的,再看 LayoutModifier 是如何影响测量和布局的。

2.1 测量过程

LayoutNode 的 remeasure() 负责测量,replace() 负责布局:

internal class LayoutNode() {// 测量internal fun remeasure(constraints: Constraints? = layoutDelegate.lastConstraints): Boolean {return if (constraints != null) {if (intrinsicsUsageByParent == UsageByParent.NotUsed) {// This LayoutNode may have asked children for intrinsics. If so, we should// clear the intrinsics usage for everything that was requested previously.clearSubtreeIntrinsicsUsage()}// measurePassDelegate 是 LayoutNode 的测量过程委托,负责实际的测量和布局,在任何预查之后measurePassDelegate.remeasure(constraints)} else {false}}// 布局internal fun replace() {if (intrinsicsUsageByParent == UsageByParent.NotUsed) {// This LayoutNode may have asked children for intrinsics. If so, we should// clear the intrinsics usage for everything that was requested previously.clearSubtreePlacementIntrinsicsUsage()}try {relayoutWithoutParentInProgress = truemeasurePassDelegate.replace()} finally {relayoutWithoutParentInProgress = false}}
}

追溯测量过程,进入到 MeasurePassDelegate 的 remeasure():

		fun remeasure(constraints: Constraints): Boolean {...if (layoutNode.measurePending || measurementConstraints != constraints) {...performMeasure(constraints)...}...}

会调用 MeasurePassDelegate 所在的外部类 LayoutNodeLayoutDelegate 的 performMeasure():

	private fun performMeasure(constraints: Constraints) {...layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(layoutNode,affectsLookahead = false) {outerWrapper.measure(constraints)}...}

再点进 outerWrapper.measure() 一看,是个似曾相识的接口:

interface Measurable : IntrinsicMeasurable {fun measure(constraints: Constraints): Placeable
}

将光标放在接口名 Measurable 或接口函数 measure 上,点击 ctrl + alt + B 可以快速查看接口实现类。

这样的话我们需要弄清 outerWrapper 的具体类型,然后再看该类型内的 measure() 实现。outerWrapper 是 LayoutNodeLayoutDelegate 类内的一个属性:

internal class LayoutNodeLayoutDelegate(private val layoutNode: LayoutNode,var outerWrapper: LayoutNodeWrapper
) {internal val measurePassDelegate = MeasurePassDelegate()
}

这样我们要看 LayoutNodeLayoutDelegate 在实例化时给 outerWrapper 传的是什么类型。首先我们先记住,measurePassDelegate 是 LayoutNodeLayoutDelegate 的属性,然后往回找,看测量过程调用链的源头,LayoutNode 的 remeasure() 内的 measurePassDelegate.remeasure(constraints),看 measurePassDelegate 是从哪个对象获取的,该对象就是 LayoutNodeLayoutDelegate 的实例:

internal class LayoutNode(private val isVirtual: Boolean = false
) : Remeasurement, OwnerScope, LayoutInfo, ComposeUiNode,Owner.OnLayoutCompletedListener {private val measurePassDelegateget() = layoutDelegate.measurePassDelegate
}

layoutDelegate 就是我们要找的 LayoutNodeLayoutDelegate 实例,看它的初始化:

	internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)internal val layoutDelegate = LayoutNodeLayoutDelegate(this, innerLayoutNodeWrapper)

构造函数的第二个参数类型是 InnerPlaceable,也就是 outerWrapper 的真实类型。看这个类的 measure():

	override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {// before rerunning the user's measure block reset previous measuredByParent for childrenlayoutNode.forEachChild {it.measuredByParent = LayoutNode.UsageByParent.NotUsed}measureResult = with(layoutNode.measurePolicy) {layoutNode.measureScope.measure(layoutNode.childMeasurables, constraints)}onMeasured()return this}

通过 outerCoordinator 的 get() 找到 NodeChain 的 outerCoordinator 属性:

internal class NodeChain(val layoutNode: LayoutNode) {internal val innerCoordinator = InnerNodeCoordinator(layoutNode)internal var outerCoordinator: NodeCoordinator = innerCoordinatorprivate set
}

outerCoordinator 被初始化为 innerCoordinator,而 innerCoordinator 是 InnerNodeCoordinator 类型的,因此 outerCoordinator 的实际类型就是 InnerNodeCoordinator。看 InnerNodeCoordinator 的 measure():

	override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {// before rerunning the user's measure block reset previous measuredByParent for childrenlayoutNode.forEachChild {it.measuredByParent = LayoutNode.UsageByParent.NotUsed}measureResult = with(layoutNode.measurePolicy) {layoutNode.measureScope.measure(layoutNode.childMeasurables, constraints)}onMeasured()return this}

在这里,调用 MeasurePolicy 的 measure() 完成对组件的测量工作,并将测量结果保存到 measureResult 中。由于 MeasureResult 内有一个 placeChildren() 用于摆放内部的子组件,所以实际上,在进行测量时,已经将如何摆放计算出来了,后续在布局流程中,只需要调用 placeChildren() 进行摆放即可。可以说测量完成了测量与布局这部分中,大部分的工作。

2.2 LayoutModifier 工作原理

我们描述页面时使用的 Composable 函数,如 Box()、Text() 等经过处理都会变成 LayoutNode,而给这些函数设置的 Modifier 经过一系列处理也会成为 LayoutNode 的 modifier 属性。这个处理包括我们前面说的调用 ComposedModifier 的工厂函数,用真正起作用的 Modifier 替换掉 ComposedModifier,当然也会有一些其他处理,最终成为 LayoutNode 的 modifier 属性,LayoutModifier 的工作原理就包含在这个属性中。

我们来看 modifier 属性的 set 函数:

	override var modifier: Modifier = Modifierset(value) {...// Create a new chain of LayoutNodeWrappers, reusing existing ones from wrappers// when possible.// foldOut() 逆向(从右向左)遍历 modifier 链,参数 mod 是本次遍历到的 Modifier 对象,// toWrap 是本次遍历开始前的初始值,也是前面遍历的结果val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->if (mod is RemeasurementModifier) {mod.onRemeasurementAvailable(this)}toWrap.entities.addBeforeLayoutModifier(toWrap, mod)if (mod is OnGloballyPositionedModifier) {getOrCreateOnPositionedCallbacks() += toWrap to mod}// 尽量重用,如果不行则创建新的 ModifiedLayoutNode 作为 wrapper 结果val wrapper = if (mod is LayoutModifier) {// Re-use the layoutNodeWrapper if possible.(reuseLayoutNodeWrapper(toWrap, mod)?: ModifiedLayoutNode(toWrap, mod)).apply {onInitialize()updateLookaheadScope(mLookaheadScope)}} else {// 如果不是 LayoutModifier,则让 toWrap 作为 wrappertoWrap}wrapper.entities.addAfterLayoutModifier(wrapper, mod)// 返回 wrapper 作为 outerWrapperwrapper}setModifierLocals(value)outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper// 用前面计算出的 outerWrapper 替换掉 layoutDelegate 的同名属性,也就是替换掉// 初始的 innerLayoutNodeWrapperlayoutDelegate.outerWrapper = outerWrapper...}

foldOut() 指定遍历的初始值是 innerLayoutNodeWrapper,从右向左遍历时,如果遇到的不是 LayoutModifier,就让 toWrap 作为 wrapper,否则用 ModifiedLayoutNode 将 mod 和 toWrap 包起来。这样计算完 wrapper 作为 outerWrapper 替换掉 layoutDelegate.outerWrapper 的原属性值,也就是初始值 innerLayoutNodeWrapper。

我们举几个例子帮助理解。

假如不设置 Modifier,那么经过 foldOut() 计算的结果就是初始值 innerLayoutNodeWrapper,使用其测量 Composable 函数;

假如通过 Modifier.layout {...} 设置了一个 LayoutModifier,那么 outerWrapper 的结构就是:

ModifiedLayoutNode[LayoutModifier+innerLayoutNodeWrapper
]

假如设置了两个 LayoutModifier,那么 outerWrapper 的结构就是:

ModifiedLayoutNode[LayoutModifier+ModifiedLayoutNode[LayoutModifier+innerLayoutNodeWrapper]
]

下面来看 ModifiedLayoutNode 是如何进行测量的:

	override fun measure(constraints: Constraints): Placeable {performingMeasure(constraints) {with(modifier) { // this: LayoutModifiermeasureResult = measureScope.measure(wrapped, constraints)this@ModifiedLayoutNode}}onMeasured()return this}

performingMeasure() 通过其 lambda 表达式参数获取测量结果,所以我们主要看大括号的内容。with() 会调用给定的接收者的 lambda 函数并将 lambda 的结果返回:

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {contract {callsInPlace(block, InvocationKind.EXACTLY_ONCE)}return receiver.block()
}

在这里,with() 实际上是在为 MeasureScope.measure() 提供调用环境,因为 MeasureScope.measure() 是 LayoutModifier 接口的函数:

@JvmDefaultWithCompatibility
interface LayoutModifier : Modifier.Element {fun MeasureScope.measure(measurable: Measurable,constraints: Constraints): MeasureResult
}

函数的具体内容也是由 with() 参数内 LayoutModifier 的实现类决定的。比如我们使用的 Modifier.layout() 内生成的 LayoutModifierImpl 是直接将构造函数上传入的测量函数作为对 LayoutModifier 接口的 measure() 的实现:

private class LayoutModifierImpl(val measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult,inspectorInfo: InspectorInfo.() -> Unit,
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {override fun MeasureScope.measure(measurable: Measurable,constraints: Constraints) = measureBlock(measurable, constraints)
}

这里可以和我们使用 Modifier.layout() 的代码连接上:

@Composable
fun LayoutModifierSample() {Box(Modifier.background(Color.Yellow)) {     Text("Compose", Modifier.layout { measurable, constraints ->val placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {placeable.placeRelative(0, 0)}})}
}

Modifier.layout() 的 lambda 参数就是 LayoutModifierImpl 构造函数的 measureBlock 参数,向上找,也就是 ModifiedLayoutNode 的 measure() 的 with() 内调用的 measureScope.measure(wrapped, constraints),参数 wrapped 是 ModifiedLayoutNode 构造函数的第一个参数,我们前面说过,这个参数的类型是不确定的,如果不设置 Modifier 或有一层就是 innerLayoutNodeWrapper,如果有两层 LayoutModifier 就是 ModifiedLayoutNode。

回到现实,来看一些可能会出现的 Modifier 写法。

Text("Compose", Modifier.padding(10.dp).padding(20.dp))

显示时 Text 的 Padding 会是多少呢?答案是 30dp。因为上述代码可以化为如下结构:

[LayoutModifier - 10.dp ModifiedLayoutNode[LayoutModifier - 20.dp ModifiedLayoutNode实际组件 Text() innerLayoutNodeWrapper - InnerPlaceable]
]

最外层知道要增加 10dp,但是不知道基于怎样的内部数据增加,所以会去测量内部,这样就到了 LayoutModifier - 20.dp,类似的原因会再测内部的 Text,Text 根据 innerLayoutNodeWrapper 提供的算法去测量,测出的结果值返回给上一层的 LayoutModifier - 20.dp,在结果上增加 20dp 再向上返回给 LayoutModifier - 10.dp,再加 10dp 得到 30dp 的 Padding。

再看一个例子:

Box(Modifier.padding(10.dp).size(80.dp).background(Color.Blue))

会得到一个 80dp 的正方形,padding 为 10dp。下面增加 padding 到 40dp:

Box(Modifier.padding(40.dp).size(80.dp).background(Color.Blue))

正方形大小不会因为 padding 的增加而变小,仍是 40dp。因为是先测量右侧的尺寸,因此先有的 80dp,然后在 80dp 的基础上更改 padding 大小。

再看:

Box(Modifier.size(40.dp).size(80.dp).background(Color.Blue))
Box(Modifier.size(40.dp).requiredSize(80.dp).background(Color.Blue))

两种情况的 Box 尺寸如何呢?上面 40,下面 80。这是因为,size() 指定的是首选大小,后续的测量约束(Modifier 链在 size() 左侧的函数)可能会覆盖此值,强制内容尺寸变得更大或更小;而 requiredSize() 声明的是一个精确尺寸,后续的测量约束不会覆盖此值。如果内容选择的大小不满足传入的约束,父布局将报告一个在约束范围内强制的大小,并且内容的位置将自动偏移以使其居中于父布局为子元素分配的空间,假设约束被遵守。

对第一种情况,size(80.dp) 先测量内部大小,它想要的尺寸是 80dp,但是由于上一层的 Modifier size(40.dp) 把尺寸限制到 40dp 了,因此最终的测量结果是 40dp。

第二种情况,requiredSize(80.dp) 会强制性的要求 80dp 无视 size(40.dp) 的限制,虽然确实获得了 80dp 的尺寸,但是 size(40.dp) 会将 80 裁剪成 40,因此最终显示的大小是 40dp,对于例子中的纯色背景看起来效果一样,但假如 Box 内放了图片,就能看出,这个图片只能显示出 1/4,因为是被裁剪过的。

再看两种情况:

Box(Modifier.requiredSize(80.dp).size(40.dp).background(Color.Blue))
Box(Modifier.size(80.dp).requiredSize(40.dp).background(Color.Blue))

第一种,requiredSize() 强制要求 Box 尺寸为 80,因此会显示一个 80dp 的 Box。

第二种,requiredSize() 强制要求 Box 尺寸为 40,但 size() 首选尺寸为 80,这样会显示一个尺寸为 40dp 的 Box,同时为了满足 size() 的要求,会将 Box 在摆放在 80 的中间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/34625.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

stl之string的详解

一&#xff0c;string定义的方式 &#xff0c;string定义了多种函数重载的方式&#xff0c;常用的构造函数如下&#xff1a; string(); string(const string& str); string(const string& str, size_t pos, size_t len npos); string(const char* s); string(const …

Leetcode-131.Palindrome Partitioning [C++][Java]

目录 一、题目描述 二、解题思路 【C】 【Java】 Leetcode-131.Palindrome Partitioninghttps://leetcode.com/problems/palindrome-partitioning/description/131. 分割回文串 - 力扣&#xff08;LeetCode&#xff09;131. 分割回文串 - 给你一个字符串 s&#xff0c;请你…

InternVL:论文阅读 -- 多模态大模型(视觉语言模型)

更多内容&#xff1a;XiaoJ的知识星球 文章目录 InternVL: 扩展视觉基础模型与通用视觉语言任务对齐1.概述2.InternVL整体架构1&#xff09;大型视觉编码器&#xff1a;InternViT-6B2&#xff09;语言中间件&#xff1a;QLLaMA。3&#xff09;训练策略&#xff08;1&#xff09…

【AWS入门】AWS云计算简介

【AWS入门】AWS云计算简介 A Brief Introduction to AWS Cloud Computing By JacksonML 什么是云计算&#xff1f;云计算能干什么&#xff1f;我们如何利用云计算&#xff1f;云计算如何实现&#xff1f; 带着一系列问题&#xff0c;我将做一个普通布道者&#xff0c;引领广…

二分算法刷题

1. 初识 总结&#xff1a;二分算法题的细节非常多&#xff0c;容易写出死循环。使用算法的条件不一定是数组有序&#xff0c;而是具有“二断性”&#xff1b;模板三种后面会讲。 朴素二分二分查找左端点二分查找右端点 2. 朴素二分 题目链接&#xff1a;704. 二分查找 - 力扣…

itsdangerous加解密源码分析|BUG汇总

这是我这两天的思考 早知道密码学的课就不旷那么多了 纯个人见解 如需转载&#xff0c;标记出处 目录 一、官网介绍 二、事例代码 源码分析&#xff1a; 加密函数dump源码使用的函数如下&#xff1a; 解密 ​编辑 ​编辑 关于签名&#xff1a; 为什么这个数字签名没有…

深度解析React Native底层核心架构

React Native 工作原理深度解析 一、核心架构&#xff1a;三层异构协作体系 React Native 的跨平台能力源于其独特的 JS层-Shadow层-Native层 架构设计&#xff0c;三者在不同线程中协同工作&#xff1a; JS层 运行于JavaScriptCore&#xff08;iOS&#xff09;或Hermes&…

前端内存优化实战指南:从内存泄漏到性能巅峰

前端内存优化实战指南&#xff1a;从内存泄漏到性能巅峰 一、内存问题引发的场景 1.1 典型内存灾难现场 // 经典内存泄漏示例 const zombieElements new Set();function createLeak() {const div document.createElement(div);zombieElements.add(div); // 元素永不释放div…

【工作记录】pytest使用总结

1、 fixture夹具 可参考&#xff1a; python3.x中 pytest之fixture - 漂泊的小虎 - 博客园 fixture是指夹具&#xff08;把用例夹在中间&#xff09;&#xff0c;它包括前置工作和后置工作&#xff0c;前置是用例代码的准备阶段&#xff0c;后置是用例执行之后的清理阶段,用…

C++基础笔记

1. C关键字 这个不多说&#xff0c;以后接触得到&#xff0c;但这里做个总结&#xff1a; 2. 命名空间 一般类型&#xff1a; namespace Xianyu {// 命名空间中可以定义变量/函数/类型int rand 10;int Add(int left, int right){return left right;}struct Node{struct No…

生活中的可靠性小案例12:类肤材质老化发粘问题

我一直觉得我买的某品牌车载吸尘器很好用&#xff0c;用了几年&#xff0c;目前性能也是杠杠的。然而它现在有个最大的问题&#xff0c;就是表面发粘了&#xff0c;用起来粘手&#xff0c;非常不舒服。 这一类问题在生活中不少见&#xff0c;尤其是一些用了类肤材质涂层的物件。…

黑马node.js教程(nodejs教程)——AJAX-Day01-04.案例_地区查询——查询某个省某个城市所有地区(代码示例)

文章目录 代码示例效果 代码示例 axiosTest.html <!DOCTYPE html> <!-- 文档类型声明&#xff0c;告诉浏览器这是一个HTML5文档 --> <html lang"en"> <!-- HTML根元素&#xff0c;设置文档语言为英语 --><head> <!-- 头部区域&am…

Ollama+OpenWebUI本地部署大模型

OllamaOpenWebUI本地部署大模型 前言Ollama使用Ollama安装Ollama修改配置Ollama 拉取远程大模型Ollama 构建本地大模型Ollama 运行本地模型&#xff1a;命令行交互Api调用Web 端调用 总结 前言 Ollama是一个开源项目&#xff0c;用于在本地计算机上运行大型语言模型&#xff0…

【NeurIPS 2024】LLM-ESR:用大语言模型破解序列推荐的长尾难题

标题期刊年份关键词LLM-ESR: Large Language Models Enhancement for Long-tailed Sequential RecommendationNeurIPS2024Large Language Models, Sequential Recommendation, Long-tailed &#x1f4da;研究背景 在电商和社交媒体的世界里&#xff0c;序列推荐系统&#xff…

C语言_数据结构总结9:树的基础知识介绍

1. 树的基本术语 - 祖先&#xff1a;考虑结点K&#xff0c;从根A到结点K的唯一路径上的所有其它结点&#xff0c;称为结点K的祖先。 - 子孙&#xff1a;结点B是结点K的祖先&#xff0c;结点K是B的子孙。结点B的子孙包括&#xff1a;E,F,K,L。 - 双亲&#xff1a;路径上…

Android 14 Telephony 网络选择功能介绍

一、总体介绍 (一)功能 手动搜网的流程:用户通过UI触发,调用TelephonyManager的API,比如startNetworkScan,然后这个请求会传递到RIL层,通过AT命令与基带通信,进行网络扫描。结果返回后,经过TelephonyRegistry通知应用层。中间可能涉及IPC,比如Binder通信,因为应用和…

系统思考全球化落地

感谢加密货币公司Bybit的再次邀请&#xff0c;为全球团队分享系统思考课程&#xff01;虽然大家来自不同国家&#xff0c;线上学习的形式依然让大家充满热情与互动&#xff0c;思维的碰撞不断激发新的灵感。 尽管时间存在挑战&#xff0c;但我看到大家的讨论异常积极&#xff…

位运算(基础算法)

按位与AND&#xff08; & &#xff09; 只有当两个位都为1时&#xff0c;结果才为1,否则为0。结果不会变大 按位或 OR&#xff08; | &#xff09; 只有当两个位中有一个为1时&#xff0c;结果才为1,否则为0。结果不会变小 按位异或 XOR &#xff08; ^ &#xff09; 只…

规模效应的三重边界:大白话解读-deepseek为例

前言&#xff1a;当Scaling Laws遇见边际递减效应 在人工智能的狂飙突进中&#xff0c;大语言模型如同不断膨胀的星体&#xff0c;吞噬着海量算力与数据。OpenAI于2020年揭开的Scaling Laws&#xff0c;曾为这场盛宴指明方向&#xff1a;模型性能随参数规模&#xff08;N&…

力扣143重排链表

143. 重排链表 给定一个单链表 L 的头节点 head &#xff0c;单链表 L 表示为&#xff1a; L0 → L1 → … → Ln - 1 → Ln 请将其重新排列后变为&#xff1a; L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → … 不能只是单纯的改变节点内部的值&#xff0c;而是需要实际的…