第2章:Compose基本布局组件详解

布局是UI的骨架。Jetpack Compose 虽然叫声明式,但基本的摆列方式,说白了还是Column、Row、Box那老三样。不过它的玩法跟传统 View 系统差别挺大,我刚从 XML 切过来时也踩过几个坑。

2.1 三剑客:Column、Row、Box

这三个组件几乎覆盖了90%的布局场景。我个人习惯是先用Column和Row把大框架搭好,再在局部用Box做叠层。

组件 作用 主轴方向
Column 垂直排列子项 从上到下
Row 水平排列子项 从左到右
Box 层叠堆叠子项(类似FrameLayout) 无方向,默认左上对齐

我的经验:Column和Row的自身尺寸默认是“包裹内容”(wrap content),但如果你给它们设了 fillMaxSize(),它们就会撑满父容器。Box则默认尺寸等于最大子项的大小,这一点和XML里的FrameLayout一致。

Column 基础用法


Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp),
    verticalArrangement = Arrangement.SpaceEvenly,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Text("第一行")
    Text("第二行")
    Button(onClick = { /* 点击 */ }) {
        Text("按钮")
    }
}

verticalArrangement控制垂直方向子项的间距分布,horizontalAlignment控制水平对齐。嗯,这里要注意:Arrangement.SpaceEvenly会把空白均匀分配到子项之间以及首尾,和Flex布局有点像。

Row 基础用法


Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically
) {
    Icon(Icons.Default.Home, contentDescription = "首页")
    Text("标题")
    Icon(Icons.Default.MoreVert, contentDescription = "更多")
}

你想想看,如果要在左侧放一个图标、中间放标题、右侧放更多按钮,用Row加SpaceBetween就搞定了。我在项目中经常用这个模式做TopAppBar。

Box 层叠布局

Box最常见的场景是在图片上显示文字或遮罩。比如一个头像上加个VIP徽章:


Box(modifier = Modifier.size(64.dp)) {
    // 底部是头像
    Image(
        painter = painterResource(id = R.drawable.avatar),
        contentDescription = null,
        modifier = Modifier.fillMaxSize()
    )
    // 顶部是VIP角标
    Text(
        text = "VIP",
        color = Color.White,
        fontSize = 10.sp,
        modifier = Modifier
            .align(Alignment.TopEnd)
            .background(Color.Red, shape = CircleShape)
            .padding(4.dp)
    )
}

我曾经踩过的坑:Box里多个子项时,默认所有子项都从左上角开始堆叠(因为没有设置ContentAlignment)。如果你希望子项居中或靠右,一定要用.align()修饰符或者给Box设置contentAlignment参数,否则子项会全挤在左上角。

2.2 Modifier——布局修饰符才是灵魂

Compose 里没有 android:layout_width 这种属性。所有尺寸、边距、对齐方式都通过 Modifier 链式调用。

我个人觉得这个设计比XML好太多——你可以把Modifier看作一个“装饰器管道”,每次调用返回一个新的Modifier,最终应用到组件上。

常用 Modifier 布局方法一览

Modifier 说明
.fillMaxWidth() 宽度撑满父容器(可传比例,如0.5f)
.fillMaxHeight() 高度撑满父容器
.fillMaxSize() 宽高都撑满
.width() / .height() 固定尺寸
.size() 同时设置宽高
.padding() 内边距(可单独设置各方向)
.offset() 偏移位置(不影响其他子项)
.weight() 在Column/Row内按比例分配空间(类似LinearLayout的weight)

weight 的重要用法

在Column或Row里,如果想让子项按比例分配剩余空间,用.weight()修饰符,而不是给子项设固定宽度。


Row(modifier = Modifier.fillMaxWidth()) {
    Text("左侧", modifier = Modifier.weight(1f))
    Text("右侧", modifier = Modifier.weight(2f))
}

这样左侧占1/3,右侧占2/3。我记得刚开始用的时候,总是忘记把weight加在子项上,结果发现宽度没变化。记住:weight是加在需要按比例分配的子项上,不是加在父Row上。

提示:Modifier的顺序很重要!比如 .padding().clickable() 与 .clickable().padding() 效果不同——前者点击区域包含内边距,后者点击区域不包括内边距。我习惯把尺寸、内边距这类布局修饰符放在前面,交互修饰符(clickable、gesture等)放在后面。

2.3 布局的嵌套与性能

Compose 的布局嵌套不像传统View系统那么“沉重”,但嵌套太多依然影响性能。官方建议尽量扁平化。可以用Layout、SubcomposeLayout自定义容器来减少嵌套。

不过我实际项目中,三层以内的嵌套(Column → Row → Box)完全没问题,不必过度优化。真正要注意的是避免在LazyColumn里嵌套可滚动的子组件——我以前遇到过内部Scrollable和外层LazyColumn互相抢手势的事件,恶心了好一阵。

2.4 知识结构总览

下面这张图帮你梳理Compose基本布局组件的核心脉络:

Compose 布局组件知识体系 Compose 布局组件 线性布局 Column Row 层叠布局 Box 修饰符布局 .weight() .padding() 约束布局 ConstraintLayout 说明 布局组件可相互嵌套使用 实际项目推荐不超过3层 Modifier的顺序影响最终效果

2.5 其他实用布局组件

Spacer——占位空白

不需要用一个Box(modifier = Modifier.height(16.dp))来撑间距,直接用Spacer(modifier = Modifier.height(16.dp))更语义化。我在列表中常用它来做分隔线前后的空白。

Divider——分隔线


Divider(
    modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
    thickness = 1.dp,
    color = Color.LightGray
)

注意Divider默认宽高是0,必须显式设置fillMaxWidth()或宽度。

AspectRatio——固定宽高比

想要图片按16:9显示?用.aspectRatio(16f/9f)修饰符。它会让子项优先按照宽度计算高度,除非父容器限制了高度。我在卡片封面里经常用。


Image(
    painter = painterResource(id = R.drawable.cover),
    contentDescription = null,
    modifier = Modifier
        .fillMaxWidth()
        .aspectRatio(16f / 9f)
        .clip(RoundedCornerShape(8.dp))
)

2.6 布局调试技巧

如果布局不按预期展示,我一般做两件事:

  1. 给组件加上.border(1.dp, Color.Red)看它的实际边界。
  2. 在Column或Row的末尾加一个Spacer(modifier = Modifier.weight(1f)),看是不是其他子项占满了空间。

提示:Compose 布局的布局阶段是不可中断的,所以不要试图在Modifier里做耗时操作。如果你需要根据测量结果动态调整,可以考虑用BoxWithConstraints。

好了,基本布局组件就这些。上手练几次,你就能形成肌肉记忆——代码一写,布局自动就浮现在脑中了。