布局基础(上):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,用 Spaceroffset() 替代 控制组件之间的间距
背景与边框 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)
        )
    }
}

widthInheightIn 这两个函数,我建议你多留意。它们不会强制组件变成某个尺寸,而是告诉组件「你可以在 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 的常用值:StartEndCenterSpaceBetweenSpaceAroundSpaceEvenly。Alignment 的常用值:TopBottomCenterVerticallyCenterHorizontally

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