本课定位: 第 1 课建立了声明式 UI 与状态驱动的心智模型。第 2 课开始进入 Compose 的"空间组织"能力。布局是 Compose 开发中使用频率最高的技能,几乎所有页面都由 Column、Row、Box 组合而成。本课不追求花哨,而是把约束、主轴、交叉轴、Arrangement、Alignment、weight、Spacer、Modifier 顺序这些基础打牢。后面列表、导航、自适应、经典示例解读,都建立在本课之上。
一、本课目标
学完本课,你应该能够:
二、Compose 布局的本质:约束驱动
这一节是第 2 课最值得深入理解的部分。API 可以查文档,但布局模型决定了你能不能解释清楚"为什么我的布局没生效"。
2.1 三阶段:测量、布局、绘制
Compose 的界面渲染大致分为三个阶段:
测量(Measure)→ 布局(Layout)→ 绘制(Draw)
- 测量:父容器给子元素约束,子元素决定自己的尺寸。
- 布局:父容器根据子元素尺寸,决定子元素放在哪里。
- 绘制:把内容画到屏幕上。
第 2 课主要关注前两个阶段。你不需要手动调用测量和布局,但理解"约束"和"放置"能帮你解释很多布局现象。
2.2 约束:父给子的最大最小范围
父容器会给子元素一个约束,例如:
最小宽度 0,最大宽度 1080
最小高度 0,最大高度 1920
子元素根据约束决定自己的尺寸。比如:
- fillMaxWidth():尽量占满最大宽度。
- wrapContentWidth():只占内容需要的宽度。
- size(80.dp):固定 80dp。
一个常见误区: 很多人以为 fillMaxWidth() 是"把宽度设成屏幕宽度",其实它是"把宽度设成父容器允许的最大宽度"。如果父容器本身没有给出足够的最大宽度,fillMaxWidth() 也不会变成屏幕宽度。
2.3 子元素决定自己的尺寸,父容器负责放置
Compose 的布局规则可以概括为:
父容器给约束;
子元素根据约束决定自己的尺寸;
父容器根据子元素尺寸,决定把它们放在哪里。
这个规则解释了几个常见现象:
2.4 一次布局的完整流程(文字版)
假设有:
Row(modifier = Modifier.fillMaxWidth()) {
Text("左")
Spacer(modifier = Modifier.weight(1f))
Text("右")
}
执行流程:
理解这个流程,后面所有布局问题都能自己推理。
2.5 布局不是"绝对坐标",而是"关系描述"
传统 View 中你可能写:
android:layout_marginStart="16dp"
android:layout_toEndOf="@id/avatar"
Compose 更强调"关系":
Row {
Avatar()
Spacer(Modifier.width(12.dp))
Name()
}
你描述的是:头像、间距、名字,从左到右排列。具体坐标由 Compose 计算。
三、三大基础布局容器
3.1 Column:垂直布局
Column 把子元素从上到下排列。
Column {
Text("第一行")
Text("第二行")
Text("第三行")
}
视觉结构:
第一行
第二行
第三行
Column 的主轴是垂直方向,交叉轴是水平方向。
常用参数:
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 子元素
}
- verticalArrangement:控制垂直方向排列,也就是主轴。
- horizontalAlignment:控制水平方向对齐,也就是交叉轴。
3.2 Row:水平布局
Row 把子元素从左到右排列。
Row {
Text("左")
Text("中")
Text("右")
}
视觉结构:
左 中 右
Row 的主轴是水平方向,交叉轴是垂直方向。
常用参数:
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 子元素
}
- horizontalArrangement:控制水平方向排列,也就是主轴。
- verticalAlignment:控制垂直方向对齐,也就是交叉轴。
3.3 Box:层叠布局
Box 把子元素叠放在一起,后面的子元素可以覆盖前面的。
Box {
Text("底层")
Text("上层")
}
视觉结构:
上层
底层
Box 常用参数:
Box(
modifier = Modifier.size(120.dp),
contentAlignment = Alignment.Center
) {
// 子元素
}
- contentAlignment:控制所有子元素在 Box 内的对齐方式。
- 子元素也可以用 Modifier.align(…) 单独对齐。
Box 常用于:
- 头像叠加在线状态小圆点。
- 图片上叠加文字。
- 加载遮罩。
- 自定义绘制容器。
3.4 三者的选择原则
| 从上到下排列 | Column |
| 从左到右排列 | Row |
| 层叠、叠加 | Box |
| 复杂结构 | 三者嵌套组合 |
一句话总结:
Column 管竖,Row 管横,Box 管叠。
四、主轴与交叉轴:布局的核心概念
4.1 一张表看懂
| Column | 垂直 | 水平 | verticalArrangement | horizontalAlignment |
| Row | 水平 | 垂直 | horizontalArrangement | verticalAlignment |
记忆方法:
Column:从上到下排,所以主轴是垂直。
Row:从左到右排,所以主轴是水平。
交叉轴就是与主轴垂直的方向。
4.2 为什么理解主轴和交叉轴很重要?
因为 Arrangement 和 Alignment 的命名,完全由主轴和交叉轴决定:
- Arrangement 管主轴。
- Alignment 管交叉轴。
如果你记不住 Column 该用 horizontalAlignment 还是 verticalAlignment,只要问自己:
Column 的主轴是垂直,交叉轴是水平。
对齐用的是交叉轴参数,所以是 horizontalAlignment。
4.3 一个容易混淆的例子
// 正确:让 Column 的子元素水平居中
Column(
horizontalAlignment = Alignment.CenterHorizontally
) { ... }
// 正确:让 Column 的子元素整体垂直居中
Column(
verticalArrangement = Arrangement.Center
) { ... }
// 错误:Column 没有 verticalAlignment 参数
Column(
verticalAlignment = Alignment.CenterVertically // 编译错误
) { ... }
理解主轴和交叉轴后,这类错误就会自然避免。
五、Arrangement 全解
Arrangement 控制子元素在主轴上的排列方式。
5.1 Column 的 verticalArrangement
Column(
verticalArrangement = Arrangement.Top
) { ... }
常用值:
| Arrangement.Top | 顶部对齐,默认 | 普通页面 |
| Arrangement.Center | 垂直居中 | 空状态、登录页 |
| Arrangement.Bottom | 底部对齐 | 底部按钮 |
| Arrangement.SpaceBetween | 两端对齐,中间平均分布 | 上下分栏 |
| Arrangement.SpaceAround | 每个元素两侧平均留白 | 均匀分布 |
| Arrangement.SpaceEvenly | 所有间距完全相等 | 均匀分布 |
| Arrangement.spacedBy(12.dp) | 元素之间固定间距 | 表单、列表 |
5.2 Row 的 horizontalArrangement
Row(
horizontalArrangement = Arrangement.SpaceBetween
) { ... }
常用值:
| Arrangement.Start | 左对齐,默认 | 普通行 |
| Arrangement.Center | 水平居中 | 按钮组居中 |
| Arrangement.End | 右对齐 | 操作按钮 |
| Arrangement.SpaceBetween | 两端对齐,中间平均分布 | 标题 + 操作 |
| Arrangement.SpaceAround | 每个元素两侧平均留白 | 均匀分布 |
| Arrangement.SpaceEvenly | 所有间距完全相等 | 均匀分布 |
| Arrangement.spacedBy(8.dp) | 元素之间固定间距 | 标签组、图标组 |
5.3 SpaceBetween、SpaceAround、SpaceEvenly 的区别
假设有三个元素,容器宽度 300:
SpaceBetween:两端贴边,中间平均。
[元素1] [元素2] [元素3]
SpaceAround:每个元素两侧平均留白,两端留白是中间的一半。
[元素1] [元素2] [元素3]
SpaceEvenly:所有间距完全相等,包括两端。
[元素1] [元素2] [元素3]
选择建议:
- 两端对齐:SpaceBetween。
- 所有间距一致:SpaceEvenly。
- 想要每个元素周围都有均匀留白:SpaceAround。
- 想要固定间距:spacedBy。
5.4 spacedBy 的深入理解
Arrangement.spacedBy(12.dp)
它的特点是:
这解释了为什么 spacedBy 适合做统一间距:它不会影响容器两端。
六、Alignment 全解
Alignment 控制子元素在交叉轴上的对齐方式。
6.1 Column 的 horizontalAlignment
Column(
horizontalAlignment = Alignment.CenterHorizontally
) { ... }
常用值:
- Alignment.Start
- Alignment.CenterHorizontally
- Alignment.End
6.2 Row 的 verticalAlignment
Row(
verticalAlignment = Alignment.CenterVertically
) { ... }
常用值:
- Alignment.Top
- Alignment.CenterVertically
- Alignment.Bottom
6.3 Box 的 contentAlignment
Box(
contentAlignment = Alignment.Center
) { ... }
常用值:
- Alignment.TopStart
- Alignment.TopCenter
- Alignment.TopEnd
- Alignment.CenterStart
- Alignment.Center
- Alignment.CenterEnd
- Alignment.BottomStart
- Alignment.BottomCenter
- Alignment.BottomEnd
6.4 子元素单独对齐:Modifier.align
在 Box 中,子元素可以用 Modifier.align 单独对齐,覆盖 contentAlignment:
Box(modifier = Modifier.size(96.dp)) {
Box(
modifier = Modifier
.size(96.dp)
.clip(CircleShape)
.background(Color.Blue)
)
Box(
modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(Color.Green)
.align(Alignment.BottomEnd)
)
}
Modifier.align 只在 BoxScope 中可用。类似地:
- Row 中可以用 Modifier.align(Alignment.Top/CenterVertically/Bottom)。
- Column 中可以用 Modifier.align(Alignment.Start/CenterHorizontally/End)。
6.5 Alignment 与 Arrangement 的协作
Arrangement 和 Alignment 是协作关系:
- Arrangement 决定子元素在主轴上的整体分布。
- Alignment 决定子元素在交叉轴上的对齐。
例如:
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) { ... }
效果:子元素在垂直方向整体居中,每个子元素在水平方向水平居中。
七、尺寸、权重与 Spacer
7.1 fillMaxWidth、fillMaxSize、wrapContent
Modifier.fillMaxWidth() // 宽度占满父容器最大约束
Modifier.fillMaxSize() // 宽高都占满
Modifier.wrapContentSize() // 尺寸包裹内容,默认行为
深入理解:
- fillMaxWidth() 不是"屏幕宽度",而是"父容器允许的最大宽度"。
- 在 Column 中,fillMaxWidth() 通常等于屏幕宽度,因为 Column 一般会占满宽度。
- 在嵌套布局中,fillMaxWidth() 只影响当前父容器内的宽度。
7.2 weight:按权重分配剩余空间
weight 只能在 Row 或 Column 的作用域中使用。
Row(modifier = Modifier.fillMaxWidth()) {
Text("左", modifier = Modifier.weight(1f))
Text("右", modifier = Modifier.weight(2f))
}
效果:
左占 1/3,右占 2/3。
weight 的计算方式:
剩余空间 = 容器总尺寸 – 未设置 weight 的子元素尺寸
每个 weight 子元素分到的空间 = 剩余空间 × (自己的 weight / 所有 weight 之和)
常见用法:让中间元素占满剩余空间。
Row {
Icon(...)
Text(
text = "标题",
modifier = Modifier.weight(1f)
)
Icon(...)
}
7.3 weight 的进阶用法
weight 还有两个参数:
Modifier.weight(weight = 1f, fill = true)
- weight:权重比例。
- fill:是否填满分配到的空间。默认 true。
如果设置 fill = false:
Row(modifier = Modifier.fillMaxWidth()) {
Text("左", modifier = Modifier.weight(1f, fill = false))
Text("右", modifier = Modifier.weight(1f, fill = false))
}
子元素不会强制填满分配到的空间,而是按自身内容尺寸显示,但空间位置按权重分配。这个细节在复杂布局中很有用。
7.4 Spacer:占位组件
Spacer 是一个没有内容的占位组件,常配合 weight 使用。
Row(modifier = Modifier.fillMaxWidth()) {
Text("左边")
Spacer(modifier = Modifier.weight(1f))
Text("右边")
}
效果:左边和右边分别贴到两端。
也可以用固定尺寸:
Spacer(modifier = Modifier.width(12.dp))
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.size(16.dp))
7.5 Spacer vs Arrangement.spacedBy vs padding
| padding | 组件自身内容与边界 | 组件内边距 |
| Arrangement.spacedBy | 主轴子元素之间 | 统一间距 |
| Spacer | 独立占位 | 推到两端、占剩余空间 |
选择建议:
- 统一间距:Arrangement.spacedBy。
- 组件内边距:padding。
- 把元素推到两端或占据剩余空间:Spacer + weight。
八、Modifier 顺序对布局的影响
Modifier 是有顺序的,顺序不同,效果不同。这是第 2 课必须掌握的核心细节。
8.1 padding 与 background
Modifier
.background(Color.Red)
.padding(16.dp)
效果:背景覆盖整个区域,包括 padding 部分。
Modifier
.padding(16.dp)
.background(Color.Red)
效果:先留出 16dp 外边距,背景只画在内容区域。
8.2 size、clip、background
Modifier
.size(80.dp)
.clip(CircleShape)
.background(Color.Blue)
效果:先确定 80dp 尺寸,再裁剪成圆形,再画背景,得到圆形头像。
如果顺序错误,例如:
Modifier
.background(Color.Blue)
.size(80.dp)
.clip(CircleShape)
背景可能不是圆形,或者范围不符合预期。
8.3 clickable 与 padding
Modifier
.clickable { }
.padding(16.dp)
点击范围包括 padding 部分。
Modifier
.padding(16.dp)
.clickable { }
点击范围只有内容区域,padding 部分不响应点击。
8.4 理解方式
可以把 Modifier 链理解为"一层一层包裹":
外层 → 内层
先写的修饰符更靠外,后写的更靠内。外层影响整体,内层影响内容。
也可以画成图:
Modifier
.background(…) // 最外层
.padding(…) // 次外层
.size(…) // 中间层
.clickable(…) // 内层
.padding(…) // 最内层
每一层都影响下一层的可用空间和行为。
8.5 常用 Modifier 顺序建议
| 圆形头像 | size -> clip -> background |
| 带边框卡片 | clip -> background -> border |
| 点击范围包含内边距 | clickable -> padding |
| 点击范围仅内容 | padding -> clickable |
| 带阴影卡片 | clip -> background -> shadow |
九、完整示例:个人名片
下面用 Column、Row、Box 搭一个个人名片页面。
package com.example.composelayout
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
ProfileScreen()
}
}
}
}
}
@Composable
fun ProfileScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
ProfileCard()
}
}
@Composable
fun ProfileCard(modifier: Modifier = Modifier) {
var isOnline by remember { mutableStateOf(true) }
val statusColor = if (isOnline) Color(0xFF2E7D32) else Color.Gray
val statusText = if (isOnline) "在线" else "离线"
Card(
modifier = modifier
.fillMaxWidth()
.padding(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 头像:Box 层叠,可用于叠加在线状态
Box(
modifier = Modifier
.size(96.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(
text = "张",
color = Color.White,
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold
)
}
Text(
text = "张三",
style = MaterialTheme.typography.headlineSmall
)
Text(
text = "Android 开发者,热爱 Compose",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// 技能标签:Row 水平排列
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
SkillTag("Kotlin")
SkillTag("Compose")
SkillTag("Android")
}
// 在线状态:Row 水平排列,圆点 + 文字
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(statusColor)
)
Text(
text = statusText,
color = statusColor,
style = MaterialTheme.typography.bodyMedium
)
}
Button(onClick = { isOnline = !isOnline }) {
Text(if (isOnline) "切换为离线" else "切换为在线")
}
}
}
}
@Composable
private fun SkillTag(text: String) {
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.secondaryContainer
) {
Text(
text = text,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium
)
}
}
@Preview(showBackground = true, name = "个人名片预览")
@Composable
fun ProfileCardPreview() {
MaterialTheme {
ProfileScreen()
}
}
9.1 布局结构拆解
ProfileScreen
└── Column(fillMaxSize,垂直居中,水平居中)
└── ProfileCard
└── Card
└── Column(垂直排列,水平居中,元素间距 12dp)
├── Box(头像,圆形)
├── Text(姓名)
├── Text(简介)
├── Row(技能标签)
│ ├── SkillTag
│ ├── SkillTag
│ └── SkillTag
├── Row(在线状态)
│ ├── Box(小圆点)
│ └── Text(在线/离线)
└── Button(切换状态)
9.2 关键布局点
- ProfileScreen 用 Column 把名片整体居中。
- ProfileCard 内部用 Column 垂直排列所有信息。
- horizontalAlignment = Alignment.CenterHorizontally 让所有子元素水平居中。
- verticalArrangement = Arrangement.spacedBy(12.dp) 统一元素间距。
- 技能标签用 Row 水平排列。
- 在线状态用 Row,verticalAlignment = Alignment.CenterVertically 让圆点和文字垂直居中。
- 头像用 Box,contentAlignment = Alignment.Center 让文字在圆形中居中。
- Modifier 顺序:size -> clip -> background 得到圆形背景。
9.3 状态驱动部分
var isOnline by remember { mutableStateOf(true) }
- 状态由 ProfileCard 持有。
- statusColor、statusText 由状态派生。
- 按钮点击切换状态。
- 读取状态的 Box、Text、Button 自动重组。
这是第 1 课状态驱动思想的延续。
十、随堂练
十一、15 道习题(含参考答案与解读)
习题 1:Column 的主轴和交叉轴分别是什么?
参考答案: Column 的主轴是垂直方向,交叉轴是水平方向。
解读: Column 从上到下排列子元素,所以主轴是垂直。主轴排列用 verticalArrangement,交叉轴对齐用 horizontalAlignment。理解主轴和交叉轴,是区分 Arrangement 和 Alignment 的关键。很多初学者会把 Column 的居中和 Row 的居中写反,本质就是没有建立主轴/交叉轴的概念。
习题 2:Row 的主轴和交叉轴分别是什么?
参考答案: Row 的主轴是水平方向,交叉轴是垂直方向。
解读: Row 从左到右排列子元素,所以主轴是水平。主轴排列用 horizontalArrangement,交叉轴对齐用 verticalAlignment。与 Column 正好相反,记忆时可以成对理解:
Column:垂直主轴,水平交叉。
Row:水平主轴,垂直交叉。
习题 3:Box 的主要特点是什么?适合什么场景?
参考答案: Box 是层叠布局,子元素按顺序叠加,后面的可以覆盖前面的。适合头像叠加状态点、图片叠加文字、加载遮罩等场景。
解读: Box 不负责线性排列,而是把所有子元素放在同一个区域内,通过 contentAlignment 或 Modifier.align 控制位置。它是实现"叠加效果"的基础容器。典型场景:
- 头像 + 在线状态点。
- 图片 + 标题文字。
- 加载中 + 遮罩层。
- 徽章 + 角标。
习题 4:Column 中让子元素水平居中,应设置哪个参数?
参考答案:
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
// 子元素
}
解读: Column 的交叉轴是水平方向,所以用 horizontalAlignment 控制水平对齐。Alignment.CenterHorizontally 表示水平居中。注意不要和 verticalArrangement = Arrangement.Center 混淆,后者控制垂直方向的整体居中。如果你想让子元素既水平居中又垂直居中,两个都要设置:
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) { ... }
习题 5:Row 中让子元素垂直居中,应设置哪个参数?
参考答案:
Row(
verticalAlignment = Alignment.CenterVertically
) {
// 子元素
}
解读: Row 的交叉轴是垂直方向,所以用 verticalAlignment 控制垂直对齐。常见场景:图标和文字并排时,让它们垂直居中对齐,视觉更自然。如果不设置,默认是 Alignment.Top,图标和文字顶部对齐,通常会显得别扭。
习题 6:Row 中让第一个元素靠左、最后一个元素靠右,有哪两种常见写法?
参考答案:
写法一:
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("左")
Text("右")
}
写法二:
Row(modifier = Modifier.fillMaxWidth()) {
Text("左")
Spacer(modifier = Modifier.weight(1f))
Text("右")
}
解读: SpaceBetween 适合只有两个元素或希望两端对齐、中间平均分布的场景。Spacer + weight 更灵活,适合中间还需要插入其他元素,或者只想让某一段占据剩余空间的场景。weight 只能在 Row 或 Column 作用域中使用。两种写法都要求 Row 有 fillMaxWidth(),否则没有"两端"可分。
习题 7:Arrangement.spacedBy(12.dp) 的作用是什么?
参考答案: 让主轴方向上的子元素之间统一间隔 12dp。
解读: spacedBy 只影响子元素之间,不影响两端。它比手动插入 Spacer 更简洁,适合统一间距。在 Column 中控制垂直间距,在 Row 中控制水平间距。注意:它不会给第一个元素前面或最后一个元素后面增加间距。如果两端也需要留白,应配合 padding 使用。
习题 8:weight 的作用是什么?写一个例子:Row 中左边固定,右边占满剩余空间。
参考答案:
Row(modifier = Modifier.fillMaxWidth()) {
Text("固定")
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "占满剩余空间",
modifier = Modifier.weight(1f)
)
}
解读: weight 按比例分配剩余空间。weight(1f) 表示占满剩余空间。如果多个子元素都设置 weight,则按权重比例分配。weight 只能在 RowScope 或 ColumnScope 中使用,所以它只能直接写在 Row 或 Column 的子元素上。这也是为什么在普通 Modifier 链中写 weight 会编译报错。
习题 9:Spacer 的作用是什么?和 padding 有什么区别?
参考答案: Spacer 是一个没有内容的占位组件,用来制造间距或配合 weight 占据剩余空间。padding 是给组件自身增加内边距。
解读: Spacer 是独立组件,会参与布局,可以设置 weight。padding 是 Modifier,影响组件内容与边界之间的距离。简单固定间距可以用 padding 或 Arrangement.spacedBy;需要"把两个元素推到两端"或"占据剩余空间"时,用 Spacer + weight 更合适。另外,Spacer 可以单独设置背景色用于调试,padding 不行。
习题 10:fillMaxWidth() 和 wrapContentWidth() 有什么区别?
参考答案: fillMaxWidth() 让组件宽度占满父容器允许的最大宽度;wrapContentWidth() 让组件宽度只包裹内容所需宽度。
解读: fillMaxWidth() 常用于让 Row 占满整行,从而让 SpaceBetween 生效。wrapContentWidth() 是默认行为,适合按钮、标签等只需要包裹内容的组件。理解这两个修饰符,能帮助你控制布局的"伸展"和"收缩"。特别提醒:fillMaxWidth() 占满的是"父容器允许的最大宽度",不是"屏幕宽度"。在嵌套布局中,父容器可能没有给到屏幕宽度。
习题 11:Modifier.background(Color.Red).padding(16.dp) 和 Modifier.padding(16.dp).background(Color.Red) 有什么区别?
参考答案:
- background 在前,padding 在后:背景覆盖整个区域,包括 padding 部分。
- padding 在前,background 在后:先留出 16dp 外边距,背景只画在内容区域。
解读: Modifier 链的顺序决定了修饰符的嵌套顺序。先写的更靠外,后写的更靠内。这个规则在 size、clip、background、border、clickable 等组合中都非常重要。第 3 课会继续深入。
习题 12:如何在 Column 中让一个元素占据剩余空间,把底部按钮推到底部?
参考答案:
Column(modifier = Modifier.fillMaxSize()) {
Text("顶部内容")
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { }) {
Text("底部按钮")
}
}
解读: Spacer(Modifier.weight(1f)) 会占据 Column 中所有剩余垂直空间,从而把后续内容推到底部。这是没有 Scaffold 时实现底部按钮的常见方式。学习第 5 课 Scaffold 后,可以用 bottomBar 更规范地实现。但理解 weight + Spacer 的原理,能让你在没有 Scaffold 的场景下也游刃有余。
习题 13:用 Box 实现头像右下角叠加一个在线状态小圆点,写出关键代码。
参考答案:
Box(modifier = Modifier.size(96.dp)) {
Box(
modifier = Modifier
.size(96.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text("张", color = Color.White)
}
Box(
modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(Color(0xFF2E7D32))
.align(Alignment.BottomEnd)
)
}
解读: 外层 Box 提供层叠区域。第一个子元素是头像,第二个子元素是状态点。Modifier.align(Alignment.BottomEnd) 把状态点放到右下角。Box 的层叠能力非常适合这种"头像 + 角标"的场景。注意:Modifier.align 只在 BoxScope 中可用,所以在 Box 的子元素上才能写。
习题 14:用嵌套布局描述"头像 + 姓名 + 简介 + 技能标签"的结构。
参考答案:
Column(垂直排列,水平居中)
├── Box(头像)
├── Text(姓名)
├── Text(简介)
└── Row(技能标签)
├── SkillTag
├── SkillTag
└── SkillTag
解读: 这是典型的"垂直主结构 + 局部水平子结构"。整体信息用 Column 从上到下排列;技能标签需要横向排列,所以内部嵌套 Row。Compose 布局就是不断嵌套 Column、Row、Box,把复杂页面拆成简单结构。建议在写代码之前,先用这种文字树或图把结构画出来,再动手写 Compose。
习题 15:综合题:写出个人名片中"在线状态行"的关键代码。
参考答案:
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(if (isOnline) Color(0xFF2E7D32) else Color.Gray)
)
Text(
text = if (isOnline) "在线" else "离线",
color = if (isOnline) Color(0xFF2E7D32) else Color.Gray
)
}
解读: 这个小题综合了 Row、Box、Modifier、状态驱动和颜色变化。Row 负责水平排列,verticalAlignment = Alignment.CenterVertically 让圆点和文字垂直居中,spacedBy(8.dp) 统一间距。颜色和文字都由 isOnline 决定,体现声明式 UI 思想。如果进一步优化,可以把 statusColor 和 statusText 提前算好:
val statusColor = if (isOnline) Color(0xFF2E7D32) else Color.Gray
val statusText = if (isOnline) "在线" else "离线"
这样代码更清晰,也避免重复计算。
十二、作业(含参考答案与解读)
作业 1(必做):完成"个人名片"页面
题目要求:
- 使用 Column、Row、Box 完成布局。
- 显示头像、姓名、简介、技能标签、在线状态。
- 一个按钮切换在线/离线。
- 至少一个 @Preview。
参考答案:
参考第九节完整示例。核心结构:
Card {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(头像)
Text(姓名)
Text(简介)
Row(技能标签)
Row(在线状态)
Button(切换状态)
}
}
解读:
- 整体用 Column 垂直排列。
- 头像用 Box 实现圆形背景和文字居中。
- 技能标签用 Row 水平排列。
- 在线状态用 Row 垂直居中,圆点用 Box。
- 状态由 isOnline 驱动,按钮切换状态后自动重组。
- 注意 Modifier 顺序:size -> clip -> background 才能得到圆形。
作业 2(选做):把名片拆成多个 Composable
题目要求:
- 拆出 Avatar、ProfileInfo、SkillTags、StatusRow。
- 每个组件都接收 Modifier 参数。
参考答案:
@Composable
fun Avatar(name: String, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(96.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(
text = name.take(1),
color = Color.White,
style = MaterialTheme.typography.headlineLarge
)
}
}
@Composable
fun ProfileInfo(name: String, bio: String, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(name, style = MaterialTheme.typography.headlineSmall)
Text(bio, style = MaterialTheme.typography.bodyMedium)
}
}
@Composable
fun SkillTags(tags: List<String>, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
tags.forEach { SkillTag(it) }
}
}
@Composable
fun StatusRow(isOnline: Boolean, modifier: Modifier = Modifier) {
val color = if (isOnline) Color(0xFF2E7D32) else Color.Gray
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(color)
)
Text(if (isOnline) "在线" else "离线", color = color)
}
}
解读:
- 拆组件时,每个组件都接收 modifier: Modifier = Modifier,方便调用方控制外部间距和尺寸。
- 数据通过参数传入,组件本身不持有状态,便于复用和预览。
- 这是从"能写页面"到"能写可维护页面"的重要一步。
- 注意 SkillTags 接收 List<String>,比写死三个标签更灵活。
作业 3(提交):代码文件或 Git 仓库链接
题目要求:
- 提交代码文件或 Git 仓库链接。
参考答案:
项目结构建议:
ComposeLayout/
├── app/
│ └── src/main/java/com/example/composelayout/
│ └── MainActivity.kt
└── README.md
解读:
- 提交前确保项目能编译、能运行、有 @Preview。
- 不要提交 build/、.gradle/ 等生成目录。
- README.md 写清楚功能、运行环境、截图。
作业 4(提交):运行截图
题目要求:
- 提交运行截图。
参考答案:
建议至少提交:
解读:
- 截图要能证明布局真实运行成功。
- 在线和离线两种状态都截,体现状态驱动 UI。
- 如果做了选做题,可以把拆组件前后的 Preview 也截出来。
作业 5(提交):200 字学习总结
题目要求:
- 写 200 字学习总结:Column、Row、Box 的区别,主轴和交叉轴,Arrangement 和 Alignment。
参考答案:
本课让我掌握了 Compose 的三大基础布局:Column 垂直排列,Row 水平排列,Box 层叠排列。我理解了主轴和交叉轴:Column 的主轴是垂直,交叉轴是水平;Row 的主轴是水平,交叉轴是垂直。Arrangement 控制主轴排列,Alignment 控制交叉轴对齐。我还学会了用 spacedBy 统一间距,用 weight 分配剩余空间,用 Spacer 把元素推到两端。Modifier 的顺序也很重要,比如 size、clip、background 的顺序会影响圆形头像的效果。通过个人名片实战,我能用嵌套布局把复杂页面拆成 Column、Row、Box 的组合。后续我会继续学习 Modifier、状态提升和列表。
解读:
- 总结中应出现关键词:Column、Row、Box、主轴、交叉轴、Arrangement、Alignment、weight、Spacer、Modifier 顺序。
- 结合个人名片实战,说明嵌套布局的组织方式。
- 如果能写出自己踩过的坑,例如"Row 中误用 horizontalAlignment",会更有深度。
十三、常见坑与调试建议
Row 中误用 horizontalAlignment
Row 的交叉轴是垂直方向,应该用 verticalAlignment。
Column 中误用 verticalAlignment
Column 的交叉轴是水平方向,应该用 horizontalAlignment。
SpaceBetween 不生效
检查 Row 或 Column 是否设置了 fillMaxWidth() 或 fillMaxSize()。没有占满空间时,没有"两端"可分。
weight 报错
weight 只能在 RowScope 或 ColumnScope 中使用,不能随便写在普通 Modifier 链中。
Spacer 没有效果
检查是否设置了 size、width、height 或 weight。没有尺寸的 Spacer 不占空间。
Box 子元素没有对齐
检查 contentAlignment 或子元素的 Modifier.align。
Modifier 顺序错误
padding、background、size、clip 的顺序会影响效果。先写的更靠外,后写的更靠内。
嵌套过深
布局嵌套过深会影响可读性和性能。适当拆分成独立 Composable。
用固定尺寸写死布局
尽量使用 fillMaxWidth、weight、wrapContent 等自适应方式,避免写死宽高。
忘了预览
每写一个组件,尽量加一个 @Preview,快速查看布局效果。
fillMaxWidth 不等于屏幕宽度
它等于父容器允许的最大宽度。嵌套布局中要结合父容器一起看。
weight 和 fillMaxWidth 冲突
在 Row 中给子元素同时设置 fillMaxWidth() 和 weight,效果可能不符合预期。通常应只保留 weight。
十四、本节课知识点总结
14.1 布局模型
14.2 三大布局容器
14.3 主轴与交叉轴
14.4 常用排列与对齐
14.5 尺寸与空间分配
14.6 Modifier 顺序
14.7 布局思维
十五、下节预告
第 3 课:Modifier 与样式——从能看看到好看。
你将掌握:
- Modifier 链式调用与顺序语义。
- padding、size、width、height、fillMaxWidth。
- background、border、clip、shadow。
- clickable、combinedClickable。
- Card、Surface 的样式定制。
- 用 Modifier 做出真实卡片、按钮、标签和头像效果。
- 实战:卡片样式组件库。
附:系列 20 课规划(已更新最后两课)
| 1 | 开课与第一个 Compose | 声明式 UI、环境搭建、@Composable、@Preview | 计数器 |
| 2 | 布局系统 | Column、Row、Box、Spacer、Arrangement、Alignment | 个人名片 |
| 3 | Modifier 与样式 | padding、size、background、border、clip、clickable | 卡片样式 |
| 4 | 状态与重组 | remember、mutableStateOf、State Hoisting | 购物车数量 |
| 5 | 列表与脚手架 | LazyColumn、LazyRow、Scaffold、TopAppBar | 待办列表 |
| 6 | 输入与表单 | TextField、Checkbox、Switch、RadioButton | 登录表单 |
| 7 | 主题与设计系统 | Material 3、ColorScheme、Typography、Shape、暗黑模式 | 一键换肤 |
| 8 | 导航 | Navigation Compose、路由、参数、底部导航 | 三页导航 |
| 9 | 架构与状态管理 | ViewModel、UiState、单向数据流、Repository | 架构版计数器 |
| 10 | 副作用 | LaunchedEffect、DisposableEffect、rememberCoroutineScope | 模拟加载数据 |
| 11 | 动画 | animate*AsState、AnimatedVisibility、Crossfade、Transition | 展开卡片 |
| 12 | 手势与交互 | clickable、pointerInput、draggable、swipe | 滑动删除 |
| 13 | 自定义布局与绘制 | Layout、Canvas、DrawScope、SubcomposeLayout | 环形进度条 |
| 14 | 性能优化 | 重组范围、稳定性、derivedStateOf、key | 优化长列表 |
| 15 | 测试 | Compose UI Test、语义、单元测试 | 测试计数器 |
| 16 | 互操作 | AndroidView、ComposeView、View/Fragment 混合 | 嵌入旧 View |
| 17 | 工程化 | 多模块、Hilt/Koin、资源、预览、调试工具 | 模块拆分 |
| 18 | 响应式与自适应 | WindowSizeClass、平板、折叠屏、多窗口 | 自适应列表 |
| 19 | 经典示例深度解读一 | 选取经典 Compose 示例,深度解读布局、状态、列表、主题、动画、导航等核心实现 | 拆解并重写关键页面 |
| 20 | 经典示例深度解读二 | 选取经典架构示例,深度解读 ViewModel、数据流、网络、分页、测试、性能优化 | 复刻并扩展一个经典示例 |
网硕互联帮助中心




评论前必须登录!
注册