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

上篇文章我们介绍了 Modifier 的基础框架,介绍了 Modifier 接口的三个直接后代:Modifier 的伴生对象、CombinedModifier 以及几乎所有 Modifier 的父接口 Modifier.Element,还介绍了用于创建有状态 Modifier 的 ComposedModifier 类以及它的简便函数 Modifier.composed()。从本篇开始,我们会详细介绍具有直接功效的 Modifier,本篇先介绍 LayoutModifier。

1、LayoutModifier 概况

LayoutModifier 非常重要,它可以对单个组件进行测量,并且对测量结果的尺寸和位置进行修改:

/**
 * 一种[Modifier.Element],用于修改其包裹内容的测量与布局方式。
 * 它拥有与[androidx.compose.ui.layout.Layout]组件相同的测量和布局功能,但作为修改器仅作用于单个布局。
 * 相比之下,[androidx.compose.ui.layout.Layout]组件用于定义多个子项的布局行为。
 *
 * @sample androidx.compose.ui.samples.LayoutModifierSample
 */
@JvmDefaultWithCompatibility
interface LayoutModifier : Modifier.Element {
    /**
     * 用于测量此 Modifier 的函数。[measurable]对应被包裹的内容,可根据[LayoutModifier]的逻辑
     * 按所需约束条件对其进行测量。Modifier 需确定自身尺寸,该尺寸可依赖于已测量的包裹
     * 内容(通过获取的[Placeable])的尺寸。
     * 最终尺寸需作为[MeasureResult]的一部分返回,同时包含[Placeable]的定位逻辑,
     * 该逻辑定义被包裹内容在[LayoutModifier]内部的位置。创建[MeasureResult]的便捷方式是
     * 使用[MeasureScope.layout]工厂函数。
     *
     * [LayoutModifier]与[Layout]使用相同的测量和布局概念与原理,唯一区别在于前者仅应用于单个子项。
     * 关于测量和布局的详细说明,请参阅[MeasurePolicy]。
     */
    fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult
}

注释实际上已经涵盖了大部分本篇文章要介绍的内容,在后续章节中会陆续展开详解。目前我们需要了解的是,LayoutModifier 是 Modifier.Element 的子接口,用于修改单个组件包裹内容的测量与布局方式。很多常见的 Modifier 扩展函数内连接的 Modifier 都直接实现了 LayoutModifier 接口的,如 padding() 内连接的 PaddingModifier、size() 内连接的 SizeModifier()。

如果 Compose 提供的现有的 LayoutModifier 的实现类都无法满足业务需求,那么可以使用 LayoutModifier 的 private 实现类 LayoutModifierImpl 来自定义 LayoutModifier,填入你想要的测量与布局逻辑。由于 LayoutModifierImpl 是 private 的,开发者无法直接获取,因此可以使用 Compose 提供的简便函数 Modifier.layout() 来使用 LayoutModifierImpl。

下面我们就来介绍 Modifier.layout(),说说如何自定义单个组件的测量与布局。

2、Modifier.layout()

2.1 基本用法

先来看 Modifier.layout() 的内容再说具体用法:

/**
* 创建一个 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
        }
    )
)

Modifier.layout() 只有一个参数 measure,是一个用于对其修饰的组件进行测量的逻辑,传给 LayoutModifierImpl 的 measureBlock 参数后,当需要进行测量时,LayoutModifierImpl 内的 measure() 就会按照 measureBlock 执行:

private class LayoutModifierImpl(
    val measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult,
    inspectorInfo: InspectorInfo.() -> Unit,
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    // 完全按照 layout() 的 measure 参数提供的逻辑进行测量和布局
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ) = measureBlock(measurable, constraints)
}

也就是说,layout() 参数上的 measure 完全就是自定义测量与布局的逻辑。

下面再来看 layout() 的用法,一个简单的例子:

@Composable
fun LayoutModifierSample() {
    Box(Modifier.background(Color.Yellow)) {
        Text("Compose", Modifier.layout { measurable, constraints ->
            // 1.先使用约束对子组件进行测量(Text 没有子组件就是对其内容进行测量),
            // 得到一个 placeable 对象用于对内容进行布局(摆放)                            
            val placeable = measurable.measure(constraints)
            // 2.由于返回值必须是 MeasureResult,因此通常在测量完毕后调用 layout(),                                 // 把测量结果宽高作为参数传进去
            layout(placeable.width, placeable.height) {
                // 3.最后调用 placeRelative() 在父级坐标系中摆放这个 placeable。坐标传 (0, 0)
                // 表示就从父级坐标系的原点摆放 placeable,不做额外的偏移
                placeable.placeRelative(0, 0)
            }
        })
    }
}

placeRelative() 的单位是像素,它在 MeasureScope.layout() 的内部调用此函数时,会自适应 RTL 布局,但除了该范围就不会自动触发水平镜像。也有少数时候会直接使用不支持 RTL 布局的 place(),比如绘制国旗时,不管是不是 RTL,国旗都是一个方向的,不会反向。

示例代码的效果如下:

请添加图片描述

实际上与 Box(Modifier.background(Color.Yellow)) { Text("Compose") } 的效果是完全一样的,多出的代码只是为了演示自定义测量与布局的实现步骤。下面我们要介绍

接下来我们详细介绍 Modifier.layout() 的 measure 参数上用到的重要成员,帮助我们更好地使用它。不过前面贴出的代码已经表明了 measure 究竟是什么:measure 被 layout() 传给 LayoutModifierImpl 作为 MeasureScope.measure() 的函数体,相当于 measure 就是 LayoutModifierImpl 对 LayoutModifier 接口的 MeasureScope.measure() 的实现。因此 measure 上出现的 Measurable、Constraints 与 MeasureResult 是 LayoutModifier 接口函数的参数类型与返回值类型。

Measurable

Measurable 表示一个可测量的组件,通常是子组件的测量接口,其唯一函数 measure() 用来使用指定的 Constraints 限制来对组件进行测量:

/**
* 一个可以被测量的组合(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

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 的 measure() 传入 Constraints 可对 Modifier.layout() 修饰的组件(示例代码中的 Text)进行测量,测量结果是一个 Placeable,通过它可以获取到测量后的组件宽高作为测量结果。

测量结果的类型是 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 的对象:

/**
* 布局的测量 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
            )
        }
    }
}

这样在调用该函数时可以只传入测量的宽高,如果还需要修改组件内容的摆放位置,还可以给出 placementBlock 的实现。像示例中使用的 placeable.placeRelative(0, 0) 就是摆放在原始位置。假如设置了 placeable.placeRelative(20, 20),那么文字内容就会在两个方向上偏移 20 个像素:

请添加图片描述

2.2 对测量与布局的思考

是否觉得通过 Modifier.layout() 对组件进行测量和布局的代码不够简单明了?既要传入 lambda 表达式,又要在表达式内调用 Measurable.measure() 进行测量、调用 MeasureScope 的 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)
            }
        })
    }
}

此外,还需重点注意,Modifier.layout() 的 placementBlock 这个参数对组件的测量与布局,与 onMeasure() 的测量、onLayout() 的布局的功能不完全等价。因为前面我们也提过,Modifier.layout() 只是对它所修饰的单个组件进行测量和布局,并不能对它所修饰的组件的子组件进行测量和布局。

先从测量角度来看,Modifier.layout() 只能测量自身尺寸然后附加到所修饰的组件上,它不像 onMeasure() 那样本身就在组件内部,可以拿到组件内部属性,并利用这些属性进行内部组件的计算以及辅助测量。Modifier.layout() 由于这部分信息的缺失而功能受限,导致在测量时不像 onMeasure() 那样自由,只能做像上面 LayoutModifierSample1() 那样,先测量自身的尺寸,然后用这个结果进行计算,再保存最终结果这个非常受限的场景。因此,Modifier.layout() 与 onMeasure() 只有在“对自身测量出的尺寸进行修改”这个功能上等价。

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

所以,Modifier.layout() 只对其修饰的单个组件生效,用于修改该组件的尺寸和整体位置偏移。

2.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() 是干嘛的?定制被修饰组件的尺寸与位置,且不会干涉组件内部的测量与布局。

3、LayoutNode 的 modifier 属性

在开始讲解 LayoutModifier 原理之前,必须要插入一个非常重要的前置知识。实际上不光是针对于 LayoutModifier,它对于整个 Modifier 体系都是非常重要的,就是对 LayoutNode 的 modifier 属性的处理。

我们在描述界面时使用的 Composable 函数,如 Box()、Text() 等,它们对应的组件在运行时不是以函数形式保存在内存中的,而是以 LayoutNode 的形式。由 LayoutNode 进行实际的测量、布局、绘制、触摸反馈等等工作。而 LayoutNode 的 modifier 属性就是将 Composable 函数参数上的 modifier 进行处理之后的结果。那这一节我们就来了解为何要处理 modifier 以及如何处理。

3.1 LayoutNodeWrapper

我们先来看一个抽象类 LayoutNodeWrapper,它是一个 LayoutNode 的包装器:

/**
 * Measurable and Placeable type that has a position.
 */
internal abstract class LayoutNodeWrapper(
    internal val layoutNode: LayoutNode
) : LookaheadCapablePlaceable(), Measurable, LayoutCoordinates, OwnerScope, (Canvas) -> Unit

LayoutNodeWrapper 在布局过程中负责传递约束(Constraints)和协调父-子节点的测量结果,确保修饰符的逻辑在正确时机生效。它内部封装了一个 LayoutNode 类型的属性 layoutNode,为后续解析 modifier 链提供了结构上的支持。因为在 LayoutNode 的 modifier 属性的 set() 中,会对原始的 modifier 链进行处理,处理后的结构就都是 LayoutNodeWrapper 的嵌套结构。

具体说来,LayoutNodeWrapper 的两个子类分别负责不同情况下的包装:

internal class InnerPlaceable(
    layoutNode: LayoutNode
) : LayoutNodeWrapper(layoutNode), Density by layoutNode.measureScope

internal class ModifiedLayoutNode(
    override var wrapped: LayoutNodeWrapper,
    var modifier: LayoutModifier
) : LayoutNodeWrapper(wrapped.layoutNode)

InnerPlaceable 只包含一个 LayoutNode,这个 LayoutNode 就是 Modifier 修饰的组件经过处理后形成的节点。比如说 Box(Modifier.size(…)),那 LayoutNode 就是 Box 经过处理后在内存中的表现形式。因此 InnerPlaceable 就是用来包装组件本身的。

而 ModifiedLayoutNode 不仅包装 LayoutNode,还通过 wrapped 包装 LayoutNodeWrapper,也就是它可以包装 InnerPlaceable 或者另外一个 ModifiedLayoutNode,形成嵌套包装结构。此外, ModifiedLayoutNode 的另一个参数是 LayoutModifier,实际上是意味着在处理 LayoutModifier 时,才需要用 ModifiedLayoutNode 将遇到 LayoutModifier 之前的 LayoutNodeWrapper 以及这个 LayoutModifier 本身包装到 ModifiedLayoutNode 的内部。

总之呢,LayoutNodeWrapper 为 Modifier 的遍历形成链式结构提供了结构上的支持,下一小节讲解 modifier 属性的 set() 时就会看到它的用途。

3.2 modifier 的 set 函数

这一次来详细看 LayoutNode modifier 属性的 set(),它最主要的作用是进行了 Modifier 链式结构的重建:

    /**
     * The [Modifier] currently applied to this node.
     */
    override var modifier: Modifier = Modifier
        set(value) {
            ...

            // 1.创建新的 LayoutNodeWrapper 的链条,尽可能重用现有的 LayoutNodeWrapper。
            // 具体做法是用 foldOut() 逆向遍历 modifier 链,将每次遍历到的 Modifier 对象
            // mod 进行分类处理并添加到迭代结果 toWrap,它会作为最终的处理结果赋值给 outerWrapper
            val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
                ...
                // 1.1 在 LayoutModifier 之前添加到 entities 集合中
                toWrap.entities.addBeforeLayoutModifier(toWrap, mod)
                ...                                                 

                // 1.2 如果 mod 是 LayoutModifier,就将 mod 与 toWrap 封装到 ModifiedLayoutNode 中
                val wrapper = if (mod is LayoutModifier) {
                    // 尽量重用 LayoutNodeWrapper,如果不可则创建新的 ModifiedLayoutNode
                    (reuseLayoutNodeWrapper(toWrap, mod)
                        ?: ModifiedLayoutNode(toWrap, mod)).apply {
                        onInitialize()
                        updateLookaheadScope(mLookaheadScope)
                    }
                } else {
                    toWrap
                }
                // 1.3 在 LayoutModifier 之后添加到 entities 集合中
                wrapper.entities.addAfterLayoutModifier(wrapper, mod)
                // 1.4 返回最终的迭代结果给 outerWrapper
                wrapper
            }
            ...

            // 2.用 outerWrapper 替换掉 layoutDelegate 的 outerWrapper 属性
            layoutDelegate.outerWrapper = outerWrapper
            ...
        }

我们一步一步来讲解。

层级结构的构建

首先,foldOut() 的初始值传的是 innerLayoutNodeWrapper,也就是一个 InnerPlaceable,它包装着组件本身被处理后形成的 LayoutNode,因此它负责对组件本身进行测量。比如 Box() 使用默认的 Modifier 参数,也就是 Modifier 伴生对象,那么在通过 foldOut() 计算 outerWrapper 时,得到的就是初始值 innerLayoutNodeWrapper。然后在第 2 步更新 layoutDelegate.outerWrapper,实际上是更新 outerLayoutNodeWrapper 时,由于其初始值就是 innerLayoutNodeWrapper,因此更新前后的值是一样的,会得到这样一个结构:

Box[
	outerLayoutNodeWrapper = InnerPlaceable
]

然后我们再看使用了 LayoutModifier 的情况。对于 Box(Modifier.layout {…}),当遍历到 layout() 内的 LayoutModifier 时,mod = LayoutModifier,toWrap = InnerPlaceable,在执行 1.2 步骤时会将二者封装到 ModifiedLayoutNode 中:

internal class ModifiedLayoutNode(
    override var wrapped: LayoutNodeWrapper,
    var modifier: LayoutModifier
) : LayoutNodeWrapper(wrapped.layoutNode)

并且这个 ModifiedLayoutNode 会作为 outerWrapper 替换掉原来的 outerLayoutNodeWrapper,形成如下的结构:

Box[
	outerLayoutNodeWrapper = ModifiedLayoutNode(
        wrapped = InnerPlaceable,
        modifier = LayoutModifier
    )
]

倘若 Box 的 Modifier 链上有两个 LayoutModifier,就会形成如下的嵌套结构:

Box[
	outerLayoutNodeWrapper = 
	ModifiedLayoutNode(
        wrapped = ModifiedLayoutNode(
            wrapped = InnerPlaceable, // 负责组件本身测量逻辑的 InnerPlaceable 被包在最内层
            modifier = LayoutModifier
        ),
        modifier = LayoutModifier
    )
]

通过以上讲解你会感受到,modifier 属性的 set 函数最重要的一个任务就是遍历 Modifier 链赋值给 LayoutNode 的 outerLayoutNodeWrapper 属性。outerLayoutNodeWrapper 是外层的 LayoutNodeWrapper,通常类型是 ModifiedLayoutNode;而初始值负责组件本身测量逻辑的 innerLayoutNodeWrapper,类型是 InnerPlaceable。

Before 与 After 阶段

观察 1.1 和 1.3 两步做了类似的操作,都是将 toWrap 和 mod 添加到 toWrap.entities 中。我们知道 toWrap 是一个 LayoutNodeWrapper,它的 entities 属性是一个 EntityList:

	/**
     * All [LayoutNodeEntity] elements that are associated with this [LayoutNodeWrapper].
     */
	val entities = EntityList()

给 LayoutNodeWrapper 的 entities 初始化时,EntityList 的 entities 属性使用了默认值,即 LayoutNodeEntity 类型的数组,数组默认容量为 7:

internal value class EntityList(
    val entities: Array<LayoutNodeEntity<*, *>?> = arrayOfNulls(TypeCount)
) {
    value class EntityType<T : LayoutNodeEntity<T, M>, M : Modifier>(val index: Int)
    
    companion object {
        val DrawEntityType = EntityType<DrawEntity, DrawModifier>(0)
        val PointerInputEntityType = EntityType<PointerInputEntity, PointerInputModifier>(1)
        val SemanticsEntityType = EntityType<SemanticsEntity, SemanticsModifier>(2)
        val ParentDataEntityType =
            EntityType<SimpleEntity<ParentDataModifier>, ParentDataModifier>(3)
        val OnPlacedEntityType =
            EntityType<SimpleEntity<OnPlacedModifier>, OnPlacedModifier>(4)
        val RemeasureEntityType =
            EntityType<SimpleEntity<OnRemeasuredModifier>, OnRemeasuredModifier>(5)
        @OptIn(ExperimentalComposeUiApi::class)
        val LookaheadOnPlacedEntityType =
            EntityType<SimpleEntity<LookaheadOnPlacedModifier>, LookaheadOnPlacedModifier>(
                6
            )
        private const val TypeCount = 7
    }
}

伴生对象中定义 TypeCount 为 7,表示类型的数量为 7,同时还定义了这 7 中类型,从 DrawEntityType 到 LookaheadOnPlacedEntityType,它们都是 EntityType,并且通过给 EntityType 的 index 传入固定值,固定了这 7 种类型的索引。

每一种类型给 EntityType 传递的泛型,都是某一种 Modifier 类型对应的 LayoutNodeEntity。比如 DrawEntityType 就是表示 DrawModifier 对应的实体 DrawEntity 的类型。而 LayoutNodeEntity 是一个链表:

internal open class LayoutNodeEntity<T : LayoutNodeEntity<T, M>, M : Modifier>(
    val layoutNodeWrapper: LayoutNodeWrapper,
    val modifier: M
) {
    /**
     * The next element in the list. [next] is the element that is wrapped
     * by this [LayoutNodeEntity].
     */
    var next: T? = null

    /**
     * Convenience access to [LayoutNode]
     */
    val layoutNode: LayoutNode
        get() = layoutNodeWrapper.layoutNode
}

LayoutNodeEntity 实际上是保存了该实体对象属于哪一个 LayoutNodeWrapper 以及处理哪一种 Modifier。这样再看 1.1 和 1.3 调用 EntityList 的 addBeforeLayoutModifier() 和 addAfterLayoutModifier() 就能看清是怎样的层级结构了:

internal value class EntityList(
    val entities: Array<LayoutNodeEntity<*, *>?> = arrayOfNulls(TypeCount)
) {
    /**
     * Add [LayoutNodeEntity] values for types that [modifier] supports that should be
     * added before the LayoutModifier.
     */
    fun addBeforeLayoutModifier(layoutNodeWrapper: LayoutNodeWrapper, modifier: Modifier) {
        if (modifier is DrawModifier) {
            add(DrawEntity(layoutNodeWrapper, modifier), DrawEntityType.index)
        }
        if (modifier is PointerInputModifier) {
            add(PointerInputEntity(layoutNodeWrapper, modifier), PointerInputEntityType.index)
        }
        if (modifier is SemanticsModifier) {
            add(SemanticsEntity(layoutNodeWrapper, modifier), SemanticsEntityType.index)
        }
        if (modifier is ParentDataModifier) {
            add(SimpleEntity(layoutNodeWrapper, modifier), ParentDataEntityType.index)
        }
    }

    /**
     * Add [LayoutNodeEntity] values that must be added after the LayoutModifier.
     */
    @OptIn(ExperimentalComposeUiApi::class)
    fun addAfterLayoutModifier(layoutNodeWrapper: LayoutNodeWrapper, modifier: Modifier) {
        if (modifier is OnPlacedModifier) {
            add(SimpleEntity(layoutNodeWrapper, modifier), OnPlacedEntityType.index)
        }
        if (modifier is OnRemeasuredModifier) {
            add(SimpleEntity(layoutNodeWrapper, modifier), RemeasureEntityType.index)
        }
        if (modifier is LookaheadOnPlacedModifier) {
            add(SimpleEntity(layoutNodeWrapper, modifier), LookaheadOnPlacedEntityType.index)
        }
    }
    
    // 将 entity 添加到 entities[index] 数组中
    private fun <T : LayoutNodeEntity<T, *>> add(entity: T, index: Int) {
        @Suppress("UNCHECKED_CAST")
        val head = entities[index] as T?
        // 头插法
        entity.next = head
        entities[index] = entity
    }
}

两个函数内部会将指定类型的 Modifier 以及它对应的 LayoutNodeEntity 通过 add() 添加到 EntityList 的 entities 数组中,由于数组元素是 LayoutNodeEntity,也就是一个链表,并且每个位置的链表类型在伴生对象中已经固定好了,所以就形成如下的结构:

请添加图片描述

就是数组 + 链表,有点类似于 JDK 1.8 之前 HashMap 那种结构,并且是采用头插法的链表。当前只看源码不结合代码示例看可能不是很好理解,后面我们会结合具体示例再说说。

最后我们综合看 3.2 小节,你会发现 LayoutModifier 是一个很关键的 Modifier。不仅构造层级结构时要对 LayoutModifier 生成 ModifiedLayoutNode,连向 LayoutNodeWrapper 的 entities 属性内的 entities 数组添加元素都要以 LayoutModifier 的处理为界,在处理 LayoutModifier 之前添加的 Modifier 有 DrawModifier、PointerInputModifier、SemanticsModifier 以及 ParentDataModifier。在 LayoutModifier 之前添加的 Modifier 有 OnPlacedModifier、OnRemeasuredModifier 与 LookaheadOnPlacedModifier。在本篇介绍完 LayoutModifier 之后,后续也会按照这种先后顺序介绍这些 Modifier。

4、LayoutModifier 原理

接下来我们要先看 LayoutNode 是如何进行测量与布局的,再看 LayoutModifier 是如何影响测量和布局的。

3.1 测量过程

Compose 测量过程的起点是在 Android 原生的 AndroidComposeView 中,因为 Compose 构建的 UI 最终会被放在 AndroidComposeView 中然后连接到原生的视图结构下:

internal class AndroidComposeView(context: Context) :
    ViewGroup(context), Owner, ViewRootForTest, PositionCalculator, DefaultLifecycleObserver {
        
	override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        trace("AndroidOwner:onMeasure") {
            if (!isAttachedToWindow) {
                // 遍历 LayoutNode 子树并标记所有 node 需要测量
                invalidateLayoutNodeMeasurement(root)
            }
            // 生成测量约束
            val (minWidth, maxWidth) = convertMeasureSpec(widthMeasureSpec)
            val (minHeight, maxHeight) = convertMeasureSpec(heightMeasureSpec)
            val constraints = Constraints(minWidth, maxWidth, minHeight, 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.measureAndLayout(resendMotionEventOnLayout)
            setMeasuredDimension(root.width, root.height)
            // 对加入到 Compose UI 结构中的原生 View 进行测量
            if (_androidViewsHandler != null) {
                androidViewsHandler.measure(
                    MeasureSpec.makeMeasureSpec(root.width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(root.height, MeasureSpec.EXACTLY)
                )
            }
        }
    }
}

MeasureAndLayoutDelegate 的 measureAndLayout() 负责从 Compose 结构的根组件开始测量和布局:

	// 遍历所有请求进行布局的 LayoutNodes,进行测量和布局操作
	fun measureAndLayout(onLayout: (() -> Unit)? = null): Boolean {
        // 标记根节点是否改变了尺寸,初始化为 false,是本函数的返回值
        var rootNodeResized = false
        // 执行核心的测量布局操作
        performMeasureAndLayout {
            // 检查是否存在需要重新布局的节点
            if (relayoutNodes.isNotEmpty()) {
                // 弹出并遍历每个待布局节点
                relayoutNodes.popEach { layoutNode ->
                    // 重新测量布局并返回尺寸变化状态
                    val sizeChanged = remeasureAndRelayoutIfNeeded(layoutNode)
                    // 更新根节点尺寸变化标志                  
                    if (layoutNode === root && sizeChanged) {
                        rootNodeResized = true
                    }
                }
                // 执行参数提供的布局完成后的回调(如果存在)
                onLayout?.invoke()
            }
        }
        // 通知所有布局完成监听器
        callOnLayoutCompletedListeners()
        return rootNodeResized
    }

看 remeasureAndRelayoutIfNeeded() 的测量布局逻辑:

    /**
     * 在需要时对节点执行实际的重新测量和布局操作。
     * 注意:[layoutNode]在执行前应已从[relayoutNodes]集合中移除
     *
     * @return 返回[LayoutNode]尺寸是否发生变化的布尔值
     */
    private fun remeasureAndRelayoutIfNeeded(layoutNode: LayoutNode): Boolean {
        // 跟踪当前节点尺寸变化状态
        var sizeChanged = false 
        // 检查节点是否需要参与布局流程的条件(5种触发场景)
        if (layoutNode.isPlaced ||
            layoutNode.canAffectParent ||
            layoutNode.isPlacedInLookahead == true ||
            layoutNode.canAffectParentInLookahead ||
            layoutNode.alignmentLinesRequired
        ) {
            // 跟踪预测量阶段尺寸变化
            var lookaheadSizeChanged = false

            // 1.处理测量阶段
            if (layoutNode.lookaheadMeasurePending || layoutNode.measurePending) {
                // 根节点使用预设约束,其他节点约束为 null
                val constraints = if (layoutNode === root) rootConstraints!! else null

                // 处理预测量(lookahead)阶段
                if (layoutNode.lookaheadMeasurePending) {
                    lookaheadSizeChanged = doLookaheadRemeasure(layoutNode, constraints)
                }

                // 处理常规测量阶段
                sizeChanged = doRemeasure(layoutNode, constraints)
            }

            // 2.处理预布局阶段
            if ((lookaheadSizeChanged || layoutNode.lookaheadLayoutPending) &&
                layoutNode.isPlacedInLookahead == true
            ) {
                // 执行预布局替换操作
                layoutNode.lookaheadReplace()
            }

            // 3.处理常规布局阶段
            if (layoutNode.layoutPending && layoutNode.isPlaced) {
                // 根节点采用绝对定位,其他节点执行替换布局
                if (layoutNode === root) {
                    // 根节点从坐标原点开始布局
                    layoutNode.place(0, 0)
                } else {
                    // 普通节点执行布局替换
                    layoutNode.replace()
                }
                // 触发布局完成事件
                onPositionedDispatcher.onNodePositioned(layoutNode)
                // 执行布局一致性检查(调试用)
                consistencyChecker?.assertConsistent()
            }

            // 4.处理延迟测量请求
            // 处理常规测量延迟请求
            if (postponedMeasureRequests.isNotEmpty()) {
                postponedMeasureRequests.forEach {
                    // 仅处理已附加节点
                    if (it.isAttached) {
                        requestRemeasure(it)
                    }
                }
                // 清空延迟请求队列
                postponedMeasureRequests.clear()
            }

            // 5.处理预测量延迟请求
            if (postponedLookaheadMeasureRequests.isNotEmpty()) {
                postponedLookaheadMeasureRequests.forEach {
                    if (it.isAttached) {
                        requestLookaheadRemeasure(it)
                    }
                }
                postponedLookaheadMeasureRequests.clear()
            }
        }
        // 返回最终尺寸变化状态
        return sizeChanged
    }

在第一阶段调用 doRemeasure() 进行测量时,会对参数上的 layoutNode 进行测量:

	private fun doRemeasure(layoutNode: LayoutNode, constraints: Constraints?): Boolean {
        // 调用端代码只有根节点才有 constraints,普通节点没有
        val sizeChanged = if (constraints != null) {
            // 使用根约束重新测量
            layoutNode.remeasure(constraints)
        } else {
            // 普通节点自主测量
            layoutNode.remeasure()
        }
        // 处理父级更新
        val parent = layoutNode.parent
        // 仅当尺寸变化且存在父节点时
        if (sizeChanged && parent != null) {
            if (layoutNode.measuredByParent == InMeasureBlock) {
                // 触发父级重新测量
                requestRemeasure(parent)
            } else if (layoutNode.measuredByParent == InLayoutBlock) {
                // 触发父级重新布局
                requestRelayout(parent)
            }
        }
        // 返回最终尺寸变化状态
        return sizeChanged
    }

对于 LayoutNode 的根节点以及普通节点都是使用同一个 remeasure() 进行测量:

internal class LayoutNode(
    private val isVirtual: Boolean = false
) : Remeasurement, OwnerScope, LayoutInfo, ComposeUiNode,
    Owner.OnLayoutCompletedListener {
    
    // 如果测量的尺寸发生变化就返回 true
    internal fun remeasure(
        constraints: Constraints? = layoutDelegate.lastConstraints
    ): Boolean {
        return if (constraints != null) {
            // 当父节点未使用固有特性测量时,清理子树相关状态
            if (intrinsicsUsageByParent == UsageByParent.NotUsed) {
                clearSubtreeIntrinsicsUsage()
            }
            // 委托测量模块执行实际测量
            measurePassDelegate.remeasure(constraints)
        } else {
            // 无约束条件时直接返回未变化
            false
        }
    }
}

对 LayoutNode 节点的测量委托给 MeasurePassDelegate:

		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() 一看,是此前介绍过的接口 Measurable:

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

为了能看清 measure() 的具体内容,需要追溯 outerWrapper 的实际类型。outerWrapper 是 LayoutNodeLayoutDelegate 的成员属性:

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

这样需要回头找一下创建 LayoutNodeLayoutDelegate 时给 outerWrapper 传了什么值,在 LayoutNode 内的声明如下:

	internal val layoutDelegate = LayoutNodeLayoutDelegate(this, innerLayoutNodeWrapper)
	internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
	internal val outerLayoutNodeWrapper: LayoutNodeWrapper
        get() = layoutDelegate.outerWrapper

前面分析 LayoutNode 的 modifier 属性的 set() 时分析过这三个属性,innerLayoutNodeWrapper 类型是 InnerPlaceable,专门用于处理组件自身的测量。如果 modifier 链上有 LayoutModifier,就会生成一个 ModifiedLayoutNode 将此前的 LayoutNodeWrapper 与 LayoutModifier 包装起来并更新给 outerLayoutNodeWrapper。所以到这一步我们也能明白不断更新 outerLayoutNodeWrapper 的用意了,就是把最外层的 LayoutNodeWrapper 交给它,进入测量阶段时,从这个最外层的 LayoutNodeWrapper 开始向内进行测量。

确认 outerWrapper 的具体类型后,下一步就要看具体的测量步骤了。

3.2 LayoutModifier 原理解析

InnerPlaceable 的测量

先看 InnerPlaceable 的 measure() 是如何测量的:

    override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
        // 1.在运行用户的测量代码块之前,重置子节点的测量标记
        layoutNode.forEachChild {
            it.measuredByParent = LayoutNode.UsageByParent.NotUsed
        }

        // 2.with() 切换到 MeasurePolicy 环境
        measureResult = with(layoutNode.measurePolicy) {
            // 调用 MeasurePolicy 的 measure() 对该 layoutNode 的子组件进行实际测量
            layoutNode.measureScope.measure(layoutNode.childMeasurables, constraints)
        }
        // 3.测量后处理
        onMeasured()
        return this
    }

关键步骤是第 2 步,使用 with() 切换到 InnerPlaceable 内包装的 layoutNode 的 MeasurePolicy 环境下。然后调用 MeasurePolicy 的 measure() 进行测量:

fun interface MeasurePolicy {
    /**
     * 
     * The function that defines the measurement and layout. Each [Measurable] in the [measurables]
     * list corresponds to a layout child of the layout, and children can be measured using the
     * [Measurable.measure] method. This method takes the [Constraints] which the child should
     * respect; different children can be measured with different constraints.
     * Measuring a child returns a [Placeable], which reveals the size chosen by the child as a
     * result of its own measurement. According to the children sizes, the parent defines the
     * position of the children, by [placing][Placeable.PlacementScope.place] the [Placeable]s in
     * the [MeasureResult.placeChildren] of the returned [MeasureResult]. Therefore the parent needs
     * to measure its children with appropriate [Constraints], such that whatever valid sizes
     * children choose, they can be laid out correctly according to the parent's layout algorithm.
     * This is because there is no measurement negotiation between the parent and children:
     * once a child chooses its size, the parent needs to handle it correctly.
     *
     * Note that a child is allowed to choose a size that does not satisfy its constraints. However,
     * when this happens, the placeable's [width][Placeable.width] and [height][Placeable.height]
     * will not represent the real size of the child, but rather the size coerced in the
     * child's constraints. Therefore, it is common for parents to assume in their layout
     * algorithm that its children will always respect the constraints. When this
     * does not happen in reality, the position assigned to the child will be
     * automatically offset to be centered on the space assigned by the parent under
     * the assumption that constraints were respected. Rarely, when a parent really needs to know
     * the true size of the child, they can read this from the placeable's
     * [Placeable.measuredWidth] and [Placeable.measuredHeight].
     *
     * [MeasureResult] objects are usually created using the [MeasureScope.layout]
     * factory, which takes the calculated size of this layout, its alignment lines, and a block
     * defining the positioning of the children layouts.
     */
    fun MeasureScope.measure(
        measurables: List<Measurable>,
        constraints: Constraints
    ): MeasureResult
}

由于 MeasurePolicy 是一个接口,需要回溯去看 layoutNode 内的 measurePolicy 的具体类型。这里我们就举一个例子来说,比如 layoutNode 是 Text 组件:

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    ...
) {
    ...
    BasicText(
        text,
        modifier,
        mergedStyle,
        onTextLayout,
        overflow,
        softWrap,
        maxLines,
    )
}

@Composable
fun BasicText(
    text: String,
    modifier: Modifier = Modifier,
    ...
) {
    ...
    Layout(modifier.then(controller.modifiers), controller.measurePolicy)
}

它会在调用 Layout() 时为其传入 measurePolicy 参数(其他的组件也是类似的情况,在内部调用 Layout() 时传 measurePolicy),比如这里的 measurePolicy 是 controller.measurePolicy:

internal class TextController(val state: TextState) : RememberObserver {
    val measurePolicy = object : MeasurePolicy {
     
        override fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult {
            // 具体的测量逻辑,代码很长,故而省略了...
        }
        ...
    }
}

这样就会按照相应的 MeasurePolicy 的 measure() 进行测量。

ModifiedLayoutNode 的测量

ModifiedLayoutNode 的 measure() :

	override fun measure(constraints: Constraints): Placeable {
        performingMeasure(constraints) {
            // with() 参数上的 modifier 是 LayoutModifier,因此是切换到 LayoutModifier 环境中
            with(modifier) {
                // 调用 LayoutModifier 的 measure() 进行测量
                measureResult = measureScope.measure(wrapped, constraints)
                this@ModifiedLayoutNode
            }
        }
        onMeasured()
        return this
    }

其实也是大致的思路,只不过这里 with() 是切换到 LayoutModifier 的环境下,调用 LayoutModifier 的 measure() 对 ModifiedLayoutNode 内包装的 wrapped,也就是 LayoutNodeWrapper 进行测量。由于 LayoutModifier 也是个接口,因此要结合具体的实现类查看 measure() 内具体的测量逻辑。比如看一个我们经常举例的 Modifier.padding(),也就是 PaddingModifier 的实现:

	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())
            }
        }
    }

以上就是测量的完整流程。实际上,Compose 会把测量与布局统称为布局阶段,而测量完成了布局阶段的大部分工作,它已经将组件如何摆放计算出来了,后续在进行布局时,只需调用 placeChildren() 进行摆放即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值