当前位置: 首页 > article >正文

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: Int
        get() {
            val mask = WidthMask[focusIndex]
            return ((value shr 2).toInt() and mask)
        }

    val maxWidth: Int
        get() {
            val mask = WidthMask[focusIndex]
            val width = ((value shr 33).toInt() and mask)
            return if (width == 0) Infinity else width - 1
        }

    val minHeight: Int
        get() {
            val focus = focusIndex
            val mask = HeightMask[focus]
            val offset = MinHeightOffsets[focus]
            return (value shr offset).toInt() and mask
        }

    val maxHeight: Int
        get() {
            val focus = focusIndex
            val mask = HeightMask[focus]
            val offset = MinHeightOffsets[focus] + 31
            val height = (value shr offset).toInt() and mask
            return 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: Int
    val height: Int
    val 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 = width
        override val height = height
        override val alignmentLines = alignmentLines
        override 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 的 Padding
            val 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 = true
            measurePassDelegate.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 measurePassDelegate
        get() = 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 children
        layoutNode.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 = innerCoordinator
        private 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 children
        layoutNode.forEachChild {
            it.measuredByParent = LayoutNode.UsageByParent.NotUsed
        }

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

原本使用的是 Compose UI 1.3.0 正式版本,与课程使用的 1.3.0-alpha1 代码不太一样,路径不同,但最后 measure() 的内容大体相同。本来想用 1.3.0 继续看了,但是后面 LayoutModifier 工作原理的源码解析,不同之处太多,为了省去源码版本不同带来的麻烦,在这里就改成与课程一致的版本了。

在这里,调用 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 = Modifier
        set(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 作为 wrapper
                    toWrap
                }
                wrapper.entities.addAfterLayoutModifier(wrapper, mod)
                // 返回 wrapper 作为 outerWrapper
                wrapper
            }

            setModifierLocals(value)

            outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
            // 用前面计算出的 outerWrapper 替换掉 layoutDelegate 的同名属性,也就是替换掉
            // 初始的 innerLayoutNodeWrapper
            layoutDelegate.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: LayoutModifier
                measureResult = 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.kler.cn/a/585536.html

相关文章:

  • 批量删除或替换 Excel 的 Sheet 工作表
  • Cesium零基础速成教程:一小时入门Cesium
  • 图注意力循环神经网络(AGCRN):基于图嵌入的时间序列预测
  • 【C++】每日一练(链表的中间结点)
  • 数据库系统导论 15-445 2023Fall part1
  • 深度学习有哪些算法?
  • IP风险度自检,互联网的安全“指南针”
  • 网络安全态势感知产品设计原则
  • 《灵珠觉醒:从零到算法金仙的C++修炼》卷三·天劫试炼(50)六魂幡控流量 - 最大网络流(Ford-Fulkerson)
  • 浅谈Linux中的Shell及其原理
  • 使用 Python 爬取微店关键词搜索接口(micro.item_search)的完整指南
  • 【赵渝强老师】达梦数据库的目录结构
  • 基于图像比对的跨平台UI一致性校验工具开发全流程指南——Android/iOS/Web三端自动化测试实战
  • Safe “AI Agentathon 2025”:加密领域的 AI Agent 开发者盛会
  • [C语言基础]13.动态内存管理
  • Centos离线安装gcc
  • 探索CSS魔法:3D翻转与渐变光效的结合
  • 品铂科技高精度UWB定位系统助力2018年北京冬奥会
  • k8s基础架构介绍
  • 一般机器学习有哪些算法?