第四篇:布局系统——从 Row、Column 到 Box 的声明式布局思维

0 阅读4分钟

4.1 声明式布局思维转变

传统的 XML 布局是嵌套约束型——你用 LinearLayoutConstraintLayout 来约束子 View 的位置,布局参数和 View 的层级在 XML 中静态定义。

Compose 的布局是函数组合型——你用 Kotlin 函数嵌套来描述 UI 结构,布局参数通过函数参数和 Modifier 动态控制。

// XML 方式(命令式层次结构)
<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView android:text="Hello"/>
    <Button android:text="Click"/>
</LinearLayout>

// Compose 方式(函数式组合)
Column(
    verticalArrangement = Arrangement.Top,
    modifier = Modifier.fillMaxWidth()
) {
    Text("Hello")
    Button(onClick = {}) { Text("Click") }
}

区别总结:

维度XMLCompose
布局容器LinearLayout、RelativeLayout 等Row、Column、Box
参数传递XML attribute函数参数 + Modifier
动态控制运行时通过代码修改声明时直接使用 Kotlin 表达式
条件显示visibility = View.GONE直接 if 表达式控制是否组合

4.2 三大基本布局组件

Row —— 水平排列

@Composable
fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
)
Row(
    modifier = Modifier.fillMaxWidth().padding(16.dp),
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically
) {
    Icon(...)
    Text("Title")
    Switch(...)
}

Arrangement 详解

Arrangement效果
Start / End从主轴起点/终点排列
Center居中
SpaceBetween首尾靠边,剩余均匀分配中间空隙
SpaceEvenly均匀分配空间,包括首尾
SpaceAround每个元素左右均匀分配,首尾空间为一半

RowScope 中的特殊 Modifier

Row {
    Text("1", Modifier.weight(1f))  // 占据 1/3 空间
    Text("2", Modifier.weight(2f))  // 占据 2/3 空间
    Text("3")                       // 自适应
}

RowScope.weight 只在 Row 的 content lambda 中可用,这是 Scope Receiver 模式的典型应用——限制特定 API 的作用域。

Column —— 垂直排列

@Composable
fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
)
Column(
    modifier = Modifier.fillMaxSize().padding(24.dp),
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Text("Title", style = MaterialTheme.typography.headlineLarge)
    Spacer(Modifier.height(16.dp))
    Text("Subtitle")
    Spacer(Modifier.height(32.dp))
    Button(onClick = {}) { Text("Get Started") }
}

Box —— 层叠布局(类似 FrameLayout)

@Composable
fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    content: @Composable BoxScope.() -> Unit
)
Box(
    modifier = Modifier.size(200.dp),
    contentAlignment = Alignment.BottomEnd
) {
    // 背景层
    AsyncImage(
        model = imageUrl,
        contentDescription = null,
        modifier = Modifier.fillMaxSize()
    )
    // 覆盖层
    Surface(
        color = Color.Black.copy(alpha = 0.5f),
        modifier = Modifier.padding(4.dp)
    ) {
        Text("Copyright", color = Color.White)
    }
}

Box 的典型场景:

  • 占位/占位图:在图片上叠加文字或按钮
  • 徽标 Badge:在头像上叠红色的未读数字
  • 全屏加载:半透明遮罩 + 转圈

4.3 Scope Receiver 模式

这是一种在 Compose 中广泛使用的设计模式。通过限制 content 参数的类型,让子 Composable 只能访问特定作用域内的 API。

// RowScope、ColumnScope、BoxScope
@LayoutScopeMarker
interface RowScope {
    fun Modifier.weight(weight: Float): Modifier
}

@LayoutScopeMarker
interface ColumnScope {
    fun Modifier.weight(weight: Float): Modifier
    fun Modifier.align(alignment: Alignment.Horizontal): Modifier
}

作用:

Column {
    Text("Top", Modifier.align(Alignment.CenterHorizontally))  // ✅ ColumnScope 允许
    Row {
        Text("RowItem", Modifier.align(Alignment.CenterHorizontally))  // ❌ RowScope 没有 align
    }
}

这种设计在编译期就避免了无效的 API 调用,比传统 View 系统在运行时忽略无效 layout_gravity 更安全。

4.4 Modifier 布局顺序的重要性

// 不同的 Modifier 顺序产生截然不同的效果
Box {
    // 效果:先 fillMaxSize,再 padding 16dp
    Text("Hello", Modifier.fillMaxSize().padding(16.dp))
    // 效果:先 padding 16dp,再 fillMaxSize
    Text("World", Modifier.padding(16.dp).fillMaxSize())
}

核心规律

  • Modifier.fillMaxSize() 是测量约束 → .padding() 是缩小约束区域
  • fillMaxSize()padding():先撑满父容器,再在内边距区域内绘制
  • padding()fillMaxSize():先加内边距,再撑满父容器,内边距被"撑"掉了

我们将在第五篇文章中深度解析 Modifier 系统。

4.5 ConstraintLayout 在 Compose 中

当布局层级较深或多约束依赖时,使用 Compose 的 ConstraintLayout:

implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0")
import androidx.constraintlayout.compose.*

@Composable
fun ConstraintExample() {
    ConstraintLayout(
        modifier = Modifier.fillMaxSize()
    ) {
        // 1. 创建引用
        val (avatar, name, badge) = createRefs()

        // 2. 约束条件
        AsyncImage(
            model = url,
            contentDescription = "Avatar",
            modifier = Modifier.constrainAs(avatar) {
                top.linkTo(parent.top, margin = 16.dp)
                start.linkTo(parent.start, margin = 16.dp)
                width = Dimension.value(48.dp)
                height = Dimension.value(48.dp)
            }
        )
        Text(
            text = "John Doe",
            modifier = Modifier.constrainAs(name) {
                top.linkTo(avatar.top)
                start.linkTo(avatar.end, margin = 12.dp)
                end.linkTo(parent.end, margin = 16.dp)
                width = Dimension.fillToConstraints
            }
        )
    }
}

何时使用 ConstraintLayout

  • 布局扁平化(避免多层嵌套 Row + Column)
  • 复杂的相对定位(A 在 B 的右下角,C 在 A 的下方等)
  • 需要在 Compose 中移植已有的 XML ConstraintLayout 布局

何时避免

  • 简单的线性布局用 Row / Column 更清晰
  • 层叠布局用 Box 更简单

4.6 IntrinsicSize 模式

在某些场景下,你需要基于子组件的"内在尺寸"来确定父容器尺寸:

// 让 Row 的高度由最高的子元素决定
Row(
    modifier = Modifier.height(IntrinsicSize.Min)  // 或 IntrinsicSize.Max
) {
    Text("Tall\nText", Modifier.height(80.dp))
    Text("Normal")
    Button(onClick = {}) { Text("Go") }
}

IntrinsicSize.Min 取所有子组件内在最小尺寸的最大值。
IntrinsicSize.Max 取所有子组件内在最大尺寸的最大值。

这在"所有子组件等高"或"父容器适应子内容"时非常有用,避免硬编码高度。

4.7 本章小结

内容要点
Row水平排列,Arrangement 控制间距,weight 按比例分配空间
Column垂直排列,同理 Arrangement + weight
Box层叠布局,适合覆盖层、Badge、加载遮罩
Scope ReceiverRowScope/ColumnScope/BoxScope 限定特定 API 的调用范围
布局顺序fillMaxSize()padding()padding()fillMaxSize() 效果不同
布局选择简单线性用 Row/Column,层叠用 Box,复杂约束用 ConstraintLayout

下一篇:Modifier 深度解析——链式调用的艺术。