4.4 弹性布局(Flex)
📚 章节概览
弹性布局允许子组件按照一定比例来分配父容器空间,本章节将学习:
- Flex - 弹性布局容器
- Expanded - 按比例扩展组件
- Flexible - 灵活的弹性组件
- Spacer - 空白占位组件
- flex参数 - 弹性系数
- FlexFit - tight vs loose
- 实际应用 - 常见布局场景
🎯 核心知识点
什么是弹性布局
弹性布局(Flex Layout)允许子组件按照比例分配父容器的可用空间,而不是固定尺寸。
// 固定布局
Row(
children: [
Container(width: 100), // 固定100
Container(width: 200), // 固定200
],
)
// 弹性布局
Row(
children: [
Expanded(flex: 1), // 占1/3
Expanded(flex: 2), // 占2/3
],
)
继承关系
graph TB
A[Flex] --> B[Row]
A --> C[Column]
style A fill:#e1f5ff
style B fill:#e1ffe1
style C fill:#ffe1e1
Row 和 Column 都继承自 Flex,所以弹性布局的特性在它们中都可以使用。
1️⃣ Flex(弹性容器)
1.1 构造函数
Flex({
Key? key,
required Axis direction, // 主轴方向(必需)
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>[],
})
1.2 关键属性
| 属性 | 类型 | 说明 |
|---|---|---|
direction | Axis | 主轴方向(horizontal/vertical)必需 |
| 其他属性 | - | 与Row/Column相同 |
1.3 Flex vs Row/Column
// 这三种写法是等价的
// 使用 Flex(水平)
Flex(
direction: Axis.horizontal,
children: [...],
)
// 使用 Row
Row(
children: [...],
)
// 使用 Flex(垂直)
Flex(
direction: Axis.vertical,
children: [...],
)
// 使用 Column
Column(
children: [...],
)
何时使用Flex?
- 需要动态切换方向时
- 需要抽象化布局逻辑时
- 通常情况下,直接用Row/Column更直观
2️⃣ Expanded(按比例扩展)
2.1 构造函数
const Expanded({
Key? key,
int flex = 1, // 弹性系数,默认1
required Widget child, // 子组件
})
2.2 基本用法
示例1:平均分配空间
Row(
children: [
Expanded(child: Container(color: Colors.red)), // 占50%
Expanded(child: Container(color: Colors.green)), // 占50%
],
)
示例2:固定+弹性混合
Row(
children: [
Container(width: 80, color: Colors.red), // 固定80
Expanded(child: Container(color: Colors.green)), // 占剩余空间
Container(width: 80, color: Colors.blue), // 固定80
],
)
效果: 绿色占满中间所有剩余空间
2.3 Expanded的本质
// Expanded实际上是Flexible的特殊形式
Expanded(
flex: 1,
child: widget,
)
// 等价于
Flexible(
flex: 1,
fit: FlexFit.tight, // 强制填满
child: widget,
)
3️⃣ flex(弹性系数)
3.1 计算公式
每个组件的尺寸 = 剩余空间 × (当前flex / 总flex)
3.2 示例解析
示例1:1:2比例
Row(
children: [
Expanded(flex: 1, child: RedBox), // 占1/3
Expanded(flex: 2, child: GreenBox), // 占2/3
],
)
计算过程:
假设Row宽度 = 300px
总flex = 1 + 2 = 3
RedBox宽度 = 300 × (1/3) = 100px
GreenBox宽度 = 300 × (2/3) = 200px
示例2:1:2:3比例
Row(
children: [
Expanded(flex: 1, child: RedBox), // 占1/6
Expanded(flex: 2, child: GreenBox), // 占2/6
Expanded(flex: 3, child: BlueBox), // 占3/6
],
)
计算过程:
假设Row宽度 = 600px
总flex = 1 + 2 + 3 = 6
RedBox宽度 = 600 × (1/6) = 100px
GreenBox宽度 = 600 × (2/6) = 200px
BlueBox宽度 = 600 × (3/6) = 300px
3.3 混合固定宽度
Row(
children: [
Container(width: 100, color: Colors.red), // 固定100
Expanded(flex: 1, child: GreenBox), // 弹性1份
Expanded(flex: 2, child: BlueBox), // 弹性2份
],
)
计算过程:
假设Row宽度 = 600px
固定宽度 = 100px
剩余空间 = 600 - 100 = 500px
总flex = 1 + 2 = 3
GreenBox宽度 = 500 × (1/3) = 166.7px
BlueBox宽度 = 500 × (2/3) = 333.3px
3.4 flex=0的特殊情况
Expanded(
flex: 0, // ⚠️ 不推荐使用
child: Container(width: 100),
)
效果: flex=0 表示不参与弹性分配,但不推荐这样用,直接用固定宽度即可。
4️⃣ Spacer(空白占位)
4.1 什么是Spacer
Spacer 用于创建可调整的空白空间。
4.2 源码分析
class Spacer extends StatelessWidget {
const Spacer({Key? key, this.flex = 1});
final int flex;
@override
Widget build(BuildContext context) {
return Expanded(
flex: flex,
child: const SizedBox.shrink(), // 空Widget
);
}
}
本质: Spacer = Expanded + SizedBox.shrink()
4.3 使用场景
场景1:两端对齐
Row(
children: [
Text('左侧'),
Spacer(), // 自动填充中间空白
Text('右侧'),
],
)
效果:
左侧__________________________右侧
场景2:垂直间距分配
Column(
children: [
Text('顶部'),
Spacer(flex: 1), // 占1份空白
Text('中间'),
Spacer(flex: 2), // 占2份空白(是上方的2倍)
Text('底部'),
],
)
场景3:等间距布局
Row(
children: [
Spacer(),
Icon(Icons.home),
Spacer(),
Icon(Icons.search),
Spacer(),
Icon(Icons.person),
Spacer(),
],
)
效果: 三个图标均匀分布,两侧和中间间距相等
4.4 Spacer vs SizedBox
| 特性 | Spacer | SizedBox |
|---|---|---|
| 尺寸 | 弹性,根据flex分配 | 固定尺寸 |
| 用途 | 动态间距 | 固定间距 |
| 示例 | Spacer() | SizedBox(width: 20) |
5️⃣ Flexible(灵活布局)
5.1 构造函数
const Flexible({
Key? key,
int flex = 1,
FlexFit fit = FlexFit.loose, // loose或tight
required Widget child,
})
5.2 FlexFit类型
| FlexFit | 说明 | 行为 |
|---|---|---|
| tight | 紧约束 | 子组件必须填满分配的空间 |
| loose | 松约束 | 子组件可以小于分配的空间 |
5.3 Expanded vs Flexible
// Expanded(强制填满)
Expanded(
flex: 1,
child: Container(width: 50), // ← 宽度被忽略,强制填满
)
// 等价于
Flexible(
flex: 1,
fit: FlexFit.tight, // tight:强制填满
child: Container(width: 50),
)
// Flexible(可以不填满)
Flexible(
flex: 1,
fit: FlexFit.loose, // loose:可以更小
child: Container(width: 50), // ← 实际宽度50
)
5.4 示例对比
tight(强制填满)
Row(
children: [
Flexible(
flex: 1,
fit: FlexFit.tight,
child: Container(
width: 50, // ← 被忽略
color: Colors.red,
),
),
Flexible(
flex: 2,
fit: FlexFit.tight,
child: Container(
width: 80, // ← 被忽略
color: Colors.green,
),
),
],
)
效果: 红色占1/3,绿色占2/3,忽略width设置
loose(可以更小)
Row(
children: [
Flexible(
flex: 1,
fit: FlexFit.loose,
child: Container(
width: 50, // ← 生效
color: Colors.red,
),
),
Flexible(
flex: 2,
fit: FlexFit.loose,
child: Container(
width: 80, // ← 生效
color: Colors.green,
),
),
],
)
效果: 红色宽度50,绿色宽度80,右侧有剩余空间
5.5 何时使用Flexible
| 场景 | 使用组件 |
|---|---|
| 需要填满空间 | Expanded(或Flexible + tight) |
| 允许小于分配空间 | Flexible + loose |
| 大多数情况 | Expanded(更常用) |
🤔 常见问题(FAQ)
Q1: Expanded和Flexible的区别?
A:
Expanded = Flexible(fit: FlexFit.tight)
| 组件 | fit | 行为 |
|---|---|---|
| Expanded | tight | 强制填满分配空间 |
| Flexible(默认) | loose | 可以小于分配空间 |
建议: 90%的情况用 Expanded 即可
Q2: 为什么Expanded必须在Flex/Row/Column中使用?
A: Expanded 依赖父组件提供剩余空间信息
// ❌ 错误:Expanded不在Flex中
Container(
child: Expanded(child: Text('错误')),
)
// Error: Incorrect use of ParentDataWidget
// ✅ 正确
Row(
children: [
Expanded(child: Text('正确')),
],
)
Q3: 如何实现三等分布局?
A: 三种方法:
方法1:Expanded(推荐)
Row(
children: [
Expanded(child: Container(color: Colors.red)),
Expanded(child: Container(color: Colors.green)),
Expanded(child: Container(color: Colors.blue)),
],
)
方法2:Flexible
Row(
children: [
Flexible(flex: 1, fit: FlexFit.tight, child: RedBox),
Flexible(flex: 1, fit: FlexFit.tight, child: GreenBox),
Flexible(flex: 1, fit: FlexFit.tight, child: BlueBox),
],
)
方法3:FractionallySizedBox
Row(
children: [
FractionallySizedBox(widthFactor: 1/3, child: RedBox),
FractionallySizedBox(widthFactor: 1/3, child: GreenBox),
FractionallySizedBox(widthFactor: 1/3, child: BlueBox),
],
)
Q4: 多个Expanded如何设置不同比例?
A: 使用 flex 参数:
Row(
children: [
Expanded(flex: 2, child: Text('占2份')), // 2/5 = 40%
Expanded(flex: 3, child: Text('占3份')), // 3/5 = 60%
],
)
记忆技巧: flex值越大,占比越大
Q5: Spacer和SizedBox的区别?
A:
| 特性 | Spacer | SizedBox |
|---|---|---|
| 尺寸 | 弹性(根据剩余空间) | 固定 |
| 适用场景 | 两端对齐、动态间距 | 固定间距 |
| 性能 | 轻量 | 轻量 |
// Spacer:动态间距
Row(
children: [
Text('左'),
Spacer(), // 自动填充
Text('右'),
],
)
// SizedBox:固定间距
Row(
children: [
Text('A'),
SizedBox(width: 20), // 固定20
Text('B'),
],
)
🎯 跟着做练习
练习1:实现一个底部导航栏
目标: 创建4个图标均分的底部导航栏
步骤:
- 使用Row布局
- 4个Expanded平均分配
- 每个包含图标和文字
💡 查看答案
class BottomNavBar extends StatefulWidget {
const BottomNavBar({super.key});
@override
State<BottomNavBar> createState() => _BottomNavBarState();
}
class _BottomNavBarState extends State<BottomNavBar> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
_buildNavItem(Icons.home, '首页', 0),
_buildNavItem(Icons.explore, '发现', 1),
_buildNavItem(Icons.notifications, '消息', 2),
_buildNavItem(Icons.person, '我的', 3),
],
),
);
}
Widget _buildNavItem(IconData icon, String label, int index) {
final isActive = _currentIndex == index;
return Expanded(
child: InkWell(
onTap: () {
setState(() {
_currentIndex = index;
});
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: isActive ? Colors.blue : Colors.grey,
size: 24,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: isActive ? Colors.blue : Colors.grey,
),
),
],
),
),
);
}
}
练习2:实现一个进度条组件
目标: 创建可自定义进度的进度条
步骤:
- 使用Row布局
- 已完成部分用Expanded
- 未完成部分用Expanded
💡 查看答案
class CustomProgressBar extends StatelessWidget {
final double progress; // 0.0 - 1.0
final Color activeColor;
final Color backgroundColor;
const CustomProgressBar({
super.key,
required this.progress,
this.activeColor = Colors.blue,
this.backgroundColor = Colors.grey,
});
@override
Widget build(BuildContext context) {
final progressPercent = (progress * 100).toInt();
final activePercent = progressPercent;
final inactivePercent = 100 - progressPercent;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'$progressPercent%',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(width: 12),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Container(
height: 8,
child: Row(
children: [
// 已完成部分
if (activePercent > 0)
Expanded(
flex: activePercent,
child: Container(color: activeColor),
),
// 未完成部分
if (inactivePercent > 0)
Expanded(
flex: inactivePercent,
child: Container(
color: backgroundColor.withOpacity(0.3),
),
),
],
),
),
),
),
],
),
],
);
}
}
// 使用示例
Column(
children: [
CustomProgressBar(progress: 0.3, activeColor: Colors.red),
SizedBox(height: 16),
CustomProgressBar(progress: 0.65, activeColor: Colors.green),
SizedBox(height: 16),
CustomProgressBar(progress: 0.9, activeColor: Colors.blue),
],
)
练习3:实现一个聊天气泡布局
目标: 左右对齐的聊天气泡
步骤:
- 使用Row布局
- 自己的消息右对齐
- 对方的消息左对齐
- 使用Spacer控制对齐
💡 查看答案
class ChatBubble extends StatelessWidget {
final String message;
final bool isMine;
const ChatBubble({
super.key,
required this.message,
required this.isMine,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
child: Row(
children: [
// 自己的消息:左侧用Spacer占位
if (isMine) const Spacer(flex: 2),
// 消息气泡
Flexible(
flex: 5,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
decoration: BoxDecoration(
color: isMine ? Colors.blue : Colors.grey[300],
borderRadius: BorderRadius.circular(16),
),
child: Text(
message,
style: TextStyle(
color: isMine ? Colors.white : Colors.black87,
fontSize: 14,
),
),
),
),
// 对方的消息:右侧用Spacer占位
if (!isMine) const Spacer(flex: 2),
],
),
);
}
}
// 使用示例
Column(
children: [
ChatBubble(message: '你好!', isMine: false),
ChatBubble(message: '你好,很高兴认识你', isMine: true),
ChatBubble(message: '最近怎么样?', isMine: false),
ChatBubble(message: '挺好的,谢谢关心!', isMine: true),
],
)
📋 小结
核心概念
| 组件 | 说明 | 常用场景 |
|---|---|---|
| Flex | 弹性容器 | 需要动态方向时 |
| Expanded | 按比例扩展 | 填充剩余空间(最常用) |
| Flexible | 灵活布局 | 需要loose约束时 |
| Spacer | 空白占位 | 两端对齐、间距分配 |
继承关系
Flex ← Row
Flex ← Column
Expanded = Flexible(fit: FlexFit.tight)
Spacer = Expanded + SizedBox.shrink()
弹性系数计算
组件尺寸 = 剩余空间 × (flex / 总flex)
FlexFit对比
| FlexFit | 行为 | 使用场景 |
|---|---|---|
| tight | 强制填满 | Expanded(默认) |
| loose | 可以更小 | Flexible(默认) |
记忆技巧
- Expanded最常用:填充剩余空间首选
- flex表示份数:flex=2表示占2份
- Spacer做间距:动态间距用Spacer
- Row/Column继承Flex:弹性布局特性都能用