云计算百科
云计算领域专业知识百科平台

【Jetpack Compose声明式UI界面开发实用技术学与练】第 2 课:布局系统——用 Column、Row、Box 搭出个人名片

本课定位: 第 1 课建立了声明式 UI 与状态驱动的心智模型。第 2 课开始进入 Compose 的"空间组织"能力。布局是 Compose 开发中使用频率最高的技能,几乎所有页面都由 Column、Row、Box 组合而成。本课不追求花哨,而是把约束、主轴、交叉轴、Arrangement、Alignment、weight、Spacer、Modifier 顺序这些基础打牢。后面列表、导航、自适应、经典示例解读,都建立在本课之上。


一、本课目标

学完本课,你应该能够:

  • 理解 Compose 布局的三阶段:约束、测量、放置。
  • 熟练使用 Column 实现垂直布局。
  • 熟练使用 Row 实现水平布局。
  • 熟练使用 Box 实现层叠布局。
  • 理解主轴与交叉轴,掌握 Arrangement 与 Alignment。
  • 使用 Spacer、weight、fillMaxWidth 控制空间分配。
  • 理解 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 的布局规则可以概括为:

    父容器给约束;
    子元素根据约束决定自己的尺寸;
    父容器根据子元素尺寸,决定把它们放在哪里。

    这个规则解释了几个常见现象:

  • 为什么 SpaceBetween 不生效?因为 Row 没有 fillMaxWidth(),宽度只包裹内容,没有"两端"可分。
  • 为什么 weight 能分配剩余空间?因为父容器知道自己的总尺寸,也知道其他子元素的尺寸。
  • 为什么 Spacer 没有效果?因为 Spacer 没有设置尺寸或 weight,测量出来是 0。
  • 2.4 一次布局的完整流程(文字版)

    假设有:

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

    执行流程:

  • Row 收到父容器的约束,比如最大宽度 1080。
  • Row 因为有 fillMaxWidth(),决定自己宽度用 1080。
  • Row 测量 Text("左"),得到它的宽度。
  • Row 测量 Text("右"),得到它的宽度。
  • Row 计算剩余空间,分给 Spacer(weight(1f))。
  • Row 决定每个子元素的放置位置:左在起始,Spacer 在中间,右在末尾。
  • 理解这个流程,后面所有布局问题都能自己推理。

    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)

    它的特点是:

  • 只在子元素之间加间距。
  • 第一个元素前面没有间距。
  • 最后一个元素后面没有间距。
  • 如果子元素有 weight,spacedBy 依然生效。
  • 这解释了为什么 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 课状态驱动思想的延续。


    十、随堂练

  • 把个人名片中的头像换成"姓名首字母 + 圆形背景"。
  • 给技能标签增加一行,显示 5 个标签,并让它们自动换行。思考:只用 Row 能换行吗?为什么?
  • 把"在线状态"小圆点放到头像右下角,用 Box 层叠实现。
  • 让名片底部增加一个"关注"按钮和一个"发消息"按钮,两个按钮平均分布。
  • 把 ProfileCard 拆成 Avatar、ProfileInfo、SkillTags、StatusRow 四个 Composable。

  • 十一、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 布局模型

  • Compose 布局三阶段:测量、布局、绘制。
  • 父容器给约束,子元素决定尺寸,父容器决定位置。
  • fillMaxWidth() 占满的是父容器允许的最大宽度,不是屏幕宽度。
  • 理解布局流程,能解释"为什么布局没生效"。
  • 14.2 三大布局容器

  • Column:垂直排列。
  • Row:水平排列。
  • Box:层叠排列。
  • 复杂页面 = Column、Row、Box 嵌套组合。
  • 14.3 主轴与交叉轴

  • Column 主轴垂直,交叉轴水平。
  • Row 主轴水平,交叉轴垂直。
  • Arrangement 管主轴,Alignment 管交叉轴。
  • Box 用 contentAlignment 控制子元素对齐。
  • 14.4 常用排列与对齐

  • Arrangement.spacedBy:固定间距。
  • Arrangement.SpaceBetween:两端对齐。
  • Arrangement.SpaceAround:每个元素两侧平均留白。
  • Arrangement.SpaceEvenly:所有间距完全相等。
  • Arrangement.Center:主轴居中。
  • Alignment.CenterHorizontally:Column 水平居中。
  • Alignment.CenterVertically:Row 垂直居中。
  • 14.5 尺寸与空间分配

  • fillMaxWidth、fillMaxSize、wrapContent。
  • weight:按权重分配剩余空间。
  • Spacer:占位,常配合 weight 使用。
  • Spacer vs spacedBy vs padding:不同场景选择不同方式。
  • 14.6 Modifier 顺序

  • Modifier 先写的更靠外,后写的更靠内。
  • size -> clip -> background 是圆形头像的常见顺序。
  • padding 与 background 顺序不同,背景范围不同。
  • clickable 与 padding 顺序不同,点击范围不同。
  • 14.7 布局思维

  • 复杂页面 = 嵌套 Column、Row、Box。
  • 先拆结构,再写代码。
  • 组件尽量拆小,每个组件接收 Modifier。
  • 能用自适应布局,就不要写死尺寸。

  • 十五、下节预告

    第 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、数据流、网络、分页、测试、性能优化 复刻并扩展一个经典示例
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Jetpack Compose声明式UI界面开发实用技术学与练】第 2 课:布局系统——用 Column、Row、Box 搭出个人名片
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!