Jetpack Compose 绘制流程与自定义布局
目录
- [Compose 渲染概述](#Compose 渲染概述 "#%E4%B8%80compose-%E6%B8%B2%E6%9F%93%E6%A6%82%E8%BF%B0")
- 三大核心阶段详解
- 状态读取与阶段关联
- 测量约束与父子组件关系
- 自定义布局实现
- 性能优化与最佳实践
- 常见问题与解决方案
一、Compose 渲染概述
1.1 与 Android View 系统的对比
与大多数其他界面工具包一样,Compose 会通过几个不同的"阶段"来渲染帧。如果我们观察一下 Android View 系统,就会发现它有 3 个主要阶段:测量、布局和绘制。
Compose 和它非常相似,但开头多了一个叫做**"组合"**的重要阶段。
1.2 Compose 的三大阶段
┌─────────────────────────────────────────────────────────────┐
│ Compose 渲染流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 组合阶段 │ → │ 布局阶段 │ → │ 绘制阶段 │ │
│ │ Composition │ │ Layout │ │ Draw │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 创建LayoutNode│ │ 测量子元素 │ │ 绘制到画布 │ │
│ │ 视图树 │ │ 确定位置 │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
| 阶段 | 英文名称 | 核心任务 | 输出结果 |
|---|---|---|---|
| 组合 | Composition | 决定要显示什么样的界面 | LayoutNode 视图树 |
| 布局 | Layout | 决定要放置界面的位置 | 每个节点的尺寸和坐标 |
| 绘制 | Draw | 决定渲染的方式 | 屏幕上的像素 |
1.3 单向数据流
这些阶段的顺序通常是相同的,从而让数据能够沿一个方向(从组合到布局,再到绘制)生成帧,也称为单向数据流。
markdown
状态变化 → 组合阶段 → 布局阶段 → 绘制阶段 → 屏幕显示
↑ ↑ ↑
└───────────┴──────────┘
跳过优化(如可能)
Compose 会避免在所有这些阶段中重复执行根据相同输入计算出相同结果的工作。如果可以重复使用前面计算出的结果,Compose 会跳过对应的可组合函数;如果没有必要,Compose 界面不会对整个树进行重新布局或重新绘制。
二、三大核心阶段详解
2.1 组合阶段(Composition)
2.1.1 阶段目标
组合阶段的主要目标是生成并维护 LayoutNode 视图树。
2.1.2 执行流程
- 首次组合 :在 Activity 中执行
setContent时,会开始首次组合 - 执行 Composable 函数:这会执行所有的 Composable 函数,生成与之对应的 LayoutNode 视图树
- 重组(Recomposition):当 Composable 依赖的状态值发生更改时,Recomposer 会安排重新运行所有要读取相应状态值的可组合函数
2.1.3 智能重组机制
css
父组件重组流程:
┌─────────────────────────────────────────────────────────┐
│ ParentComposable 发生重组 │
│ ↓ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Child A │ │ Child B │ │
│ │ 参数变化? │ │ 参数变化? │ │
│ │ 是 → 重组 │ │ 否 → 跳过 │ │
│ │ 更新 LayoutNode │ │ 保持原节点 │ │
│ └─────────────────┘ └─────────────────┘ │
│ ↓ │
│ 未被调用的子组件 → 从视图树中删除 │
└─────────────────────────────────────────────────────────┘
当前组件发生重组时,子 Composable 被依次重新调用,被调用的子 Composable 会将当前传入的参数和之前重组中的参数进行比较:
- 若参数变化:则 Composable 发生重组,更新 LayoutNode 视图树上的节点,UI 发生更新
- 若参数无变化 :则跳过本次执行,即所谓的智能重组,LayoutNode 视图树中对应的节点不变,UI 无变化
- 若子 Composable 在重组中没有被调用:其对应节点及其子节点会从 LayoutNode 视图树中被删除,UI 会从屏幕移除
2.1.4 代码示例
kotlin
@Composable
fun CompositionExample() {
// 在组合阶段创建状态
var padding by remember { mutableStateOf(8.dp) }
Column {
// padding 状态在组合阶段被读取
// 当 padding 变化时,会触发重组
Text(
text = "Hello",
modifier = Modifier.padding(padding)
)
Button(onClick = { padding += 4.dp }) {
Text("增加内边距")
}
}
}
2.2 布局阶段(Layout)
2.2.1 阶段目标
布局阶段的主要目的是为了对视图树中的每个 LayoutNode 节点进行测量和摆放。
2.2.2 两个子步骤
布局阶段包含两个步骤:
| 步骤 | 说明 | 相关 API |
|---|---|---|
| 测量(Measure) | 计算每个节点的尺寸 | Layout 的 measure lambda、MeasureScope.measure |
| 放置(Place)** | 确定每个节点的位置 | layout 函数的放置位置块、Modifier.offset |
2.2.3 测量与放置的关系
scss
布局阶段流程:
┌─────────────────────────────────────────────────────────────┐
│ 布局阶段 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 测量步骤 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 测量子1 │ → │ 测量子2 │ → │ 测量子3 │ │ │
│ │ │ 100x50 │ │ 80x50 │ │ 120x50 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ ↓ │ │
│ │ 计算父节点尺寸 │ │
│ │ 300x50 (水平排列) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 放置步骤 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │子1位置 │ → │子2位置 │ → │子3位置 │ │ │
│ │ │(0, 0) │ │(100, 0) │ │(180, 0) │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
2.2.4 独立重启作用域
测量步骤和放置步骤分别具有单独的重启作用域,这意味着:
- 放置步骤中的状态读取不会在此之前重新调用测量步骤
- 这两个步骤通常是交织在一起的,因此在放置步骤中读取的状态可能会影响属于测量步骤的其他重启作用域
kotlin
@Composable
fun LayoutPhaseExample() {
var offsetX by remember { mutableStateOf(0) }
// offsetX 在布局阶段(放置步骤)被读取
// 当 offsetX 变化时,只会触发重新放置,不会重新测量
Box(
modifier = Modifier.offset { IntOffset(offsetX, 0) }
) {
Text("可偏移的文本")
}
}
2.3 绘制阶段(Draw)
2.3.1 阶段目标
绘制阶段的主要任务是将所有 LayoutNode 界面元素绘制到画布(通常是设备屏幕)之上。
2.3.2 绘制原理
css
绘制阶段流程:
┌─────────────────────────────────────────────────────────────┐
│ 绘制阶段 │
├─────────────────────────────────────────────────────────────┤
│ │
│ LayoutNode 视图树 │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 遍历每个节点,执行绘制操作 │ │
│ │ │ │
│ │ 节点1: drawBackground(Color.Red) │ │
│ │ ↓ │ │
│ │ 节点2: drawContent() - 绘制文本 │ │
│ │ ↓ │ │
│ │ 节点3: drawBorder(2.dp, Color.Blue) │ │
│ │ ↓ │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 输出到屏幕画布 │
│ │
└─────────────────────────────────────────────────────────────┘
2.3.3 代码示例
kotlin
@Composable
fun DrawPhaseExample() {
var color by remember { mutableStateOf(Color.Red) }
// color 状态在绘制阶段被读取
// 当 color 变化时,只会触发重新绘制,不会触发重组或重新布局
Canvas(modifier = Modifier.size(100.dp)) {
drawRect(color = color)
}
Button(onClick = {
color = if (color == Color.Red) Color.Blue else Color.Red
}) {
Text("切换颜色")
}
}
三、状态读取与阶段关联
3.1 状态读取的基本概念
所谓状态读取就是通常使用 mutableStateOf() 创建的,然后通过以下两种方式之一进行访问:
方式一:直接访问 value 属性
kotlin
// State read without property delegate.
val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) }
Text(
text = "Hello",
modifier = Modifier.padding(paddingState.value) // 读取 state.value
)
方式二:使用 Kotlin 属性委托
kotlin
// State read with property delegate.
var padding: Dp by remember { mutableStateOf(8.dp) }
Text(
text = "Hello",
modifier = Modifier.padding(padding) // 直接使用 state
)
3.2 状态读取的跟踪机制
当您在上述任一阶段中读取快照状态值时,Compose 会自动跟踪在系统读取该值时正在执行的操作。通过这项跟踪,Compose 能够在状态值发生更改时重新执行读取程序;Compose 以这项跟踪为基础实现了对状态的观察。
ini
状态观察机制:
┌─────────────────────────────────────────────────────────────┐
│ 状态读取跟踪 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 状态创建 │
│ val state = mutableStateOf(0) │
│ ↓ │
│ 在阶段中读取状态 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 组合阶段 │ │ 布局阶段 │ │ 绘制阶段 │ │
│ │ state.value │ │ state.value │ │ state.value │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ↓ ↓ ↓ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 记录依赖 │ │ 记录依赖 │ │ 记录依赖 │ │
│ │ 组合作用域 │ │ 测量作用域 │ │ 绘制作用域 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ↓ ↓ ↓ │
│ 状态变化时触发对应阶段 │
│ state.value = 1 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 触发重组 │ │ 触发重新布局 │ │ 触发重新绘制 │ │
│ │ Recompose │ │ Re-layout │ │ Re-draw │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
3.3 不同阶段状态读取的影响
3.3.1 组合阶段读取状态
kotlin
@Composable
fun StateInComposition() {
var padding by remember { mutableStateOf(8.dp) }
// padding 状态在组合阶段被读取
// 当 Modifier.padding() 被构造时,读取了 padding 值
// 因此 padding 的变化会触发重组
Text(
text = "Hello",
modifier = Modifier.padding(padding)
)
Button(onClick = { padding += 4.dp }) {
Text("增加内边距(触发重组)")
}
}
影响范围:组合阶段 → 可能触发布局 → 可能触发绘制
3.3.2 布局阶段读取状态
kotlin
@Composable
fun StateInLayout() {
var offsetX by remember { mutableStateOf(0) }
// offsetX 在布局阶段被读取
// Modifier.offset 的 lambda 在放置步骤执行
// 因此 offsetX 的变化只会触发重新放置
Box(
modifier = Modifier.offset { IntOffset(offsetX, 0) }
) {
Text("拖动我")
}
Slider(
value = offsetX.toFloat(),
onValueChange = { offsetX = it.toInt() },
valueRange = 0f..300f
)
}
影响范围:仅布局阶段(放置步骤)→ 可能触发绘制
3.3.3 绘制阶段读取状态
kotlin
@Composable
fun StateInDraw() {
var color by remember { mutableStateOf(Color.Red) }
// color 在绘制阶段被读取
// drawRect 在 Canvas 的 drawBehind 中执行
// 因此 color 的变化只会触发重新绘制
Canvas(modifier = Modifier.size(200.dp)) {
drawRect(
color = color,
size = size
)
}
Button(onClick = {
color = Color(randomColor())
}) {
Text("随机颜色(仅重绘)")
}
}
影响范围:仅绘制阶段
3.4 状态读取位置对比表
| 状态读取位置 | 使用的 API 示例 | 状态变化触发 | 性能影响 |
|---|---|---|---|
| 组合阶段 | Modifier.padding(state) |
重组 + 可能布局 + 可能绘制 | 最大 |
| 布局阶段-测量 | Layout { measurables, constraints -> ... } |
重新测量 + 重新放置 + 绘制 | 中等 |
| 布局阶段-放置 | Modifier.offset { IntOffset(state, 0) } |
重新放置 + 绘制 | 较小 |
| 绘制阶段 | Canvas { drawRect(color = state) } |
仅重新绘制 | 最小 |
3.5 优化建议
kotlin
// ❌ 不推荐:在组合阶段读取频繁变化的状态
@Composable
fun BadExample() {
var offset by remember { mutableStateOf(0f) }
// 每次 offset 变化都会触发重组
Box(
modifier = Modifier
.offset(x = offset.dp) // 在组合阶段读取
.size(100.dp)
.background(Color.Red)
)
}
// ✅ 推荐:在布局阶段读取频繁变化的状态
@Composable
fun GoodExample() {
var offset by remember { mutableStateOf(0) }
// offset 变化只触发重新放置,不触发重组
Box(
modifier = Modifier
.offset { IntOffset(offset, 0) } // 在布局阶段读取
.size(100.dp)
.background(Color.Red)
)
}
// ✅ 更好:在绘制阶段读取仅影响外观的状态
@Composable
fun BestExample() {
var color by remember { mutableStateOf(Color.Red) }
// color 变化只触发重新绘制
Box(
modifier = Modifier
.size(100.dp)
.drawBehind {
drawRect(color = color) // 在绘制阶段读取
}
)
}
四、测量约束与父子组件关系
4.1 Constraints 约束系统
在 Compose 的布局阶段,父组件通过 Constraints 对象向子组件传递测量约束。理解约束系统是实现正确自定义布局的基础。
4.1.1 Constraints 数据结构
kotlin
class Constraints private constructor(
val minWidth: Int, // 最小宽度(像素)
val maxWidth: Int, // 最大宽度(像素)
val minHeight: Int, // 最小高度(像素)
val maxHeight: Int // 最大高度(像素)
)
| 属性 | 说明 | 典型值 |
|---|---|---|
minWidth |
子组件必须满足的最小宽度 | 0(无限制)或具体值 |
maxWidth |
子组件不能超过的最大宽度 | Constraints.Infinity(无穷大)或具体值 |
minHeight |
子组件必须满足的最小高度 | 0(无限制)或具体值 |
maxHeight |
子组件不能超过的最大高度 | Constraints.Infinity(无穷大)或具体值 |
4.1.2 约束类型分类
scss
约束类型示意图:
┌─────────────────────────────────────────────────────────────┐
│ 约束类型分类 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 紧约束 (Tight Constraints) │
│ minWidth == maxWidth 或 minHeight == maxHeight │
│ 子组件必须精确匹配指定尺寸 │
│ │
│ 例如:Constraints.fixed(100, 200) │
│ minWidth=100, maxWidth=100, minHeight=200, maxHeight=200│
│ │
│ 2. 松约束 (Loose Constraints) │
│ minWidth < maxWidth 或 minHeight < maxHeight │
│ 子组件可以在范围内选择自己的尺寸 │
│ │
│ 例如:Constraints(0, 300, 0, 400) │
│ 宽度范围 0-300,高度范围 0-400 │
│ │
│ 3. 无穷约束 (Infinite Constraints) │
│ maxWidth 或 maxHeight 为 Constraints.Infinity │
│ 子组件在对应方向上无尺寸限制 │
│ │
│ 例如:Constraints(0, Infinity, 0, 400) │
│ 宽度无上限,高度最大 400 │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 父子组件约束传递规则
4.2.1 约束传递流程
scss
父子约束传递流程:
┌─────────────────────────────────────────────────────────────┐
│ 约束传递机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 父组件 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 接收来自祖父的约束 │
│ │ 2. 根据自身策略计算子组件约束 │
│ │ 3. 调用 measure(childConstraints) │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 子组件约束 = f(父组件约束, 父组件策略) │
│ ↓ │
│ 子组件 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 接收父组件传递的约束 │
│ │ 2. 根据自身内容计算实际尺寸 │
│ │ - 必须在约束范围内 │
│ │ - 返回 Placeable (包含测量后的尺寸) │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 父组件 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 4. 根据子组件实际尺寸计算自身尺寸 │
│ │ 5. 调用 layout(width, height) 确定最终尺寸 │
│ │ 6. 调用 placeable.place() 放置子组件 │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
4.2.2 常见布局的约束策略
| 布局类型 | 对子组件的宽度约束 | 对子组件的高度约束 | 说明 |
|---|---|---|---|
| Box | 0 ~ 父最大宽度 | 0 ~ 父最大高度 | 子组件可自由选择尺寸 |
| Column | 0 ~ 父最大宽度 | 0 ~ 父最大高度 | 垂直排列,宽度通常填满 |
| Row | 0 ~ 父最大宽度 | 0 ~ 父最大高度 | 水平排列,高度通常填满 |
| Box with Alignment | 紧约束(父尺寸) | 紧约束(父尺寸) | 子组件必须匹配父尺寸 |
4.2.3 约束传递代码示例
kotlin
@Composable
fun ConstraintPropagationDemo() {
Column {
// 场景1:父组件传递松约束
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.LightGray)
) {
// 子组件在 0~maxWidth, 0~maxHeight 范围内自由选择
Text(
"我在松约束下测量",
modifier = Modifier.background(Color.Red)
)
}
Spacer(modifier = Modifier.height(16.dp))
// 场景2:父组件传递紧约束
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
// 子组件必须匹配父组件尺寸 (fillMaxSize 效果)
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Green)
)
}
}
}
4.3 测量过程中的约束协商
4.3.1 子组件必须遵守的规则
kotlin
// 子组件测量时必须遵守约束规则
fun Measurable.measure(constraints: Constraints): Placeable {
// 规则1:测量后的宽度必须在 [minWidth, maxWidth] 范围内
val width = calculatedWidth.coerceIn(constraints.minWidth, constraints.maxWidth)
// 规则2:测量后的高度必须在 [minHeight, maxHeight] 范围内
val height = calculatedHeight.coerceIn(constraints.minHeight, constraints.maxHeight)
// 返回测量结果
return Placeable(width, height)
}
4.3.2 常见约束场景分析
场景1:子组件尝试超出约束
kotlin
@Composable
fun ConstraintViolationDemo() {
Layout(
content = {
// 子组件希望尺寸为 200x200
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
}
) { measurables, constraints ->
// 父组件只允许最大 100x100
val childConstraints = Constraints(
minWidth = 0,
maxWidth = 100,
minHeight = 0,
maxHeight = 100
)
val placeable = measurables.first().measure(childConstraints)
// 子组件实际尺寸会被强制限制在 100x100 以内
println("子组件实际尺寸: ${placeable.width} x ${placeable.height}")
// 输出: 子组件实际尺寸: 100 x 100
layout(100, 100) {
placeable.placeRelative(0, 0)
}
}
}
场景2:无穷约束的处理
kotlin
@Composable
fun InfiniteConstraintDemo() {
// Column 在垂直方向上提供无穷约束
Column(
modifier = Modifier.fillMaxWidth()
) {
// 子组件在垂直方向上可以无限延伸
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Red)
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.background(Color.Green)
)
// Column 会累加所有子组件高度
}
}
场景3:嵌套滚动中的约束
kotlin
@Composable
fun NestedScrollConstraintDemo() {
// LazyColumn 在垂直方向提供无穷约束
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(100) { index ->
// 每个子项在垂直方向上可以自由选择高度
ListItem(index)
}
}
}
@Composable
fun ListItem(index: Int) {
// 子组件根据内容确定高度
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text("Item $index", fontSize = 18.sp)
Text("描述文本", fontSize = 14.sp, color = Color.Gray)
}
}
4.4 Constraints 的创建与修改
4.4.1 常用创建方法
kotlin
// 1. 固定尺寸(紧约束)
val fixedConstraints = Constraints.fixed(width = 100, height = 200)
// minWidth=100, maxWidth=100, minHeight=200, maxHeight=200
// 2. 指定范围
val rangeConstraints = Constraints(
minWidth = 50,
maxWidth = 300,
minHeight = 50,
maxHeight = Constraints.Infinity // 高度无上限
)
// 3. 复制并修改现有约束
val modifiedConstraints = constraints.copy(
minWidth = 0,
maxWidth = constraints.maxWidth / 2 // 宽度减半
)
// 4. 使用 constrain 方法限制尺寸
val size = Size(500f, 800f)
val constrainedSize = constraints.constrain(size)
// 确保 size 在约束范围内
4.4.2 约束的传递与转换
kotlin
@Composable
fun ConstraintTransformationDemo() {
Layout(
content = { /* 子组件 */ }
) { measurables, constraints ->
// 策略1:将松约束转为紧约束
val tightConstraints = Constraints.fixed(
constraints.maxWidth,
constraints.maxHeight
)
// 策略2:限制最大尺寸
val limitedConstraints = Constraints(
minWidth = constraints.minWidth,
maxWidth = minOf(constraints.maxWidth, 400),
minHeight = constraints.minHeight,
maxHeight = minOf(constraints.maxHeight, 600)
)
// 策略3:为每个子组件分配固定宽度
val childWidth = constraints.maxWidth / measurables.size
val childConstraints = Constraints.fixedWidth(childWidth)
val placeables = measurables.map { measurable ->
measurable.measure(childConstraints)
}
// ... 放置逻辑
layout(constraints.maxWidth, constraints.maxHeight) {
// ...
}
}
}
4.5 测量顺序与多次测量
4.5.1 单次测量规则
Compose 对子组件的测量有严格限制:每个子组件在每次布局传递中只能被测量一次。
kotlin
// ❌ 错误:多次测量同一个子组件
Layout(content = { Text("Hello") }) { measurables, constraints ->
val measurable = measurables.first()
// 第一次测量
val placeable1 = measurable.measure(constraints)
// 第二次测量 - 会抛出异常!
val placeable2 = measurable.measure(
Constraints.fixed(placeable1.width, placeable1.height)
)
// ...
}
// ✅ 正确:只测量一次
Layout(content = { Text("Hello") }) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
// 使用 placeable 进行布局计算
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
4.5.2 需要多次测量的解决方案
如果确实需要多次测量(如实现固有特性测量),可以使用 SubcomposeLayout:
kotlin
@Composable
fun SubcomposeLayoutDemo() {
SubcomposeLayout { constraints ->
// 可以多次测量不同的子组件组合
val mainMeasurables = subcompose("main") {
MainContent()
}
val mainPlaceables = mainMeasurables.map {
it.measure(constraints)
}
// 根据主内容尺寸计算其他子组件
val maxHeight = mainPlaceables.maxOfOrNull { it.height } ?: 0
val overlayMeasurables = subcompose("overlay") {
OverlayContent()
}
val overlayConstraints = Constraints.fixedWidth(constraints.maxWidth)
val overlayPlaceables = overlayMeasurables.map {
it.measure(overlayConstraints)
}
// 布局计算...
layout(constraints.maxWidth, maxHeight) {
mainPlaceables.forEach { it.placeRelative(0, 0) }
overlayPlaceables.forEach { it.placeRelative(0, 0) }
}
}
}
4.6 固有特性测量(Intrinsic Measurements)
4.6.1 固有尺寸的概念
固有特性测量允许父组件在正式测量之前,查询子组件的"理想尺寸"。
kotlin
// 固有特性测量方法
interface IntrinsicMeasurable {
fun minIntrinsicWidth(height: Int): Int // 给定高度时的最小宽度
fun minIntrinsicHeight(width: Int): Int // 给定宽度时的最小高度
fun maxIntrinsicWidth(height: Int): Int // 给定高度时的最大宽度
fun maxIntrinsicHeight(width: Int): Int // 给定宽度时的最大高度
}
4.6.2 使用场景
kotlin
// 场景:实现 Divider 根据文本高度自适应
@Composable
fun IntrinsicDemo() {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text("左侧文本", fontSize = 14.sp)
// Divider 高度与文本高度一致
Divider(
modifier = Modifier
.width(1.dp)
.height(IntrinsicSize.Min) // 使用固有最小高度
)
Text("右侧文本", fontSize = 20.sp) // 更大的字体
}
}
4.6.3 自定义固有特性测量
kotlin
@Composable
fun CustomIntrinsicLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier,
measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
// 正常测量逻辑
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.sumOf { it.width }
val height = placeables.maxOfOrNull { it.height } ?: 0
return layout(width, height) {
var x = 0
placeables.forEach {
it.placeRelative(x, 0)
x += it.width
}
}
}
// 固有宽度测量
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
): Int {
return measurables.sumOf { it.minIntrinsicWidth(height) }
}
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
): Int {
return measurables.sumOf { it.maxIntrinsicWidth(height) }
}
// 固有高度测量
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int {
return measurables.maxOfOrNull { it.minIntrinsicHeight(width) } ?: 0
}
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int {
return measurables.maxOfOrNull { it.maxIntrinsicHeight(width) } ?: 0
}
}
)
}
五、自定义布局实现
5.1 使用 Layout 可组合函数
Compose 提供了 Layout 可组合函数来实现完全自定义的布局逻辑。
5.1.1 基本结构
kotlin
@Composable
fun CustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// 测量子元素
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 计算布局尺寸
val width = placeables.sumOf { it.width }
val height = placeables.maxOf { it.height }
// 放置子元素
layout(width, height) {
var xPosition = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = xPosition, y = 0)
xPosition += placeable.width
}
}
}
}
5.1.2 Layout 函数参数说明
kotlin
@Composable
inline fun Layout(
content: @Composable () -> Unit, // 子元素内容
modifier: Modifier = Modifier, // 修饰符
measurePolicy: MeasurePolicy // 测量和放置策略
)
// MeasurePolicy 接口
interface MeasurePolicy {
fun MeasureScope.measure(
measurables: List<Measurable>, // 可测量的子元素列表
constraints: Constraints // 父元素约束条件
): MeasureResult
}
5.2 自定义水平布局示例
kotlin
@Composable
fun HorizontalLayout(
modifier: Modifier = Modifier,
spacing: Dp = 0.dp,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val spacingPx = spacing.roundToPx()
// 测量所有子元素
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 计算总宽度和最大高度
val totalWidth = placeables.sumOf { it.width } +
(placeables.size - 1).coerceAtLeast(0) * spacingPx
val maxHeight = placeables.maxOfOrNull { it.height } ?: 0
// 应用约束
val layoutWidth = totalWidth.coerceIn(constraints.minWidth, constraints.maxWidth)
val layoutHeight = maxHeight.coerceIn(constraints.minHeight, constraints.maxHeight)
// 放置子元素
layout(layoutWidth, layoutHeight) {
var xPosition = 0
placeables.forEach { placeable ->
// 垂直居中
val yPosition = (layoutHeight - placeable.height) / 2
placeable.placeRelative(x = xPosition, y = yPosition)
xPosition += placeable.width + spacingPx
}
}
}
}
// 使用
@Composable
fun HorizontalLayoutDemo() {
HorizontalLayout(
modifier = Modifier.fillMaxWidth(),
spacing = 16.dp
) {
Box(Modifier.size(50.dp).background(Color.Red))
Box(Modifier.size(70.dp).background(Color.Green))
Box(Modifier.size(60.dp).background(Color.Blue))
}
}
5.3 自定义网格布局
kotlin
@Composable
fun GridLayout(
modifier: Modifier = Modifier,
columns: Int = 2,
spacing: Dp = 8.dp,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val spacingPx = spacing.roundToPx()
// 计算每个单元格的宽度
val totalSpacing = (columns - 1) * spacingPx
val cellWidth = (constraints.maxWidth - totalSpacing) / columns
// 创建单元格约束
val cellConstraints = Constraints.fixedWidth(cellWidth)
// 测量所有子元素
val placeables = measurables.map { measurable ->
measurable.measure(cellConstraints)
}
// 计算行数
val rows = (placeables.size + columns - 1) / columns
// 计算每行的高度
val rowHeights = mutableListOf<Int>()
for (row in 0 until rows) {
val startIndex = row * columns
val endIndex = minOf(startIndex + columns, placeables.size)
val maxHeight = placeables.subList(startIndex, endIndex)
.maxOfOrNull { it.height } ?: 0
rowHeights.add(maxHeight)
}
// 计算总高度
val totalHeight = rowHeights.sum() + (rows - 1).coerceAtLeast(0) * spacingPx
// 放置子元素
layout(constraints.maxWidth, totalHeight) {
var yPosition = 0
placeables.chunked(columns).forEachIndexed { rowIndex, rowPlaceables ->
var xPosition = 0
val rowHeight = rowHeights[rowIndex]
rowPlaceables.forEach { placeable ->
placeable.placeRelative(x = xPosition, y = yPosition)
xPosition += cellWidth + spacingPx
}
yPosition += rowHeight + spacingPx
}
}
}
}
// 使用
@Composable
fun GridLayoutDemo() {
GridLayout(
modifier = Modifier.fillMaxWidth(),
columns = 3,
spacing = 8.dp
) {
repeat(7) { index ->
Box(
modifier = Modifier
.height(80.dp)
.background(Color(0xFF6200EE + index * 100)),
contentAlignment = Alignment.Center
) {
Text("$index", color = Color.White)
}
}
}
}
5.4 自定义流式布局(Flow Layout)
kotlin
@Composable
fun FlowLayout(
modifier: Modifier = Modifier,
horizontalSpacing: Dp = 8.dp,
verticalSpacing: Dp = 8.dp,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val hSpacingPx = horizontalSpacing.roundToPx()
val vSpacingPx = verticalSpacing.roundToPx()
var currentRowWidth = 0
var currentRowHeight = 0
var totalHeight = 0
val rowPlaceables = mutableListOf<List<Placeable>>()
var currentRow = mutableListOf<Placeable>()
// 测量并分配到行
measurables.forEach { measurable ->
val placeable = measurable.measure(constraints)
// 检查是否需要换行
if (currentRow.isNotEmpty() &&
currentRowWidth + hSpacingPx + placeable.width > constraints.maxWidth) {
// 保存当前行
rowPlaceables.add(currentRow)
totalHeight += currentRowHeight + vSpacingPx
// 开始新行
currentRow = mutableListOf()
currentRowWidth = 0
currentRowHeight = 0
}
currentRow.add(placeable)
currentRowWidth += if (currentRow.size == 1) placeable.width
else hSpacingPx + placeable.width
currentRowHeight = maxOf(currentRowHeight, placeable.height)
}
// 添加最后一行
if (currentRow.isNotEmpty()) {
rowPlaceables.add(currentRow)
totalHeight += currentRowHeight
}
// 放置子元素
layout(constraints.maxWidth, totalHeight) {
var yPosition = 0
rowPlaceables.forEach { row ->
var xPosition = 0
val rowHeight = row.maxOfOrNull { it.height } ?: 0
row.forEach { placeable ->
placeable.placeRelative(x = xPosition, y = yPosition)
xPosition += placeable.width + hSpacingPx
}
yPosition += rowHeight + vSpacingPx
}
}
}
}
// 使用
@Composable
fun FlowLayoutDemo() {
FlowLayout(
modifier = Modifier.fillMaxWidth(),
horizontalSpacing = 8.dp,
verticalSpacing = 8.dp
) {
val tags = listOf(
"Android", "Kotlin", "Jetpack Compose", "UI",
"Layout", "Custom View", "Performance", "Animation",
"Material Design", "State Management"
)
tags.forEach { tag ->
Chip(text = tag)
}
}
}
@Composable
fun Chip(text: String) {
Box(
modifier = Modifier
.background(Color(0xFFE0E0E0), RoundedCornerShape(16.dp))
.padding(horizontal = 12.dp, vertical = 6.dp)
) {
Text(text, fontSize = 14.sp)
}
}
5.5 使用 MeasurePolicy 复用布局逻辑
kotlin
// 定义可复用的测量策略
val HorizontalMeasurePolicy = MeasurePolicy { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
val width = placeables.sumOf { it.width }
val height = placeables.maxOfOrNull { it.height } ?: 0
layout(width, height) {
var x = 0
placeables.forEach { placeable ->
placeable.placeRelative(x, 0)
x += placeable.width
}
}
}
// 使用
@Composable
fun ReusableLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier,
measurePolicy = HorizontalMeasurePolicy
)
}
六、性能优化与最佳实践
6.1 避免不必要的重组
kotlin
// ❌ 不推荐:在循环中创建状态
@Composable
fun BadList() {
Column {
items.forEach { item ->
// 每次重组都会创建新的状态
var isExpanded by remember { mutableStateOf(false) }
ListItem(item, isExpanded)
}
}
}
// ✅ 推荐:状态提升到合适的位置
@Composable
fun GoodList(items: List<Item>) {
Column {
items.forEach { item ->
// 使用 key 确保状态与 item 关联
key(item.id) {
var isExpanded by remember { mutableStateOf(false) }
ListItem(item, isExpanded) { isExpanded = it }
}
}
}
}
6.2 合理使用 remember
kotlin
// ❌ 不推荐:在 remember 中进行耗时计算
@Composable
fun BadCalculation(data: List<Int>) {
// 如果 calculation 很耗时,会阻塞组合
val result = remember(data) {
data.map { expensiveCalculation(it) }
}
}
// ✅ 推荐:使用 derivedStateOf 优化频繁变化的状态
@Composable
fun GoodOptimization(items: List<Item>) {
val searchQuery by remember { mutableStateOf("") }
// 只在 searchQuery 变化时重新计算
val filteredItems by remember(items) {
derivedStateOf {
items.filter { it.name.contains(searchQuery, ignoreCase = true) }
}
}
}
6.3 布局阶段优化
kotlin
// ❌ 不推荐:频繁改变影响测量的状态
@Composable
fun BadLayoutAnimation() {
var width by remember { mutableStateOf(100) }
// 动画改变宽度,会触发测量+放置+绘制
LaunchedEffect(Unit) {
animate(
initialValue = 100f,
targetValue = 300f,
animationSpec = infiniteRepeatable(tween(1000))
) { value, _ ->
width = value.toInt() // 触发测量!
}
}
Box(modifier = Modifier.width(width.dp))
}
// ✅ 推荐:使用 graphicsLayer 进行变换
@Composable
fun GoodLayoutAnimation() {
val animatedProgress = remember { Animatable(0f) }
LaunchedEffect(Unit) {
animatedProgress.animateTo(
targetValue = 1f,
animationSpec = infiniteRepeatable(tween(1000))
)
}
// 只触发绘制,不触发测量
Box(
modifier = Modifier
.width(100.dp)
.graphicsLayer {
scaleX = 1f + animatedProgress.value * 2f
}
)
}
6.4 绘制阶段优化
kotlin
// ❌ 不推荐:在绘制中创建对象
@Composable
fun BadDraw() {
Canvas(modifier = Modifier.fillMaxSize()) {
// 每次绘制都创建新的 Paint
drawIntoCanvas { canvas ->
val paint = Paint().apply { // ❌ 创建对象
color = Color.Red
}
canvas.drawRect(rect, paint)
}
}
}
// ✅ 推荐:缓存绘制对象
@Composable
fun GoodDraw() {
val paint = remember { Paint() } // ✅ 缓存对象
Canvas(modifier = Modifier.fillMaxSize()) {
paint.color = Color.Red
drawIntoCanvas { canvas ->
canvas.drawRect(rect, paint)
}
}
}
6.5 布局层级优化
kotlin
// ❌ 不推荐:深层嵌套
@Composable
fun DeepNesting() {
Box {
Column {
Row {
Box {
Column {
Text("Deep nesting")
}
}
}
}
}
}
// ✅ 推荐:扁平化布局
@Composable
fun FlatLayout() {
// 使用 ConstraintLayout 或自定义 Layout 减少层级
ConstraintLayout {
val (text1, text2, button) = createRefs()
Text("Title", modifier = Modifier.constrainAs(text1) {
top.linkTo(parent.top)
})
Text("Subtitle", modifier = Modifier.constrainAs(text2) {
top.linkTo(text1.bottom)
})
Button(onClick = {}, modifier = Modifier.constrainAs(button) {
top.linkTo(text2.bottom)
}) {
Text("Action")
}
}
}
七、常见问题与解决方案
7.1 无限重组问题
kotlin
// ❌ 问题代码:在重组中修改状态
@Composable
fun InfiniteRecomposition() {
var count by remember { mutableStateOf(0) }
// 错误:在组合中直接修改状态
count++ // 这会导致无限重组!
Text("Count: $count")
}
// ✅ 解决方案:使用副作用
@Composable
fun FixedRecomposition() {
var count by remember { mutableStateOf(0) }
// 使用 LaunchedEffect 在副作用中修改状态
LaunchedEffect(Unit) {
while (true) {
delay(1000)
count++
}
}
Text("Count: $count")
}
7.2 布局尺寸为 0 的问题
kotlin
// ❌ 问题:子元素没有尺寸
@Composable
fun ZeroSizeProblem() {
Layout(content = {
// 子元素没有设置尺寸
Box {}
}) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
// placeables 的 width/height 可能为 0
layout(0, 0) {}
}
}
// ✅ 解决方案:确保子元素有尺寸或使用约束
@Composable
fun FixedSize() {
Layout(content = {
Box(Modifier.size(100.dp)) // 明确设置尺寸
}) { measurables, constraints ->
val placeables = measurables.map {
it.measure(Constraints.fixed(100, 100)) // 或使用固定约束
}
layout(100, 100) {
placeables.forEach { it.placeRelative(0, 0) }
}
}
}
7.3 约束传递问题
kotlin
// ❌ 问题:没有正确传递约束
@Composable
fun ConstraintProblem() {
Layout(content = { Text("Long text that might overflow") }) { measurables, constraints ->
val placeable = measurables.first().measure(
Constraints() // 无约束,可能导致溢出
)
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
}
// ✅ 解决方案:正确传递约束
@Composable
fun FixedConstraint() {
Layout(content = { Text("Long text") }) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(
width = min(placeable.width, constraints.maxWidth),
height = min(placeable.height, constraints.maxHeight)
) {
placeable.placeRelative(0, 0)
}
}
}
7.4 RTL 布局支持
kotlin
// 自定义布局需要考虑 RTL
@Composable
fun RtlAwareLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(content, modifier) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.sumOf { it.width }
val height = placeables.maxOfOrNull { it.height } ?: 0
layout(width, height) {
var x = 0
placeables.forEach { placeable ->
// 使用 placeRelative 自动处理 RTL
placeable.placeRelative(x, 0)
x += placeable.width
}
}
}
}
总结
Jetpack Compose 的渲染流程遵循组合 → 布局 → 绘制三个阶段,每个阶段都有其特定的职责:
- 组合阶段:构建和更新 LayoutNode 视图树,决定显示什么内容
- 布局阶段:测量和放置子元素,决定元素的位置和尺寸
- 绘制阶段:将元素渲染到屏幕,决定最终视觉效果
理解状态读取在不同阶段的影响对于性能优化至关重要。通过将状态读取推迟到合适的阶段(尽可能在绘制阶段),可以显著减少不必要的重组和布局计算,提升应用性能。
自定义布局通过 Layout 可组合函数和 MeasurePolicy 提供了强大的灵活性,允许开发者实现各种复杂的布局需求。