4.3 线性布局(Row和Column)
📚 章节概览
线性布局是最常用的布局方式之一,本章节将学习:
- 主轴和纵轴 - 理解线性布局的坐标系
- Row - 水平线性布局
- Column - 垂直线性布局
- MainAxisAlignment - 主轴对齐方式
- CrossAxisAlignment - 纵轴对齐方式
- MainAxisSize - 主轴尺寸控制
- TextDirection - 文本方向
- VerticalDirection - 垂直方向
- 特殊情况 - 嵌套布局规则
🎯 核心知识点
什么是线性布局
线性布局指沿水平或垂直方向排列子组件的布局方式。
Row → 水平方向排列(类似Android的LinearLayout horizontal)
Column → 垂直方向排列(类似Android的LinearLayout vertical)
继承关系
Widget → Flex → Row/Column
Row 和 Column 都继承自 Flex(弹性布局),我们将在4.4节详细介绍 Flex。
1️⃣ 主轴和纵轴
1.1 概念
线性布局有主轴(Main Axis)和纵轴(Cross Axis)之分:
graph LR
A[Row<br/>水平布局] -->|主轴| B[水平方向 ←→]
A -->|纵轴| C[垂直方向 ↑↓]
D[Column<br/>垂直布局] -->|主轴| E[垂直方向 ↑↓]
D -->|纵轴| F[水平方向 ←→]
style A fill:#e1f5ff
style D fill:#e1ffe1
| 布局类型 | 主轴方向 | 纵轴方向 |
|---|---|---|
| Row | 水平 (←→) | 垂直 (↑↓) |
| Column | 垂直 (↑↓) | 水平 (←→) |
1.2 对齐枚举类
Flutter提供两个枚举类来控制对齐:
MainAxisAlignment- 主轴对齐方式CrossAxisAlignment- 纵轴对齐方式
2️⃣ Row(水平布局)
2.1 构造函数
Row({
Key? key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, // 主轴对齐
MainAxisSize mainAxisSize = MainAxisSize.max, // 主轴尺寸
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, // 纵轴对齐
TextDirection? textDirection, // 文本方向
VerticalDirection verticalDirection = VerticalDirection.down, // 垂直方向
TextBaseline? textBaseline, // 基线
List<Widget> children = const <Widget>[], // 子组件列表
})
2.2 主要属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mainAxisAlignment | MainAxisAlignment | start | 主轴对齐方式 |
mainAxisSize | MainAxisSize | max | 主轴占用空间 |
crossAxisAlignment | CrossAxisAlignment | center | 纵轴对齐方式 |
textDirection | TextDirection? | null | 文本方向(ltr/rtl) |
verticalDirection | VerticalDirection | down | 垂直方向(down/up) |
textBaseline | TextBaseline? | null | 基线对齐类型 |
children | List<Widget> | [] | 子组件列表 |
2.3 基础用法
Row(
children: [
Container(
width: 60,
height: 60,
color: Colors.red,
child: Center(child: Text('Box 1')),
),
Container(
width: 60,
height: 60,
color: Colors.green,
child: Center(child: Text('Box 2')),
),
Container(
width: 60,
height: 60,
color: Colors.blue,
child: Center(child: Text('Box 3')),
),
],
)
效果: 三个盒子从左到右水平排列
3️⃣ MainAxisAlignment(主轴对齐)
控制子组件在主轴方向的对齐方式。
3.1 所有对齐方式
graph TB
A[MainAxisAlignment] --> B[start<br/>起始对齐]
A --> C[end<br/>末尾对齐]
A --> D[center<br/>居中对齐]
A --> E[spaceBetween<br/>两端对齐]
A --> F[spaceAround<br/>间距环绕]
A --> G[spaceEvenly<br/>间距均分]
style A fill:#e1f5ff
3.2 详细说明
① MainAxisAlignment.start(默认)
从主轴起始位置开始排列。
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [Box1, Box2, Box3],
)
效果:
[Box1][Box2][Box3]___________________
② MainAxisAlignment.end
从主轴末尾位置开始排列。
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [Box1, Box2, Box3],
)
效果:
___________________[Box1][Box2][Box3]
③ MainAxisAlignment.center
在主轴居中对齐。
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [Box1, Box2, Box3],
)
效果:
_________[Box1][Box2][Box3]__________
④ MainAxisAlignment.spaceBetween
两端对齐,子组件之间均分间距。
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Box1, Box2, Box3],
)
效果:
[Box1]____________[Box2]____________[Box3]
- 第一个组件靠左
- 最后一个组件靠右
- 中间组件均分间距
⑤ MainAxisAlignment.spaceAround
间距环绕,每个子组件两侧都有间距。
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [Box1, Box2, Box3],
)
效果:
___[Box1]______[Box2]______[Box3]___
- 每个组件两侧间距相等
- 边缘间距 = 组件间间距 / 2
⑥ MainAxisAlignment.spaceEvenly
间距均分,所有间距都相等。
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [Box1, Box2, Box3],
)
效果:
____[Box1]____[Box2]____[Box3]____
- 所有间距完全相等
- 包括边缘间距
3.3 对比表
| 对齐方式 | 边缘间距 | 组件间间距 | 特点 |
|---|---|---|---|
| start | 0 | 0 | 靠起始对齐 |
| end | 0 | 0 | 靠末尾对齐 |
| center | 相等 | 0 | 居中对齐 |
| spaceBetween | 0 | 均分 | 两端对齐 |
| spaceAround | 间距/2 | 间距 | 间距环绕 |
| spaceEvenly | 间距 | 间距 | 完全均分 |
4️⃣ CrossAxisAlignment(纵轴对齐)
控制子组件在纵轴方向的对齐方式。
4.1 所有对齐方式
| 枚举值 | 说明 | 效果 |
|---|---|---|
| start | 起始对齐 | Row中为顶部对齐 |
| end | 末尾对齐 | Row中为底部对齐 |
| center | 居中对齐(默认) | 垂直居中 |
| stretch | 拉伸填充 | 子组件填充整个纵轴 |
| baseline | 基线对齐 | 文本基线对齐 |
4.2 示例说明
① CrossAxisAlignment.start
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(height: 30, width: 50, color: Colors.red),
Container(height: 50, width: 50, color: Colors.green),
Container(height: 40, width: 50, color: Colors.blue),
],
)
效果: 所有盒子顶部对齐
② CrossAxisAlignment.end
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(height: 30, width: 50, color: Colors.red),
Container(height: 50, width: 50, color: Colors.green),
Container(height: 40, width: 50, color: Colors.blue),
],
)
效果: 所有盒子底部对齐
③ CrossAxisAlignment.center(默认)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(height: 30, width: 50, color: Colors.red),
Container(height: 50, width: 50, color: Colors.green),
Container(height: 40, width: 50, color: Colors.blue),
],
)
效果: 所有盒子垂直居中
④ CrossAxisAlignment.stretch
Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: Container(color: Colors.red)),
Expanded(child: Container(color: Colors.green)),
Expanded(child: Container(color: Colors.blue)),
],
)
效果: 所有盒子拉伸到Row的高度
注意: 使用 stretch 时,子组件不应设置固定高度
⑤ CrossAxisAlignment.baseline
基线对齐,需要配合 textBaseline 使用。
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text('Hello', style: TextStyle(fontSize: 30)),
Text('World', style: TextStyle(fontSize: 20)),
Text('!', style: TextStyle(fontSize: 40)),
],
)
效果: 所有文本按基线对齐
5️⃣ MainAxisSize(主轴尺寸)
控制 Row/Column 在主轴方向占用的空间大小。
5.1 枚举值
| 枚举值 | 说明 | 效果 |
|---|---|---|
| MainAxisSize.max | 最大化(默认) | 占满主轴所有空间 |
| MainAxisSize.min | 最小化 | 只占用子组件实际大小 |
5.2 示例对比
max(默认)- 占满空间
Container(
color: Colors.yellow[100], // 黄色背景
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(width: 50, height: 50, color: Colors.red),
SizedBox(width: 10),
Container(width: 50, height: 50, color: Colors.blue),
],
),
)
效果: Row占满整个宽度,黄色背景填满
min - 最小空间
Container(
color: Colors.yellow[100], // 黄色背景
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, // ⚠️ 无效!
children: [
Container(width: 50, height: 50, color: Colors.red),
SizedBox(width: 10),
Container(width: 50, height: 50, color: Colors.blue),
],
),
)
效果: Row只占用110宽度,黄色背景只在Row范围内
⚠️ 注意: 当 mainAxisSize = min 时,mainAxisAlignment 无效,因为没有多余空间可以分配。
6️⃣ TextDirection(文本方向)
控制子组件的排列顺序。
6.1 枚举值
| 枚举值 | 说明 | 效果 |
|---|---|---|
| TextDirection.ltr | Left to Right(默认) | 从左到右 |
| TextDirection.rtl | Right to Left | 从右到左 |
6.2 示例
ltr(默认)
Row(
textDirection: TextDirection.ltr,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('1'), Text('2'), Text('3'),
],
)
效果:
_____________[1][2][3]
↑ 右对齐
rtl
Row(
textDirection: TextDirection.rtl, // 从右到左
mainAxisAlignment: MainAxisAlignment.end, // ⚠️ 此时表示左对齐!
children: [
Text('1'), Text('2'), Text('3'),
],
)
效果:
[3][2][1]_____________
↑ 此时end表示左对齐
⚠️ 重要: 当 textDirection = rtl 时:
- 子组件顺序反转:
[1][2][3]→[3][2][1] - 对齐方向也反转:
start变成右对齐,end变成左对齐
7️⃣ Column(垂直布局)
7.1 构造函数
Column 的参数与 Row 完全相同,只是布局方向不同。
Column({
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection? textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline? textBaseline,
List<Widget> children = const <Widget>[],
})
7.2 主轴和纵轴(相对于Row反转)
| 方向 | Row | Column |
|---|---|---|
| 主轴 | 水平 (←→) | 垂直 (↑↓) |
| 纵轴 | 垂直 (↑↓) | 水平 (←→) |
7.3 基础用法
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('hi'),
Text('world'),
],
)
7.4 Column的实际宽度
重要规则: Column的宽度取决于最宽的子组件。
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('短'), // 宽度: 20
Text('这是一个很长的文本'), // 宽度: 150 ← 最宽
],
)
// Column的实际宽度 = 150
7.5 让Column占满屏幕宽度
方法1:使用 ConstrainedBox
ConstrainedBox(
constraints: BoxConstraints(minWidth: double.infinity),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('hi'),
Text('world'),
],
),
)
方法2:使用 SizedBox
SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('hi'),
Text('world'),
],
),
)
方法3:使用 Center
Center(
child: Column(
children: [
Text('hi'),
Text('world'),
],
),
)
7.6 VerticalDirection(垂直方向)
控制Column中子组件的排列顺序。
| 枚举值 | 说明 | 效果 |
|---|---|---|
| VerticalDirection.down | 从上到下(默认) | 正常顺序 |
| VerticalDirection.up | 从下到上 | 反向顺序 |
示例
// down(默认)
Column(
verticalDirection: VerticalDirection.down,
children: [
Text('1'), // 顶部
Text('2'),
Text('3'), // 底部
],
)
// up(反向)
Column(
verticalDirection: VerticalDirection.up,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('1'), // 底部!
Text('2'),
Text('3'), // 顶部!
],
)
⚠️ 注意: 当 verticalDirection = up 时:
- 子组件从下往上排列
crossAxisAlignment.start表示底部对齐
8️⃣ 特殊情况 - 嵌套布局
8.1 核心规则
⚠️ 只有最外层的 Row/Column 会占用尽可能大的空间
内层的 Row/Column 只占用实际大小
8.2 示例:嵌套Column
问题代码
Container(
color: Colors.green, // 外层背景
child: Column(
mainAxisSize: MainAxisSize.max, // ✅ 有效:占满整个屏幕高度
children: [
Container(
color: Colors.red, // 内层背景
child: Column(
mainAxisSize: MainAxisSize.max, // ❌ 无效:只占实际高度
children: [
Text('hello world'),
Text('I am Jack'),
],
),
),
],
),
)
效果:
- 外层Column(绿色)占满整个屏幕高度
- 内层Column(红色)只占两行文本的高度
8.3 解决方案:使用 Expanded
Container(
color: Colors.green,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded( // ✅ 使用Expanded
child: Container(
color: Colors.red,
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // 现在有效了
children: [
Text('hello world'),
Text('I am Jack'),
],
),
),
),
],
),
)
效果: 内层Column填充外层Column的剩余空间
8.4 可视化对比
graph TB
A[外层Column<br/>mainAxisSize=max<br/>占满屏幕] --> B[内层Column<br/>mainAxisSize=max<br/>无效!只占实际大小]
C[外层Column<br/>mainAxisSize=max<br/>占满屏幕] --> D[Expanded] --> E[内层Column<br/>填充剩余空间]
style A fill:#90EE90
style B fill:#FFB6C1
style C fill:#90EE90
style D fill:#FFD700
style E fill:#87CEEB
🤔 常见问题(FAQ)
Q1: Row/Column的默认尺寸是多少?
A: 取决于 mainAxisSize:
mainAxisSize = max(默认):主轴占满,纵轴取最大子组件mainAxisSize = min:主轴和纵轴都取实际大小
// Row: 宽度占满,高度取最高的子组件
Row(
mainAxisSize: MainAxisSize.max, // 默认
children: [
Container(width: 50, height: 30),
Container(width: 50, height: 80), // ← 最高
],
)
// Row的尺寸: 宽度=屏幕宽度, 高度=80
// Row: 宽度和高度都取实际大小
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 50, height: 30),
Container(width: 50, height: 80),
],
)
// Row的尺寸: 宽度=100, 高度=80
Q2: 如何在Row中平均分配空间?
A: 使用 Expanded 或 Flexible:
// 方法1:Expanded(平均分配)
Row(
children: [
Expanded(child: Container(color: Colors.red)), // 1/3
Expanded(child: Container(color: Colors.green)), // 1/3
Expanded(child: Container(color: Colors.blue)), // 1/3
],
)
// 方法2:Expanded with flex(按比例分配)
Row(
children: [
Expanded(flex: 1, child: Container(color: Colors.red)), // 1/6
Expanded(flex: 2, child: Container(color: Colors.green)), // 2/6
Expanded(flex: 3, child: Container(color: Colors.blue)), // 3/6
],
)
Q3: Row/Column中的子组件溢出怎么办?
A: 有三种解决方案:
方案1:使用 Expanded/Flexible
Row(
children: [
Expanded( // 自动调整宽度
child: Text('很长很长的文本...', overflow: TextOverflow.ellipsis),
),
],
)
方案2:使用 SingleChildScrollView
SingleChildScrollView(
scrollDirection: Axis.horizontal, // 水平滚动
child: Row(
children: [
Container(width: 500), // 超出屏幕宽度
Container(width: 500),
],
),
)
方案3:使用 Wrap(自动换行)
Wrap(
children: [
Container(width: 100),
Container(width: 100),
Container(width: 100),
// 如果一行放不下,会自动换行
],
)
Q4: 如何实现两端对齐且最后一行左对齐?
A: 这是一个常见需求,使用 Wrap 更合适:
Wrap(
spacing: 10, // 水平间距
runSpacing: 10, // 垂直间距
children: List.generate(10, (index) {
return Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text('$index')),
);
}),
)
为什么不用Row?
- Row不会自动换行
- 超出宽度会溢出
Q5: Column中如何让某个子组件占满剩余空间?
A: 使用 Expanded:
Column(
children: [
Container(height: 100, color: Colors.red), // 固定高度
Expanded( // 填充剩余空间
child: Container(color: Colors.blue),
),
Container(height: 100, color: Colors.green), // 固定高度
],
)
🎯 跟着做练习
练习1:实现一个卡片标题栏
目标: 创建一个卡片标题栏,左侧图标,中间标题,右侧按钮
步骤:
- 使用Row布局
- 左侧放置Icon
- 中间Expanded放置标题
- 右侧放置IconButton
💡 查看答案
class CardHeader extends StatelessWidget {
const CardHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue[50],
border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
),
child: Row(
children: [
// 左侧图标
Icon(Icons.article, color: Colors.blue),
const SizedBox(width: 12),
// 中间标题(占满剩余空间)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'文章标题',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'副标题或描述信息',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
// 右侧按钮
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
print('更多操作');
},
),
],
),
);
}
}
练习2:实现一个评分组件
目标: 创建一个星级评分显示,包含星星和评分文本
步骤:
- 使用Row布局
- 用循环生成5个星星Icon
- 右侧显示评分数字
💡 查看答案
class RatingWidget extends StatelessWidget {
final double rating; // 0.0 - 5.0
final int reviewCount;
const RatingWidget({
super.key,
required this.rating,
required this.reviewCount,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// 星星
...List.generate(5, (index) {
if (index < rating.floor()) {
// 整颗星
return const Icon(Icons.star, color: Colors.amber, size: 20);
} else if (index < rating) {
// 半颗星
return const Icon(Icons.star_half, color: Colors.amber, size: 20);
} else {
// 空星
return Icon(Icons.star_border, color: Colors.grey[400], size: 20);
}
}),
const SizedBox(width: 8),
// 评分文本
Text(
'$rating',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 4),
// 评价数量
Text(
'($reviewCount)',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
);
}
}
// 使用示例
RatingWidget(rating: 4.5, reviewCount: 128)
练习3:实现一个信息展示列表
目标: 创建一个用户信息展示列表,每行包含标签和值
步骤:
- 使用Column布局
- 每行用Row布局
- 标签固定宽度,值占剩余空间
💡 查看答案
class InfoList extends StatelessWidget {
const InfoList({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildInfoRow('姓名', '张三'),
_buildInfoRow('手机', '138-xxxx-xxxx'),
_buildInfoRow('邮箱', 'zhangsan@example.com'),
_buildInfoRow('地址', '北京市朝阳区某某街道某某小区xxx号'),
],
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标签(固定宽度)
SizedBox(
width: 60,
child: Text(
'$label:',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
),
const SizedBox(width: 12),
// 值(占满剩余空间)
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
}
📋 小结
核心概念
| 概念 | 说明 |
|---|---|
| 主轴 | 布局方向的轴(Row=水平,Column=垂直) |
| 纵轴 | 垂直于主轴的轴 |
| MainAxisAlignment | 主轴对齐方式 |
| CrossAxisAlignment | 纵轴对齐方式 |
| MainAxisSize | 主轴占用空间(max/min) |
Row vs Column
| 特性 | Row | Column |
|---|---|---|
| 主轴方向 | 水平 (←→) | 垂直 (↑↓) |
| 纵轴方向 | 垂直 (↑↓) | 水平 (←→) |
| 默认尺寸 | 宽度占满,高度取最大子组件 | 高度占满,宽度取最大子组件 |
嵌套规则
✅ 外层 Row/Column:占满空间(mainAxisSize=max)
❌ 内层 Row/Column:只占实际大小
✅ 使用 Expanded:让内层也填充空间
记忆技巧
- 主轴 = 布局方向:Row主轴水平,Column主轴垂直
- max vs min:max占满,min最小
- start/end/center:最常用的对齐方式
- spaceBetween最实用:两端对齐,中间均分
- 嵌套用Expanded:让内层填充空间