布局基础(上):Column、Row、Box布局组件、Modifier修饰符详解、尺寸与约束、对齐方式

说实话,Jetpack Compose 的布局系统,是我从传统 View 体系转过来时最不适应、但后来觉得最爽的部分。传统 Android 里,你写个 LinearLayout 要配一堆 weight、gravity、orientation,嵌套多了性能还容易崩。Compose 呢?三个核心组件——Column、Row、Box——搞定绝大多数场景。今天我们就把这几个家伙掰开揉碎讲清楚。

一、三大布局组件:Column、Row、Box

这三个组件,说白了就是 Compose 里的「线性布局」和「帧布局」。但它们的用法比传统 View 灵活得多。

1. Column:垂直排列

Column 把子组件从上往下排。我刚开始用的时候,总觉得它就是个垂直的 LinearLayout,后来发现它配合 Modifier 能玩出很多花样。

@Composable
fun ColumnExample() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0xFFF0F4FF)),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.SpaceEvenly
    ) {
        Text("第一行", fontSize = 20.sp)
        Text("第二行", fontSize = 20.sp)
        Text("第三行", fontSize = 20.sp)
    }
}

这里有个坑:verticalArrangement 控制的是垂直方向上的间距分布,horizontalAlignment 控制的是水平方向上的对齐。我见过不少新手把这两个搞反,结果布局乱成一团。

我的习惯:如果 Column 的高度是固定的(比如 fillMaxSize),用 Arrangement.SpaceEvenly 或 SpaceBetween 效果很好。如果高度由内容决定,用 Arrangement.Top 或 Center 更稳妥。

2. Row:水平排列

Row 和 Column 是对称的——一个横着排,一个竖着排。参数也几乎一样,只是方向不同。

@Composable
fun RowExample() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(60.dp)
            .background(Color(0xFFFFF3E0)),
        horizontalArrangement = Arrangement.SpaceAround,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text("左", fontSize = 18.sp)
        Text("中", fontSize = 18.sp)
        Text("右", fontSize = 18.sp)
    }
}

嗯,这里要注意:Row 的 verticalAlignment 默认是 Alignment.Top,如果你不指定,子组件会贴顶部对齐。我早期做搜索栏时吃过这个亏——图标和文字死活对不齐,后来才发现是默认对齐方式的问题。

3. Box:层叠布局

Box 是 Compose 里的「万能容器」。它允许子组件在 Z 轴上层叠,类似传统 View 的 FrameLayout。但比 FrameLayout 强大得多——你可以精确控制每个子组件的位置。

@Composable
fun BoxExample() {
    Box(
        modifier = Modifier
            .size(200.dp)
            .background(Color(0xFFE8F5E9)),
        contentAlignment = Alignment.Center
    ) {
        // 背景层
        Box(
            modifier = Modifier
                .size(180.dp)
                .background(Color(0xFFC8E6C9))
        )
        // 文字层
        Text(
            text = "Hello",
            fontSize = 24.sp,
            color = Color(0xFF2E7D32)
        )
        // 角标层
        Box(
            modifier = Modifier
                .align(Alignment.TopEnd)
                .size(30.dp)
                .background(Color.Red, shape = CircleShape)
        )
    }
}

你看,Box 的 contentAlignment 控制所有子组件的默认对齐,而每个子组件又可以通过 Modifier.align() 单独指定位置。这种「全局默认 + 局部覆盖」的设计,我个人觉得非常优雅。

我曾经踩过的坑:Box 里放多个子组件时,如果子组件都用了 fillMaxSize,后面的会完全覆盖前面的。记得给背景层设置合适的透明度或尺寸。

二、Modifier 修饰符详解

Modifier 是 Compose 布局的灵魂。没有 Modifier,你连给组件加个背景色都做不到。它本质上是一个「装饰器模式」的实现——每个 Modifier 函数返回一个新的 Modifier,链式调用形成一条处理链。

1. 常用 Modifier 分类

类别 常用函数 说明
尺寸 size(), width(), height(), fillMaxSize(), fillMaxWidth(), fillMaxHeight() 控制组件的宽高
内边距 padding(), paddingFrom() 设置组件内部与边缘的距离
外边距 Compose 没有 margin,用 Spacer 或 offset() 替代 控制组件之间的间距
背景与边框 background(), border() 设置颜色、形状、边框
对齐 align(), alignBy() 在父布局中对齐
权重 weight() 在 Row/Column 中按比例分配空间
点击 clickable() 添加点击事件

2. Modifier 的链式调用顺序

这个点特别重要——Modifier 的顺序会影响最终效果。为什么?因为每个 Modifier 函数会创建一个新的布局节点,顺序决定了它们应用的先后。

// 顺序不同,效果不同
Text(
    text = "Hello",
    modifier = Modifier
        .padding(16.dp)        // 先加内边距
        .background(Color.Red) // 再涂背景
)

Text(
    text = "Hello",
    modifier = Modifier
        .background(Color.Red) // 先涂背景
        .padding(16.dp)        // 再加内边距
)

第一个例子:先 padding 再 background,背景会覆盖 padding 区域,文字周围有红色背景。第二个例子:先 background 再 padding,背景只涂在文字区域,padding 区域是透明的。你看,同样的代码,顺序一变,视觉效果完全不同。

我的经验:一般先写尺寸相关的 Modifier(如 fillMaxSize),再写背景和边框,最后写 padding 和点击事件。这个顺序最符合直觉,也最容易调试。

三、尺寸与约束

Compose 的尺寸系统,核心是「约束」这个概念。每个组件都会收到一个「约束范围」——最小宽度、最大宽度、最小高度、最大高度。组件在这个范围内决定自己的实际尺寸。

1. 尺寸修饰符

@Composable
fun SizeModifiers() {
    Column {
        // 固定尺寸
        Box(modifier = Modifier.size(100.dp).background(Color.Blue))
        
        // 按比例填充
        Box(modifier = Modifier.fillMaxWidth(0.5f).height(50.dp).background(Color.Green))
        
        // 最小/最大尺寸
        Box(
            modifier = Modifier
                .widthIn(min = 50.dp, max = 200.dp)
                .heightIn(min = 30.dp)
                .background(Color.Yellow)
        )
    }
}

widthIn 和 heightIn 这两个函数,我建议你多留意。它们不会强制组件变成某个尺寸,而是告诉组件「你可以在 xx 到 xx 之间自由选择」。这在做自适应布局时特别有用。

2. 约束的传递机制

父组件会给子组件传递约束,子组件在约束范围内计算自己的尺寸,然后父组件根据子组件的尺寸决定自己的布局。这个过程是单向的——从父到子。

举个例子:

Row(
    modifier = Modifier.fillMaxWidth()
) {
    Text("短文本", modifier = Modifier.weight(1f))
    Text("这是一段很长的文本,可能会换行", modifier = Modifier.weight(2f))
}

这里 Row 的宽度是屏幕宽度,它把约束传递给两个 Text。因为用了 weight,Row 会把可用空间按 1:2 分配给两个 Text。第一个 Text 拿到 1/3 的宽度,第二个拿到 2/3。

注意:weight 只在 Row 或 Column 中有效。如果你在 Box 里用 weight,编译器不会报错,但运行时会忽略它。我刚开始学的时候在这个问题上浪费了半小时。

四、对齐方式

对齐方式分两种:一种是父布局层面的对齐(通过 Arrangement 和 Alignment 参数),另一种是子组件层面的对齐(通过 Modifier.align())。

1. 父布局对齐参数

布局 主轴对齐 交叉轴对齐
Column verticalArrangement horizontalAlignment
Row horizontalArrangement verticalAlignment
Box contentAlignment(同时控制两个轴)

Arrangement 的常用值:Start、End、Center、SpaceBetween、SpaceAround、SpaceEvenly。Alignment 的常用值:Top、Bottom、CenterVertically、CenterHorizontally。

2. 子组件单独对齐

有时候你希望某个子组件「特立独行」,不跟其他组件走同一个对齐方式。这时候用 Modifier.align()。

Row(
    modifier = Modifier.fillMaxWidth().height(100.dp),
    verticalAlignment = Alignment.CenterVertically
) {
    Text("居中", modifier = Modifier.align(Alignment.CenterVertically))
    Text("顶部", modifier = Modifier.align(Alignment.Top))
    Text("底部", modifier = Modifier.align(Alignment.Bottom))
}

你看,Row 默认让所有子组件垂直居中,但第二个和第三个 Text 通过 align() 覆盖了默认行为。这种灵活性,传统 View 体系里得写不少代码才能实现。

五、知识体系总览

下面这张图,是我自己梳理的布局知识结构。你把它存下来,以后写布局时对照着看,思路会清晰很多。

Compose 布局知识体系 三大布局组件 Column · Row · Box Modifier 修饰符 尺寸 · 背景 · 内边距 · 对齐 尺寸与约束 固定尺寸 · 约束范围 · weight 对齐方式 Arrangement · Alignment · align() 核心原则:父布局传递约束 → 子组件计算尺寸 → 父布局决定位置 所有布局最终都围绕「约束 - 测量 - 放置」三个步骤展开

六、实战小技巧

最后分享几个我在项目中积累的经验:

  • 能用 Box 就别嵌套:很多新手喜欢 Column 套 Row 再套 Column,其实一个 Box 加几个 align() 就能搞定。嵌套越少,性能越好,代码也越清晰。
  • Modifier 顺序记不住?记住「尺寸 → 背景 → 内边距 → 点击」这个口诀。虽然不绝对,但覆盖了 90% 的场景。
  • 调试布局用 border:我经常给组件加一个 .border(1.dp, Color.Red),这样能直观看到每个组件的边界。调试完再删掉,比用 Layout Inspector 快多了。
  • weight 和 fillMaxWidth 别混用:如果你在 Row 里给一个子组件同时用了 weight 和 fillMaxWidth,weight 会覆盖 fillMaxWidth。因为 weight 会重新分配约束。
一个小技巧:当你觉得布局怎么调都不对时,试试给父布局加个背景色。这样你能清楚看到父布局的实际尺寸,很多问题一眼就能发现。

好了,布局基础的上半部分就到这里。Column、Row、Box 这三个组件,配合 Modifier 的灵活运用,已经能解决日常开发中 80% 的布局需求。剩下的 20%,我们会在下一节「布局基础(下)」里继续聊——包括 Scaffold、LazyColumn、ConstraintLayout 这些更高级的布局工具。


公众号:蓝海资料掘金营,微信deep3321