第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基本布局组件的核心脉络:
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 布局调试技巧
如果布局不按预期展示,我一般做两件事:
- 给组件加上
.border(1.dp, Color.Red)看它的实际边界。 - 在Column或Row的末尾加一个
Spacer(modifier = Modifier.weight(1f)),看是不是其他子项占满了空间。
提示:Compose 布局的布局阶段是不可中断的,所以不要试图在Modifier里做耗时操作。如果你需要根据测量结果动态调整,可以考虑用BoxWithConstraints。
好了,基本布局组件就这些。上手练几次,你就能形成肌肉记忆——代码一写,布局自动就浮现在脑中了。