Compose 自定义 - 数据转UI的三阶段(组合、布局、绘制)

本文详细解释了Compose框架的三个关键阶段:组合(将数据转化为UI树),布局(测量和摆放节点),以及绘制(节点在屏幕上的渲染)。通过例子说明了Modifier的链式调用、Contraints的约束传递,以及UI树的自顶向下遍历过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 一、概念

Compose 通过三个阶段把数据转化为UI:组合(要显示什么)、布局(要显示在哪里)、绘制(如何渲染)。

组合阶段

Compisition

界面首次渲染时会将可组合函数转化为一个个布局节点 Layout Node, 使用多叉树结构构建一颗节点树,这棵树被称为 Composition。

布局阶段

Layout

父节点会测量自己的子节点,然后在一个二维空间里进行摆放。通过从上往下遍历测量(如果存在子节点则测量子节点,测量完子节点后决定自身的尺寸)、从下往上摆放(根据子节点的尺寸摆放子节点)来决定该节点的宽高和坐标。

绘制阶段

Drawing

基于 Layout 阶段拿到的尺寸和位置信息,绘制在屏上。(通过 Android Cavas API ,底层调用 skia 实现)

二、组合阶段 Compisition

每个可组合函数都会映射成UI树的 Layout Node 。可组合函数里还可以包含逻辑和控制流(if else, when...),在不同的状态下产生不同的UI树。

2.1.1 Modifier 链式调用

使用 Modifier 可以更改外观,当链式调用 Modifier 的时候,先调用的会包裹后调用的,最里层是 Layout Node。详见

三、布局阶段 Layout

3.1 布局三阶段

Measure Children

测量子节点 

遍历子节点,测量它们的尺寸。

Decide own size

确定自身尺寸

根据收集的子节点尺寸,决定当前节点自己的尺寸。

Place children

摆放子节点

将子节点摆放到相对位置。

从上往下测量,从下往上摆放: 

  1. 系统要求根节点 Row 测量自身。
  2. 根节点 Row 要求第一个子元素 Image 测量自身。
  3. 由于 Image 是叶子节点(没有子节点)能确定自身的尺寸和摆放并上报。
  4. 根节点 Row 要求第二个子元素 Column 测量自身。由于 Column 是分支节点(有子节点)需要先测量所有子元素来确定自身。
  5. 父容器 Column 要求第一个子元素 Text 测量自身。
  6. 由于 Text 是叶子节点能确定自身的尺寸和摆放并上报。
  7. 父容器 Column 要求第二个子元素 Text 测量自身。
  8. 由于 Text 是叶子节点能确定自身的尺寸和摆放并上报。
  9. 父容器 Column 所有子元素都测量摆放完毕,可以确定自身的尺寸和摆放并上报。
  10. 根节点 Row 所有子元素都测量摆放完毕,可以确定自身的尺寸和摆放。

3.2 代码实现过程

所有的 Composable 最终都会调用一个 Layout() 方法,这里面创建 LayoutNode 存储在节点树。 

@Composable
inline fun Layout(
    content: @Composable @UiComposable () -> Unit,        //定义子内容(组合后形成子节点)
    modifier: Modifier = Modifier,        //外部用来修饰该自定义组件的属性或约束(修饰链参与布局或绘制阶段)
    measurePolicy: MeasurePolicy        //具体测量和摆放的策略(在此实现三步走的逻辑)

measurePolicy 和 modifier 会存储在当前 LayoutNode 上,等待 Measure 阶段的开始并参与其中。

3.2.1 MeasurePolicy 测量策略

详见:Compose 自定义 - 布局 Layout

实现接口 MeasurePolicy 的抽象方法 MeasureScope.measure() 以实现三步走逻辑,重写其它四个方法 minIntrinsicWidth()、minIntrinsicHeight()、maxIntrinsicWidth()、maxIntrinsicHeight() 以实现固有特性测量。

fun MeasureScope.measure(
    measurables: List<Measurable>,        //集合中的元素对应自己的每一个子节点(等待测量的对象),元素调用 measure() 后返回的 Placeable 对象存储了子节点测量后的宽高,可用来累积算出自身尺寸。
    constraints: Constraints        //父容器用来对该节点的测量约束,定义了该节点可以显示的尺寸上下限,即能拿到自己可以显示的宽高最大和最小值,需要修改对子元素的约束可以通过 contraints.copy(minWidth = 20dp) 修改后在子元素测量时传入(通过上方提到的 Measurable.measure() 中传参)。
): MeasureResult

通过返回值类型和作用域约束,这种API设计很好的保证了写出安全又一气呵成的代码:

①该抽象方法 MeasureScope.measure() 返回值类型是 MeasureResult,layout() 的返回值类型也是这保证了最后一定是调用它来完成三步走。

②Measuable.measure() 返回 Placeable 类型,然后才能调用 Placeable 的 palce() 方法,这保证了 measure() 和 layout() 的先后顺序。

③扩展方法 Placeable.palce() 被定义在 PlacementScope 中,因此只能在 layout() 提供的 PlacementScope 中调用,这保证了 place() 只能在 layout() 调用而不能在 measure() 中调用。

@Composable
fun Demo(
    content: @Composable () -> Unit,    //被包裹的内容
    modifier: Modifier = Modifier    //用于外部修饰自己
) {
    Layout(
        modifier = modifier,
        content = content
    ) {measurables, constraints ->
        val needHeight = 0
        val needWidth = 0
        //【测量阶段】
        //遍历并测量子元素,返回结果集合
        val placeables = measurables.map { measurable ->
            //返回子元素测量结果
            //不需要修改约束就直接将constraints传入
            //需要修改约束就使用 constraints.copy(minWidth = 20dp)
            measurable.measure(constraints).also { placeable ->
                //获取测量后的子元素宽高
                val childMeasuredWidth = placeable.width
                val childMeasuredHeight = placeable.height
                //【计算阶段】
                //通过子元素累加出总的宽高,并结合 constraints 得出最终宽高
                needHeight = ...
                needWidth = ...
                //用集合保存每一行或每一列的宽高信息
            }
        }
        //【摆放阶段】
        //该作用域中调用 layout() 传入自身尺寸后开始摆放
        layout(needWidth, needHeight) {
            var xOffset = 0
            var yOffset = 0
            //遍历子元素,设置它们在xy上的偏移
            placeables.forEachIndexed { index, placeable ->
                //通过子元素的宽高信息和偏移量,摆放子元素,结合之前保存的每行或每列的信息注意换行
                placeable.placeRelative(placeable.width + xOffset, placeable.height + yOffset)
                //记录x和y轴放置的偏移量供后续子元素使用
                xOffset += placeable.width
                yOffset += placeable.height
            }
        }
    }
}

3.2.1 Contrains 传递约束

详见:Compose 自定义 - 约束 Constrains

        是节点宽高的上限下限(最大值和最小值)。在 Layout 阶段,当父节点测量子节点的时候会把 Contraints 往下传递,好让子元素知道自己被允许的最大最小尺寸(约束子元素的测量),当所有子元素被测量完成后开始决定自身尺寸时,同样的需要考虑自身的父容器给出的约束。

        Compose不怕嵌套正是得益于它,View体系由于测量阶段的约束不明确,子元素需要再次请求父容器给出清楚的 MeasureSpec。

        Modifier 的装饰能力也是通过修改 Constraints 完成的。例如 fillMaxWidth 要求被修饰的节点填充整个父容器,所以 Modifier 会在布局阶段将 minHeight/minWidth 对齐 max 组值。

class Constraints {
    val minWidth: Int
    val maxWidth: Int
    val minHeight: Int
    val maxHeight: Int

}

自上而下测量:假设屏幕宽高是300*200,对于 fillMaxSize 这个 Modifier Node,填满剩余空间会将 Constraints 的 min 值设为最大值,接着 warpContentSize 让子节点自己决定,会将 min 值设为0,最后 size(50) 给出一个具体的约束,min 和 max 都设为 50。

四、Modifier Node

        Modifier 在组合阶段也会成为 Node 存储在节点树上,Modifier 的调用链生成一条单向继承的子节点树,而被修饰的 Layout Node 会成为这条树枝的叶子节点。

        如图 Image 最终成为 clip→size 的子节点,挂在节点树上的 Modifier Node 可以参与到深度遍历的绘制流程中,在 Image 之前对 Constraints 做出调整,完成对末端 Image 的装饰。

        组合阶段, Modifier#then 创建 Element 加入 Modifier chain 中。Element 是无状态的,重组中会重新生成,Element 会在组合中创建有状态的 ModifierNode。ModifierNode 有状态,重组中仅当状态发生变化时被更新,否则不会重新生成。Modifier Node 是 Compose 1.5 引入的新优化,目的就是通过存储 Modifier 状态参与比较,提升重组性能。

        ModifierNode 按照参与的阶段不同,分为 LayoutModifierNode 和 DrawModifierNode。

4.1 LayoutModifierNode

Modifier.layout() 也能自定义布局,与 Layout() 方式唯一的区别是接受单个 measurable 参数而不是 List,因为 Modifier Node 是单向继承,所以只会有一个后续子节点。如果把 Layout() 的 measure 看做是自定义 ViewGroup 需要针对多个子 View 布局,那么 Modifier.layout() 的 measure 更像是自定义 View,只对自身负责。

fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
)

4.1.1 基本使用

Box(
    Modifier.layout { measurable, constraints ->
        //垂直方向增加边距
        val placeable = measurable.measure(constraints)
        val verticalPadding = 50
        layout(width = placeable.width, height = placeable.height + verticalPadding * 2) {
            placeable.placeRelative(0, verticalPadding)
        }
    }
)

4.1.2 封装使用

fun Modifier.XXX(): Modifier = then(
    layout { measurable, constraints ->
        //测量自身并返回结果
        //通过contraints可以拿到自身宽或高可以设置的最大和最小值
        val placeable = measurable.measure(constraints)
        //获取测量后的控件宽高
        val measuredWidth = placeable.width
        val measuredHeight = placeable.height
        //计算实际宽高
        val needWidth = ...
        val needHeight = ...
        //指定控件宽高
        layout(needWidth, needHeight) {
            //指定摆放的左上角偏移(在xy上移动的距离)
            placeable.placeRelative(0, 0)
        }
    }
)

4.2 DrawModifierNode

五、绘制阶段 Drawing

同样地,UI树会自顶向下地遍历,每个节点依次在屏幕上绘制自身。首先Row会绘制它自己的内容如背景。然后 Image 绘制自身,再之后到分支节点Column,Column的第一个Text,Column的第二个Text。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值