一、概念
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 摆放子节点 | 将子节点摆放到相对位置。 |
从上往下测量,从下往上摆放:
- 系统要求根节点 Row 测量自身。
- 根节点 Row 要求第一个子元素 Image 测量自身。
- 由于 Image 是叶子节点(没有子节点)能确定自身的尺寸和摆放并上报。
- 根节点 Row 要求第二个子元素 Column 测量自身。由于 Column 是分支节点(有子节点)需要先测量所有子元素来确定自身。
- 父容器 Column 要求第一个子元素 Text 测量自身。
- 由于 Text 是叶子节点能确定自身的尺寸和摆放并上报。
- 父容器 Column 要求第二个子元素 Text 测量自身。
- 由于 Text 是叶子节点能确定自身的尺寸和摆放并上报。
- 父容器 Column 所有子元素都测量摆放完毕,可以确定自身的尺寸和摆放并上报。
- 根节点 Row 所有子元素都测量摆放完毕,可以确定自身的尺寸和摆放。
3.2 代码实现过程
所有的 Composable 最终都会调用一个 Layout() 方法,这里面创建 LayoutNode 存储在节点树。
@Composable measurePolicy 和 modifier 会存储在当前 LayoutNode 上,等待 Measure 阶段的开始并参与其中。 |
3.2.1 MeasurePolicy 测量策略
实现接口 MeasurePolicy 的抽象方法 MeasureScope.measure() 以实现三步走逻辑,重写其它四个方法 minIntrinsicWidth()、minIntrinsicHeight()、maxIntrinsicWidth()、maxIntrinsicHeight() 以实现固有特性测量。
fun MeasureScope.measure( 通过返回值类型和作用域约束,这种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 {
} |
自上而下测量:假设屏幕宽高是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。