本篇文章将从源码层面深入解析 Compose 的核心机制,帮助你真正理解 Modifier 的设计哲学、布局系统的测量流程,以及 LazyColumn 的惰性加载原理。
前言
在上一篇中,我们完成了 Compose 的入门。但如果你只停留在"会用"的层面,遇到复杂场景时就会束手无策。比如:
- 为什么 Modifier 的调用顺序会影响最终效果?
- LazyColumn 为什么能流畅处理上万条数据?
- 自定义 Layout 时,Constraints 到底该怎么用?
- 什么时候应该用 ConstraintLayout,什么时候用嵌套 Column/Row?
本篇文章将带你深入原理层,不仅告诉你"怎么做",更要让你理解"为什么这样做"。准备好了吗?让我们开始深度探索!
一、Modifier 深度解析:从设计哲学到源码实现
1.1 为什么需要 Modifier?
在传统 View 系统中,每个 View 都有一大堆属性方法:setPadding()、setBackground()、setOnClickListener()... 这不仅让 View 类变得臃肿,也限制了扩展性。
Compose 采用了完全不同的设计:将修饰能力从组件中分离出来,形成独立的 Modifier 系统。
设计哲学:
- 单一职责:Text 只负责显示文字,修饰交给 Modifier
- 可组合:通过链式调用组合各种修饰效果
- 可扩展:随时可以增加新的 Modifier,无需修改组件
1.2 Modifier 的本质:接口与链表
Modifier 是一个接口,它的核心设计非常精巧:
// 简化版源码
interface Modifier {
// 从左到右遍历所有 Element
fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
// 从右到左遍历所有 Element
fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
// 检查是否有满足条件的 Element
fun any(predicate: (Element) -> Boolean): Boolean
// 检查是否所有 Element 都满足条件
fun all(predicate: (Element) -> Boolean): Boolean
// 组合两个 Modifier
infix fun then(other: Modifier): Modifier
// 空的 Modifier 实例
companion object : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
override fun then(other: Modifier): Modifier = other
}
}
关键设计:Modifier 内部使用链表结构存储所有的修饰元素。
1.3 链式调用的秘密
当我们写下这样的代码时,发生了什么?
Modifier
.padding(16.dp)
.background(Color.Blue)
.size(100.dp)
执行过程:
步骤 1: Modifier (空)
↓ then()
步骤 2: Modifier + PaddingModifier
↓ then()
步骤 3: Modifier + PaddingModifier + BackgroundModifier
↓ then()
步骤 4: Modifier + PaddingModifier + BackgroundModifier + SizeModifier
源码实现:
// Modifier.kt
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
// CombinedModifier 将两个 Modifier 组合在一起
internal class CombinedModifier(
private val outer: Modifier,
private val inner: Modifier
) : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
inner.foldIn(outer.foldIn(initial, operation), operation)
override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
outer.foldOut(inner.foldOut(initial, operation), operation)
}
核心洞察:CombinedModifier 是一个二叉树结构,outer 指向链表头部,inner 指向链表尾部。
1.4 执行顺序:为什么顺序很重要?
这是 Modifier 最容易让人困惑的地方。让我们通过一个例子理解:
// 情况 A
Modifier
.padding(16.dp) // 外层
.background(Color.Blue) // 中间
.padding(8.dp) // 内层
.size(100.dp)
视觉效果:
┌─────────────────────────────┐ ← 外层 padding (16dp) - 透明区域
│ │
│ ┌───────────────────────┐ │
│ │ 蓝色背景 │ │
│ │ ┌─────────────────┐ │ │
│ │ │ │ │ │ ← 内层 padding (8dp) - 蓝色区域内
│ │ │ 100x100 │ │ │
│ │ │ 内容 │ │ │
│ │ │ │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ └───────────────────────┘ │
│ │
└─────────────────────────────┘
设计原则:Modifier 的执行遵循**"从外到内"**的顺序。先应用的 Modifier 在外层,后应用的在内层。
1.5 Modifier 的遍历机制
Compose 需要遍历 Modifier 链表来应用各种效果。遍历有两种方式:
// foldIn: 从左到右(outer → inner)
modifier.foldIn(initialValue) { acc, element ->
// 处理每个 Element
acc + element
}
// foldOut: 从右到左(inner → outer)
modifier.foldOut(initialValue) { element, acc ->
// 处理每个 Element
element + acc
}
应用场景:
- foldIn:测量、布局时,需要从外到内传递约束
- foldOut:绘制时,需要从内到外绘制内容
1.6 Modifier 的分类与内部实现
1.6.1 布局类 Modifier
size() 的实现原理:
// SizeModifier 简化版
private class SizeModifier(
private val minWidth: Dp = Dp.Unspecified,
private val minHeight: Dp = Dp.Unspecified,
private val maxWidth: Dp = Dp.Unspecified,
private val maxHeight: Dp = Dp.Unspecified,
private val enforceIncoming: Boolean = true
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
// 解析尺寸值
val resolvedMinWidth = if (minWidth != Dp.Unspecified) {
minWidth.roundToPx()
} else {
constraints.minWidth
}
val resolvedMaxWidth = if (maxWidth != Dp.Unspecified) {
maxWidth.roundToPx()
} else {
if (enforceIncoming) constraints.maxWidth else Constraints.Infinity
}
// ... 高度同理
// 创建新的约束
val wrappedConstraints = Constraints(
minWidth = resolvedMinWidth,
maxWidth = resolvedMaxWidth,
minHeight = resolvedMinHeight,
maxHeight = resolvedMaxHeight
)
// 测量子元素
val placeable = measurable.measure(wrappedConstraints)
// 返回测量结果
return layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
}
关键洞察:SizeModifier 是一个 LayoutModifier,它拦截了测量过程,修改了传递给子元素的 Constraints。
1.6.2 绘制类 Modifier
background() 的实现原理:
// BackgroundModifier 简化版
private class BackgroundModifier(
private val color: Color? = null,
private val brush: Brush? = null,
private val shape: Shape = RectangleShape,
private val alpha: Float = 1.0f
) : DrawModifier {
override fun ContentDrawScope.draw() {
// 绘制背景
if (shape == RectangleShape) {
// 矩形背景
if (color != null) {
drawRect(color = color, alpha = alpha)
} else if (brush != null) {
drawRect(brush = brush, alpha = alpha)
}
} else {
// 圆角/自定义形状背景
val outline = shape.createOutline(size, layoutDirection, this)
if (color != null) {
drawOutline(outline, color = color, alpha = alpha)
} else if (brush != null) {
drawOutline(outline, brush = brush, alpha = alpha)
}
}
// 绘制内容(调用 drawContent 继续链式绘制)
drawContent()
}
}
关键洞察:DrawModifier 拦截了绘制流程,先绘制背景,再通过 drawContent() 继续绘制后续内容和子元素。
1.6.3 交互类 Modifier
clickable() 的实现原理:
// clickable 的 Modifier 链
fun Modifier.clickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
): Modifier = composed(
inspectorInfo = debugInspectorInfo { ... }
) {
val interactionSource = remember { MutableInteractionSource() }
val indication = LocalIndication.current
Modifier
.clickable(
interactionSource = interactionSource,
indication = indication,
enabled = enabled,
onClickLabel = onClickLabel,
role = role,
onClick = onClick
)
}
clickable 内部 Modifier 链:
clickable
├── focusableInNonTouchMode (焦点管理)
├── pointerInput (手势检测)
├── semantics (无障碍支持)
├── indication (涟漪效果)
└── isComposeRootInScrollableContainer (滚动容器检测)
1.7 自定义 Modifier 的完整指南
1.7.1 简单自定义:使用 composed
适用于需要访问 CompositionLocal 或 remember 状态的场景:
// 自定义带阴影的圆角背景
fun Modifier.shadowBackground(
color: Color,
cornerRadius: Dp = 8.dp,
shadowElevation: Dp = 4.dp
): Modifier = composed {
// 可以在这里使用 remember 和 CompositionLocal
val density = LocalDensity.current
val shadowPx = with(density) { shadowElevation.toPx() }
remember(color, cornerRadius, shadowElevation) {
Modifier
.shadow(shadowElevation, RoundedCornerShape(cornerRadius))
.background(color, RoundedCornerShape(cornerRadius))
.padding(cornerRadius)
}
}
1.7.2 中级自定义:实现 Modifier.NodeElement
适用于需要自定义测量、布局或绘制的场景:
// 自定义绘制对角线的 Modifier
class DiagonalLineElement(
private val color: Color,
private val strokeWidth: Float
) : ModifierNodeElement<DiagonalLineNode>() {
override fun create(): DiagonalLineNode =
DiagonalLineNode(color, strokeWidth)
override fun update(node: DiagonalLineNode) {
node.color = color
node.strokeWidth = strokeWidth
}
override fun hashCode(): Int =
color.hashCode() * 31 + strokeWidth.hashCode()
override fun equals(other: Any?): Boolean =
other is DiagonalLineElement &&
other.color == color &&
other.strokeWidth == strokeWidth
}
class DiagonalLineNode(
var color: Color,
var strokeWidth: Float
) : Modifier.Node(), DrawModifierNode {
override fun ContentDrawScope.draw() {
// 先绘制内容
drawContent()
// 再绘制对角线
drawLine(
color = color,
start = Offset(0f, 0f),
end = Offset(size.width, size.height),
strokeWidth = strokeWidth
)
}
}
// 使用扩展函数
fun Modifier.diagonalLine(
color: Color,
strokeWidth: Float = 2f
): Modifier = this.then(DiagonalLineElement(color, strokeWidth))
1.7.3 高级自定义:LayoutModifier
适用于需要自定义测量和布局逻辑的场景:
// 自定义 AspectRatio Modifier(保持宽高比)
class AspectRatioElement(
private val ratio: Float
) : ModifierNodeElement<AspectRatioNode>() {
override fun create() = AspectRatioNode(ratio)
override fun update(node: AspectRatioNode) {
node.ratio = ratio
}
}
class AspectRatioNode(var ratio: Float) :
Modifier.Node(),
LayoutModifierNode {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
// 根据约束计算合适的尺寸
val width = constraints.maxWidth
val height = (width / ratio).toInt()
// 确保不超过最大高度
val finalHeight = if (height > constraints.maxHeight) {
constraints.maxHeight
} else {
height
}
// 重新计算宽度以保持比例
val finalWidth = (finalHeight * ratio).toInt()
// 测量子元素
val placeable = measurable.measure(
Constraints.fixed(finalWidth, finalHeight)
)
return layout(finalWidth, finalHeight) {
placeable.placeRelative(0, 0)
}
}
}
1.8 Intrinsic 测量:解决"先有鸡还是先有蛋"
1.8.1 问题场景
考虑这样一个需求:两个 Text 垂直排列,要求 Divider 的宽度等于两个 Text 中最宽的那个。
Column {
Text("Short")
Text("Longer text")
Divider() // 宽度应该等于 "Longer text" 的宽度
}
问题:在测量 Divider 时,Column 还不知道两个 Text 的宽度,因为 Text 还没被测量。
1.8.2 Intrinsic 测量的原理
Intrinsic 测量允许父组件在正式测量前,预先询问子元素的期望尺寸:
interface IntrinsicMeasurable {
// 给定高度,返回最小/最大宽度
fun minIntrinsicWidth(height: Int): Int
fun maxIntrinsicWidth(height: Int): Int
// 给定宽度,返回最小/最大高度
fun minIntrinsicHeight(width: Int): Int
fun maxIntrinsicHeight(width: Int): Int
}
使用 Intrinsic 测量解决 Divider 问题:
@Composable
fun IntrinsicWidthDemo() {
Column(
modifier = Modifier.width(IntrinsicSize.Max) // 使用最大固有宽度
) {
Text("Short text")
Text("This is a longer text")
Divider(
modifier = Modifier.fillMaxWidth(),
color = Color.Black
)
}
}
执行流程:
1. Column 收到 IntrinsicSize.Max 约束
2. Column 询问所有子元素的 maxIntrinsicWidth
3. Text 返回自己的文本宽度
4. Column 取最大值作为自身宽度
5. Column 正式测量,将宽度约束传递给子元素
6. Divider 收到与最宽 Text 相同的宽度
1.8.3 IntrinsicSize.Min 与 IntrinsicSize.Max
| 类型 | 说明 | 应用场景 |
|---|---|---|
IntrinsicSize.Min | 使用子元素的最小固有尺寸 | 按钮等需要紧凑尺寸的场景 |
IntrinsicSize.Max | 使用子元素的最大固有尺寸 | 表单等需要对齐的场景 |
1.9 Modifier 性能优化
1.9.1 避免不必要的重组
// ❌ 错误:每次重组都创建新的 Modifier
@Composable
fun BadExample() {
var count by remember { mutableStateOf(0) }
Text(
text = "Count: $count",
modifier = Modifier.padding(16.dp) // 每次重组都创建新实例
)
}
// ✅ 正确:使用 remember 缓存 Modifier
@Composable
fun GoodExample() {
var count by remember { mutableStateOf(0) }
val modifier = remember { Modifier.padding(16.dp) }
Text(
text = "Count: $count",
modifier = modifier // 复用同一个实例
)
}
// ✅ 更好:将 Modifier 作为参数,由父组件提供
@Composable
fun BestExample(
text: String,
modifier: Modifier = Modifier // 由调用方提供
) {
Text(
text = text,
modifier = modifier
)
}
1.9.2 Modifier 的相等性优化
Compose 使用 equals() 判断 Modifier 是否变化,从而决定是否跳过重组:
// 自定义 Modifier 时,务必实现 equals 和 hashCode
class MyModifier(
private val color: Color,
private val size: Dp
) : Modifier.Element {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is MyModifier) return false
return color == other.color && size == other.size
}
override fun hashCode(): Int {
return color.hashCode() * 31 + size.hashCode()
}
}
二、布局系统深度解析:从测量到绘制
2.1 Compose 布局的三阶段
Compose 的渲染流程分为三个阶段:
┌─────────────────────────────────────────────────────────────┐
│ Composition │
│ 执行 Composable 函数,构建 UI 树(LayoutNode 树) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Layout │
│ 测量每个节点的尺寸(Measure),确定位置(Place) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Drawing │
│ 将 UI 绘制到 Canvas 上 │
└─────────────────────────────────────────────────────────────┘
2.2 Layout 节点的数据结构
每个 Composable 在布局阶段都会对应一个 LayoutNode:
// 简化版 LayoutNode 结构
class LayoutNode {
// 父节点
var parent: LayoutNode? = null
// 子节点列表
val children: MutableList<LayoutNode> = mutableListOf()
// Modifier 链表
var modifier: Modifier = Modifier
// 测量结果
var width: Int = 0
var height: Int = 0
// 位置
var x: Int = 0
var y: Int = 0
// 测量函数(由 Layout Composable 提供)
lateinit var measurePolicy: MeasurePolicy
}
2.3 Constraints:布局的约束条件
Constraints 是 Compose 布局系统的核心,它定义了尺寸的限制范围:
class Constraints {
val minWidth: Int
val maxWidth: Int
val minHeight: Int
val maxHeight: Int
companion object {
// 创建固定尺寸约束
fun fixed(width: Int, height: Int): Constraints
// 创建最大尺寸约束
fun maxWidth(width: Int): Constraints
fun maxHeight(height: Int): Constraints
// 无限约束
val Infinity = Int.MAX_VALUE
}
}
约束类型:
┌─────────────────────────────────────────────────────────────┐
│ 约束类型图解 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 精确约束 范围约束 │
│ ┌─────────┐ ┌───────────────────────────┐ │
│ │ │ │ minWidth maxWidth│ │
│ │ 100x100 │ │ ├─────────────────────┤ │ │
│ │ │ │ 可以是任意值 │ │
│ └─────────┘ └───────────────────────────┘ │
│ minWidth = maxWidth = 100 │
│ │
│ 无约束 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Infinity │ │
│ │ 可以是任意大的尺寸 │ │
│ └─────────────────────────────────────────────────────┘ │
│ maxWidth = Int.MAX_VALUE │
│ │
└─────────────────────────────────────────────────────────────┘
2.4 测量流程详解
测量是一个自顶向下的过程:
// 父节点测量子节点的流程
fun measureChild(child: Measurable, constraints: Constraints): Placeable {
// 1. 根据父节点的约束,计算给子节点的约束
val childConstraints = calculateChildConstraints(constraints)
// 2. 让子节点测量自己
val placeable = child.measure(childConstraints)
// 3. 返回测量结果(Placeable)
return placeable
}
完整测量流程示例:
@Composable
fun MeasureDemo() {
Layout(
content = {
Text("Child 1")
Text("Child 2")
}
) { measurables, constraints ->
// constraints: 父节点给当前节点的约束
println("父约束: minW=${constraints.minWidth}, maxW=${constraints.maxWidth}")
// 测量子节点
val placeables = measurables.map { measurable ->
// 可以给子节点不同的约束
val childConstraints = Constraints(
minWidth = 0,
maxWidth = constraints.maxWidth / 2, // 每个子节点最多一半宽度
minHeight = 0,
maxHeight = constraints.maxHeight
)
measurable.measure(childConstraints)
}
// 计算自己的尺寸
val width = placeables.sumOf { it.width }
val height = placeables.maxOf { it.height }
// 布局
layout(width, height) {
var x = 0
placeables.forEach { placeable ->
placeable.placeRelative(x, 0)
x += placeable.width
}
}
}
}
2.5 Column 的测量与布局原理
让我们深入分析 Column 的实现:
// Column 的简化实现
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment
)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
// Column 的测量策略
private fun columnMeasurePolicy(
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal
): MeasurePolicy = MeasurePolicy { measurables, constraints ->
// 1. 测量所有子元素
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 2. 计算 Column 的尺寸
val width = placeables.maxOf { it.width }
.coerceIn(constraints.minWidth, constraints.maxWidth)
val height = placeables.sumOf { it.height }
.coerceIn(constraints.minHeight, constraints.maxHeight)
// 3. 计算垂直位置(根据 Arrangement)
val positions = verticalArrangement.arrange(
totalSize = height,
sizes = placeables.map { it.height },
layoutDirection = layoutDirection
)
// 4. 布局
layout(width, height) {
placeables.forEachIndexed { index, placeable ->
// 计算水平位置(根据 Alignment)
val x = horizontalAlignment.align(
size = placeable.width,
space = width,
layoutDirection = layoutDirection
)
placeable.placeRelative(x, positions[index])
}
}
}
关键设计:
- Column 给子元素的约束是完整的父约束,子元素可以决定自己的高度
- Column 的总高度是子元素高度之和
- Arrangement 决定子元素的垂直分布
- Alignment 决定子元素的水平对齐
2.6 Row 的测量与布局原理
Row 与 Column 类似,只是方向不同:
// Row 的测量策略
private fun rowMeasurePolicy(
horizontalArrangement: Arrangement.Horizontal,
verticalAlignment: Alignment.Vertical
): MeasurePolicy = MeasurePolicy { measurables, constraints ->
// 1. 测量所有子元素
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 2. 计算 Row 的尺寸
val width = placeables.sumOf { it.width }
.coerceIn(constraints.minWidth, constraints.maxWidth)
val height = placeables.maxOf { it.height }
.coerceIn(constraints.minHeight, constraints.maxHeight)
// 3. 计算水平位置(根据 Arrangement)
val positions = horizontalArrangement.arrange(
totalSize = width,
sizes = placeables.map { it.width },
layoutDirection = layoutDirection
)
// 4. 布局
layout(width, height) {
placeables.forEachIndexed { index, placeable ->
// 计算垂直位置(根据 Alignment)
val y = verticalAlignment.align(
size = placeable.height,
space = height
)
placeable.placeRelative(positions[index], y)
}
}
}
2.7 Box 的测量与布局原理
Box 是层叠布局,子元素可以重叠:
// Box 的测量策略
private fun boxMeasurePolicy(alignment: Alignment): MeasurePolicy =
MeasurePolicy { measurables, constraints ->
// 1. 测量所有子元素
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 2. 计算 Box 的尺寸
val width = placeables.maxOf { it.width }
.coerceIn(constraints.minWidth, constraints.maxWidth)
val height = placeables.maxOf { it.height }
.coerceIn(constraints.minHeight, constraints.maxHeight)
// 3. 布局(所有子元素都使用相同的对齐方式)
layout(width, height) {
placeables.forEach { placeable ->
val x = alignment.align(
size = placeable.width,
space = width,
layoutDirection = layoutDirection
)
val y = alignment.align(
size = placeable.height,
space = height
)
placeable.placeRelative(x, y)
}
}
}
Box 的特殊之处:
- 所有子元素都测量为最大尺寸
- 子元素默认使用 contentAlignment 对齐
- 可以单独指定子元素的对齐方式(使用 Modifier.align)
2.8 自定义 Layout 的设计思路
2.8.1 设计自定义布局的步骤
- 明确需求:要实现什么样的布局效果?
- 确定约束:子元素的尺寸如何确定?
- 设计测量策略:如何测量子元素?
- 设计布局策略:子元素如何排列?
2.8.2 实战:自定义瀑布流布局
/**
* 瀑布流布局(StaggeredGrid)
*
* 设计思路:
* 1. 将子元素分配到多列中
* 2. 每列独立计算高度
* 3. 新元素添加到最短的列
*/
@Composable
fun StaggeredGrid(
columns: Int = 2,
horizontalGap: Dp = 8.dp,
verticalGap: Dp = 8.dp,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val hGapPx = horizontalGap.roundToPx()
val vGapPx = verticalGap.roundToPx()
// 计算每列的宽度
val columnWidth = (constraints.maxWidth - (columns - 1) * hGapPx) / columns
// 创建列高度追踪器
val columnHeights = IntArray(columns) { 0 }
// 存储每个子元素的位置信息
data class ItemInfo(
val placeable: Placeable,
val column: Int,
val x: Int,
val y: Int
)
val itemInfos = mutableListOf<ItemInfo>()
// 测量并放置每个子元素
measurables.forEach { measurable ->
// 测量子元素(宽度固定为列宽,高度不限)
val placeable = measurable.measure(
Constraints.fixedWidth(columnWidth)
)
// 找到最短的列
val shortestColumn = columnHeights.indices.minByOrNull { columnHeights[it] } ?: 0
// 计算位置
val x = shortestColumn * (columnWidth + hGapPx)
val y = columnHeights[shortestColumn]
// 更新列高度
columnHeights[shortestColumn] += placeable.height + vGapPx
itemInfos.add(ItemInfo(placeable, shortestColumn, x, y))
}
// 计算总高度(最高列的高度)
val totalHeight = columnHeights.maxOrNull()?.let {
it - vGapPx // 减去最后一个间隙
} ?: 0
// 布局
layout(constraints.maxWidth, totalHeight.coerceIn(constraints.minHeight, constraints.maxHeight)) {
itemInfos.forEach { info ->
info.placeable.placeRelative(info.x, info.y)
}
}
}
}
// 使用示例
@Composable
fun StaggeredGridDemo() {
val items = listOf(
"短文本" to 80,
"这是一个比较长的文本内容" to 120,
"中等" to 100,
"很长很长很长的文本内容在这里" to 150,
"短" to 60,
)
StaggeredGrid(
columns = 2,
horizontalGap = 8.dp,
verticalGap = 8.dp,
modifier = Modifier.fillMaxWidth()
) {
items.forEach { (text, height) ->
Card(
modifier = Modifier.height(height.dp)
) {
Text(
text = text,
modifier = Modifier.padding(8.dp)
)
}
}
}
}
2.9 SubcomposeLayout:延迟组合
SubcomposeLayout 允许在测量阶段动态创建子元素,这在某些场景下非常有用。
2.9.1 使用场景
- 根据测量结果决定子元素内容
- 实现复杂的自适应布局
- 动态加载内容
2.9.2 实战:响应式文本
/**
* 响应式文本:根据可用宽度自动调整字体大小
*/
@Composable
fun ResponsiveText(
text: String,
modifier: Modifier = Modifier,
minFontSize: TextUnit = 12.sp,
maxFontSize: TextUnit = 48.sp
) {
SubcomposeLayout(modifier = modifier) { constraints ->
var fontSize = maxFontSize
var placeable: Placeable? = null
// 二分查找合适的字体大小
while (fontSize >= minFontSize) {
val measurable = subcompose("text_$fontSize") {
Text(
text = text,
fontSize = fontSize,
maxLines = 1
)
}[0]
val testPlaceable = measurable.measure(constraints)
if (testPlaceable.width <= constraints.maxWidth) {
placeable = testPlaceable
break
}
fontSize *= 0.9f
}
placeable = placeable ?: subcompose("text_min") {
Text(text = text, fontSize = minFontSize, maxLines = 1)
}[0].measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
}
三、LazyColumn 深度解析:惰性加载的艺术
3.1 为什么需要惰性加载?
假设我们要显示 10000 条数据的列表:
// 使用普通 Column - 会创建 10000 个 Text 组件!
Column {
repeat(10000) { index ->
Text("Item $index")
}
}
问题:
- 内存占用:10000 个 Text 组件同时存在
- 首次加载:需要测量和布局所有 item
- 滑动性能:大量视图参与绘制
3.2 LazyColumn 的解决方案
LazyColumn 采用视口渲染 + 对象池复用策略:
┌────────────────────────────────────────────────────────────────┐
│ LazyColumn 视口 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 可见区域 (Viewport) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Item 5 │ │ Item 6 │ │ Item 7 │ │ Item 8 │ │ │
│ │ │ (可见) │ │ (可见) │ │ (可见) │ │ (可见) │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Item 1-4: 已滑出视口,组件被回收,状态保存 │
│ Item 9+: 未进入视口,组件未创建 │
│ │
└────────────────────────────────────────────────────────────────┘
3.3 LazyColumn 的架构设计
LazyColumn
├── LazyListState (状态管理)
│ ├── firstVisibleItemIndex
│ ├── firstVisibleItemScrollOffset
│ └── layoutInfo
├── LazyListLayoutInfo (布局信息)
│ ├── visibleItemsInfo (可见 item 信息)
│ ├── totalItemsCount
│ └── viewportSize
└── LazyListItemProvider (Item 提供器)
└── itemProvider (根据索引创建 item)
3.4 测量与布局流程
LazyColumn 的测量流程与普通 Layout 不同:
// LazyColumn 的测量策略(简化版)
fun measureLazyList(constraints: Constraints): MeasureResult {
// 1. 计算视口大小
val viewportWidth = constraints.maxWidth
val viewportHeight = constraints.maxHeight
// 2. 确定可见 item 范围
val firstVisibleIndex = calculateFirstVisibleItem()
val lastVisibleIndex = calculateLastVisibleItem(viewportHeight)
// 3. 测量可见 item
val visiblePlaceables = mutableListOf<Placeable>()
for (index in firstVisibleIndex..lastVisibleIndex) {
val placeable = measureItem(index, constraints)
visiblePlaceables.add(placeable)
}
// 4. 计算总高度(预估)
val totalHeight = if (hasMoreItems) {
estimateTotalHeight()
} else {
visiblePlaceables.sumOf { it.height }
}
// 5. 布局
return layout(viewportWidth, totalHeight) {
var y = -scrollOffset
visiblePlaceables.forEach { placeable ->
placeable.placeRelative(0, y)
y += placeable.height
}
}
}
3.5 key 的作用与 diff 算法
3.5.1 为什么需要 key?
当列表数据变化时,Compose 需要知道:
- 哪些 item 是新增的?
- 哪些 item 被删除了?
- 哪些 item 移动了位置?
没有 key 的问题:
// 假设列表初始状态
items = [A, B, C]
// 删除 A 后
items = [B, C]
// 没有 key 时,Compose 会认为:
// - 位置 0 的内容从 A 变成了 B
// - 位置 1 的内容从 B 变成了 C
// - 位置 2 被删除
// 结果是:B 和 C 都会重组,状态丢失!
有 key 时:
items(items, key = { it.id }) { item ->
ItemCard(item)
}
// 删除 A 后
// Compose 知道:
// - A 被删除了
// - B 和 C 只是位置变了
// 结果是:只有 A 的组件被销毁,B 和 C 只是移动位置
3.5.2 key 的 diff 算法
Compose 使用类似 React 的 diff 算法:
旧列表: [A(key=1), B(key=2), C(key=3)]
新列表: [B(key=2), C(key=3), D(key=4)]
Diff 结果:
1. key=1 的 A 不在新列表中 → 删除 A
2. key=2 的 B 在位置 0 → 移动到位置 0
3. key=3 的 C 在位置 1 → 移动到位置 1
4. key=4 的 D 是新元素 → 创建 D
3.6 列表状态管理
3.6.1 记住滚动位置
@Composable
fun LazyColumnWithState() {
// rememberLazyListState 会自动保存和恢复滚动位置
val listState = rememberLazyListState()
LazyColumn(state = listState) {
items(1000) { index ->
Text("Item $index")
}
}
// 滚动到指定位置
LaunchedEffect(Unit) {
listState.scrollToItem(100)
}
// 平滑滚动
scope.launch {
listState.animateScrollToItem(100)
}
}
3.6.2 监听滚动状态
@Composable
fun LazyColumnWithScrollListener() {
val listState = rememberLazyListState()
// 使用 derivedStateOf 优化频繁变化的状态
val firstVisibleItem by remember {
derivedStateOf { listState.firstVisibleItemIndex }
}
val isScrolling by remember {
derivedStateOf { listState.isScrollInProgress }
}
// 判断是否滑到底部
val isAtBottom by remember {
derivedStateOf {
val layoutInfo = listState.layoutInfo
val visibleItems = layoutInfo.visibleItemsInfo
val lastVisibleItem = visibleItems.lastOrNull()
lastVisibleItem != null &&
lastVisibleItem.index >= layoutInfo.totalItemsCount - 1
}
}
LazyColumn(state = listState) {
// ...
}
}
3.7 分页加载实现
@Composable
fun PaginatedList() {
val listState = rememberLazyListState()
var items by remember { mutableStateOf<List<Item>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
var hasMore by remember { mutableStateOf(true) }
// 监听是否需要加载更多
val shouldLoadMore by remember {
derivedStateOf {
val layoutInfo = listState.layoutInfo
val visibleItems = layoutInfo.visibleItemsInfo
val lastVisibleItem = visibleItems.lastOrNull()
// 当最后一个可见 item 是倒数第 5 个时,开始加载
lastVisibleItem != null &&
lastVisibleItem.index >= layoutInfo.totalItemsCount - 5 &&
!isLoading && hasMore
}
}
// 加载数据
LaunchedEffect(shouldLoadMore) {
if (shouldLoadMore) {
isLoading = true
val newItems = repository.loadMore(items.size, pageSize = 20)
items = items + newItems
hasMore = newItems.size >= 20
isLoading = false
}
}
LazyColumn(state = listState) {
items(items, key = { it.id }) { item ->
ItemCard(item)
}
if (isLoading) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}
}
}
3.8 性能优化最佳实践
3.8.1 使用 key
// ✅ 正确:使用 key
items(dataList, key = { it.id }) { item ->
ItemCard(item)
}
// ❌ 错误:不使用 key
items(dataList) { item ->
ItemCard(item)
}
3.8.2 避免在 item 中创建新的对象
// ❌ 错误:每次重组都创建新的 lambda
items(dataList, key = { it.id }) { item ->
ItemCard(
item = item,
onClick = { viewModel.onItemClick(item) } // 每次重组都创建
)
}
// ✅ 正确:使用 remember 缓存 lambda
items(dataList, key = { it.id }) { item ->
val onClick = remember(item.id) {
{ viewModel.onItemClick(item) }
}
ItemCard(item = item, onClick = onClick)
}
3.8.3 使用 contentType 优化不同类型 item
LazyColumn {
items(
items = mixedList,
key = { it.id },
contentType = { it.type } // 帮助 Compose 复用 item
) { item ->
when (item.type) {
Type.HEADER -> HeaderItem(item)
Type.CONTENT -> ContentItem(item)
Type.FOOTER -> FooterItem(item)
}
}
}
3.8.4 合理使用 remember 缓存计算
@Composable
fun ItemCard(item: Item) {
// ✅ 缓存格式化后的价格
val formattedPrice = remember(item.price) {
NumberFormat.getCurrencyInstance().format(item.price)
}
// ✅ 缓存渐变 Brush
val gradient = remember(item.color) {
Brush.linearGradient(
colors = listOf(item.color, item.color.copy(alpha = 0.5f))
)
}
Text(text = formattedPrice)
}
四、ConstraintLayout 深度解析
4.1 ConstraintLayout 的设计哲学
ConstraintLayout 的核心思想是通过约束条件描述布局,而不是通过嵌套层次。
优势:
- 扁平的视图层级(通常只有一层)
- 灵活的相对定位
- 更好的性能(减少测量次数)
适用场景:
- 复杂的表单页面
- 需要相对定位的布局
- 需要动态调整位置的元素
4.2 约束求解原理
ConstraintLayout 的布局过程可以看作是一个约束求解问题:
变量:每个 View 的 x, y, width, height
约束条件:
- View A 的左边与父布局左边对齐
- View B 的左边与 View A 的右边对齐
- View C 在父布局中水平居中
- ...
求解目标:找到满足所有约束的 x, y, width, height
4.3 与嵌套布局的选择策略
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单线性排列 | Column/Row | 代码更简洁,语义更清晰 |
| 需要相对定位 | ConstraintLayout | 减少嵌套层级 |
| 复杂表单 | ConstraintLayout | 标签和输入框对齐更方便 |
| 列表项 | Column/Row | 性能更好,代码更易读 |
| 需要动态位置 | ConstraintLayout | 约束可以动态修改 |
4.4 性能对比
场景:一个包含 10 个子元素的复杂布局
嵌套 Column + Row:
- 测量次数:O(n²) 级别
- 视图层级:3-4 层
ConstraintLayout:
- 测量次数:O(n) 级别
- 视图层级:1 层
建议:
- 简单布局优先使用 Column/Row
- 复杂布局考虑 ConstraintLayout
- 不要过度优化,先保证代码可读性
五、常见问题与解决方案
5.1 Modifier 相关问题
Q: 为什么 Modifier.padding() 和 Modifier.background() 的顺序会影响效果?
A: 因为 Modifier 是从外到内执行的。先应用的 Modifier 在外层,后应用的在内层。
Q: 如何实现点击区域比实际 View 大?
A: 使用 Modifier.clickable 配合 Modifier.padding:
Box(
modifier = Modifier
.clickable { /* 点击处理 */ }
.padding(16.dp) // 点击区域包含 padding
.background(Color.Blue)
) {
Text("Click me")
}
5.2 布局相关问题
Q: 如何实现宽高比固定的组件?
A: 使用 AspectRatio Modifier:
Image(
painter = painterResource(R.drawable.image),
contentDescription = null,
modifier = Modifier.aspectRatio(16f / 9f)
)
Q: 如何让子元素填满剩余空间?
A: 使用 Modifier.weight:
Column {
Text("Header")
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f) // 填满剩余空间
)
Text("Footer")
}
5.3 LazyColumn 相关问题
Q: 列表滑动卡顿怎么办?
A: 检查以下几点:
- 是否使用了 key
- item 中是否避免了创建新对象
- 是否使用了 derivedStateOf 优化频繁变化的状态
- item 的布局是否过于复杂
Q: 如何实现粘性 Header?
A: 使用 stickyHeader:
LazyColumn {
stickyHeader {
Text("Section A", modifier = Modifier.background(Color.Gray))
}
items(itemsA) { Item(it) }
stickyHeader {
Text("Section B", modifier = Modifier.background(Color.Gray))
}
items(itemsB) { Item(it) }
}
六、本篇小结
今天我们深入探讨了 Compose 的核心基石:
Modifier
- 理解了 Modifier 的接口设计和链表结构
- 掌握了链式调用的执行顺序
- 学会了自定义 Modifier 的三种方式
- 了解了 Intrinsic 测量的原理
布局系统
- 理解了 Composition → Layout → Drawing 的三阶段
- 掌握了 Constraints 的使用方法
- 深入了解了 Column、Row、Box 的测量与布局原理
- 学会了自定义 Layout 的设计思路
LazyColumn
- 理解了惰性加载的视口渲染机制
- 掌握了 key 的作用和 diff 算法
- 学会了列表状态管理和分页加载
- 了解了性能优化的最佳实践
ConstraintLayout
- 理解了约束求解的原理
- 掌握了与嵌套布局的选择策略
下篇预告
第三篇:状态管理 将深入讲解:
- 状态与重组机制深度解析(Slot Table 原理)
- remember、mutableStateOf 源码分析
- 状态提升(State Hoisting)设计模式
- ViewModel 与 Compose 的最佳实践
- CompositionLocal 原理与应用
敬请期待!
参考资源
📌 系列文章导航
- 第一篇:初识 Compose ✅
- 第二篇:核心基石(当前)✅
- 第三篇:状态管理
- 第四篇:Material 组件与主题
- 第五篇:动画与交互
- 第六篇:架构与工程化
- 第七篇:高级特性与实战
如果这篇文章对你有帮助,欢迎 点赞、收藏、关注!有任何问题可以在评论区留言。
Generated by Kimi K2.5 Agent