Jetpack Compose 从入门到精通(二):核心基石 - 深入理解 Modifier 与布局系统

186 阅读18分钟

本篇文章将从源码层面深入解析 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 设计自定义布局的步骤

  1. 明确需求:要实现什么样的布局效果?
  2. 确定约束:子元素的尺寸如何确定?
  3. 设计测量策略:如何测量子元素?
  4. 设计布局策略:子元素如何排列?

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 使用场景

  1. 根据测量结果决定子元素内容
  2. 实现复杂的自适应布局
  3. 动态加载内容

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: 检查以下几点:

  1. 是否使用了 key
  2. item 中是否避免了创建新对象
  3. 是否使用了 derivedStateOf 优化频繁变化的状态
  4. 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