4.5 流式布局(Wrap、Flow)
📚 章节概览
流式布局是指超出屏幕范围会自动换行的布局方式,本章节将学习:
- Row/Column溢出问题 - 为什么需要流式布局
- Wrap - 自动换行布局
- spacing - 主轴方向间距
- runSpacing - 纵轴方向间距
- alignment - 对齐方式
- Flow - 高性能自定义布局
- FlowDelegate - 自定义布局策略
🎯 核心知识点
什么是流式布局
当子组件超出父容器范围时,自动换行的布局方式称为流式布局。
dart
// ❌ Row会溢出
Row(
children: [
Text('很长的文本' * 100), // 超出屏幕 → 报错
],
)
// ✅ Wrap自动换行
Wrap(
children: [
Text('很长的文本' * 100), // 超出屏幕 → 自动换行
],
)
Wrap vs Flow
| 特性 | Wrap | Flow |
|---|---|---|
| 易用性 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 性能 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 灵活性 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
建议: 90%的场景用 Wrap 即可
1️⃣ Row/Column的溢出问题
1.1 溢出示例
dart
Row(
children: [
Text('xxx' * 100), // 超长文本
],
)
运行效果:
css
xxxxxxxxxxxxxxxxxxxxx... ⚠️ OVERFLOW
错误信息:
csharp
A RenderFlex overflowed by XXX pixels on the right.
1.2 传统解决方案
方案1:SingleChildScrollView(滚动)
dart
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Text('很长的文本'),
Text('很长的文本'),
],
),
)
缺点: 需要手动滚动,不适合多行展示
方案2:Expanded(截断)
dart
Row(
children: [
Expanded(
child: Text(
'很长的文本',
overflow: TextOverflow.ellipsis, // 省略号
),
),
],
)
缺点: 内容被截断,信息不完整
方案3:Wrap(自动换行)✅
dart
Wrap(
children: [
Text('很长的文本'),
Text('很长的文本'),
],
)
优点: 自动换行,内容完整显示
2️⃣ Wrap(自动换行布局)
2.1 构造函数
dart
Wrap({
Key? key,
Axis direction = Axis.horizontal, // 主轴方向
WrapAlignment alignment = WrapAlignment.start, // 主轴对齐
double spacing = 0.0, // 主轴间距
WrapAlignment runAlignment = WrapAlignment.start, // 纵轴对齐
double runSpacing = 0.0, // 纵轴间距
WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start, // 交叉轴对齐
TextDirection? textDirection, // 文本方向
VerticalDirection verticalDirection = VerticalDirection.down, // 垂直方向
List<Widget> children = const <Widget>[], // 子组件
})
2.2 主要属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
direction |
Axis | horizontal | 主轴方向 |
alignment |
WrapAlignment | start | 主轴对齐方式 |
spacing |
double | 0.0 | 主轴方向子组件间距 |
runAlignment |
WrapAlignment | start | 纵轴对齐方式 |
runSpacing |
double | 0.0 | 纵轴方向行间距 |
crossAxisAlignment |
WrapCrossAlignment | start | 交叉轴对齐 |
2.3 基础用法
dart
Wrap(
children: [
Chip(label: Text('Flutter')),
Chip(label: Text('Dart')),
Chip(label: Text('iOS')),
Chip(label: Text('Android')),
Chip(label: Text('Web')),
],
)
效果: 超出宽度自动换行
3️⃣ spacing 和 runSpacing
3.1 spacing(主轴间距)
控制同一行内子组件之间的间距。
dart
Wrap(
spacing: 8.0, // 水平间距8
children: [
Chip(label: Text('A')),
Chip(label: Text('B')),
Chip(label: Text('C')),
],
)
效果:
css
[A] 8px [B] 8px [C]
3.2 runSpacing(纵轴间距)
控制不同行之间的间距。
dart
Wrap(
runSpacing: 12.0, // 行间距12
children: [
Chip(label: Text('A')),
Chip(label: Text('B')),
Chip(label: Text('C')),
// ... 更多组件,自动换行
],
)
效果:
css
[A] [B] [C]
↕️ 12px
[D] [E] [F]
3.3 同时使用
dart
Wrap(
spacing: 8.0, // 水平间距
runSpacing: 12.0, // 垂直间距
children: [
Chip(label: Text('Flutter')),
Chip(label: Text('Dart')),
Chip(label: Text('iOS')),
Chip(label: Text('Android')),
],
)
可视化效果:
css
[Flutter] 8px [Dart] 8px [iOS]
↕️ 12px
[Android]
4️⃣ alignment(对齐方式)
4.1 WrapAlignment枚举值
| 枚举值 | 说明 | 效果 |
|---|---|---|
| start | 起始对齐(默认) | 从左到右 |
| end | 末尾对齐 | 从右到左 |
| center | 居中对齐 | 居中排列 |
| spaceBetween | 两端对齐 | 两端贴边,均分间距 |
| spaceAround | 间距环绕 | 每个组件两侧间距相等 |
| spaceEvenly | 间距均分 | 所有间距完全相等 |
4.2 示例对比
start(默认)
dart
Wrap(
alignment: WrapAlignment.start,
children: [
Chip(label: Text('A')),
Chip(label: Text('B')),
Chip(label: Text('C')),
],
)
效果:
markdown
[A][B][C]_____________________
center
dart
Wrap(
alignment: WrapAlignment.center,
children: [
Chip(label: Text('A')),
Chip(label: Text('B')),
Chip(label: Text('C')),
],
)
效果:
markdown
__________[A][B][C]___________
spaceBetween
dart
Wrap(
alignment: WrapAlignment.spaceBetween,
spacing: 8,
children: [
Chip(label: Text('A')),
Chip(label: Text('B')),
Chip(label: Text('C')),
],
)
效果:
markdown
[A]___________[B]___________[C]
4.3 runAlignment(纵轴对齐)
控制多行之间的对齐方式。
dart
SizedBox(
height: 200,
child: Wrap(
runAlignment: WrapAlignment.center, // 垂直居中
children: [...],
),
)
5️⃣ Wrap实际应用
应用1:标签云(Tag Cloud)
dart
class TagCloud extends StatelessWidget {
final List<String> tags;
const TagCloud({required this.tags});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: tags.map((tag) {
return Chip(
label: Text(tag),
avatar: CircleAvatar(
backgroundColor: Colors.blue,
child: Text(tag[0]),
),
);
}).toList(),
);
}
}
// 使用
TagCloud(
tags: ['Flutter', 'Dart', 'iOS', 'Android', 'Web'],
)
应用2:可选择标签
dart
class SelectableTags extends StatefulWidget {
@override
_SelectableTagsState createState() => _SelectableTagsState();
}
class _SelectableTagsState extends State<SelectableTags> {
final List<String> _allTags = ['前端', '后端', '移动端', '算法'];
final List<String> _selected = [];
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: _allTags.map((tag) {
final isSelected = _selected.contains(tag);
return FilterChip(
label: Text(tag),
selected: isSelected,
onSelected: (selected) {
setState(() {
if (selected) {
_selected.add(tag);
} else {
_selected.remove(tag);
}
});
},
);
}).toList(),
);
}
}
应用3:图片网格(自适应列数)
dart
Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(
20,
(index) => Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text('$index')),
),
),
)
优点: 根据屏幕宽度自动调整列数
6️⃣ Flow(高性能自定义布局)
6.1 什么是Flow
Flow 是一个对子组件尺寸和位置调整非常高效的控件。
6.2 Flow的优缺点
✅ 优点
-
性能好
- 使用转换矩阵(Transform Matrix)优化
- 重绘时不实际调整组件位置
- 适合动画场景
-
灵活
- 自定义布局策略
- 完全控制子组件位置
❌ 缺点
-
使用复杂
- 需要实现
FlowDelegate - 手动计算每个子组件位置
- 需要实现
-
不能自适应
- Flow不能自适应子组件大小
- 必须指定固定大小
6.3 FlowDelegate
需要继承 FlowDelegate 并实现三个方法:
dart
class MyFlowDelegate extends FlowDelegate {
// 1. 绘制子组件(必需)
@override
void paintChildren(FlowPaintingContext context) {
// 计算并绘制每个子组件
}
// 2. 返回Flow大小(必需)
@override
Size getSize(BoxConstraints constraints) {
// 返回Flow的大小
return Size(width, height);
}
// 3. 是否需要重绘(必需)
@override
bool shouldRepaint(FlowDelegate oldDelegate) {
return oldDelegate != this;
}
}
6.4 完整示例
dart
class TestFlowDelegate extends FlowDelegate {
final EdgeInsets margin;
TestFlowDelegate({this.margin = EdgeInsets.zero});
@override
void paintChildren(FlowPaintingContext context) {
var x = margin.left;
var y = margin.top;
// 遍历所有子组件
for (int i = 0; i < context.childCount; i++) {
var childSize = context.getChildSize(i)!;
var w = childSize.width + x + margin.right;
// 判断是否需要换行
if (w < context.size.width) {
// 当前行能放下
context.paintChild(
i,
transform: Matrix4.translationValues(x, y, 0.0),
);
x = w + margin.left;
} else {
// 需要换行
x = margin.left;
y += childSize.height + margin.top + margin.bottom;
context.paintChild(
i,
transform: Matrix4.translationValues(x, y, 0.0),
);
x += childSize.width + margin.left + margin.right;
}
}
}
@override
Size getSize(BoxConstraints constraints) {
// 返回固定大小
return Size(double.infinity, 200.0);
}
@override
bool shouldRepaint(FlowDelegate oldDelegate) {
return oldDelegate != this;
}
}
// 使用
Flow(
delegate: TestFlowDelegate(margin: EdgeInsets.all(10)),
children: [
Container(width: 80, height: 80, color: Colors.red),
Container(width: 80, height: 80, color: Colors.green),
Container(width: 80, height: 80, color: Colors.blue),
],
)
6.5 FlowPaintingContext方法
| 方法 | 说明 |
|---|---|
childCount |
子组件数量 |
getChildSize(int i) |
获取第i个子组件的尺寸 |
paintChild(int i, {...}) |
绘制第i个子组件 |
size |
Flow的尺寸 |
6.6 何时使用Flow
| 场景 | 推荐 |
|---|---|
| 简单流式布局 | ❌ 用Wrap |
| 需要动画 | ✅ 用Flow |
| 需要精确控制位置 | ✅ 用Flow |
| 性能要求极高 | ✅ 用Flow |
| 大多数情况 | ❌ 用Wrap |
🤔 常见问题(FAQ)
Q1: Wrap和Flow的区别?
A:
| 特性 | Wrap | Flow |
|---|---|---|
| 易用性 | 简单,开箱即用 | 复杂,需要自定义 |
| 性能 | 好 | 更好(转换矩阵优化) |
| 灵活性 | 固定规则 | 完全自定义 |
| 自适应 | ✅ 自动适应子组件 | ❌ 需要指定大小 |
| 推荐场景 | 大多数场景 | 动画、高性能需求 |
建议: 优先使用Wrap,只有在特殊需求下才用Flow
Q2: spacing和runSpacing的区别?
A:
- spacing:主轴方向间距(同一行/列内)
- runSpacing:纵轴方向间距(不同行/列之间)
dart
Wrap(
spacing: 8, // 水平间距(同行内)
runSpacing: 12, // 垂直间距(行之间)
children: [
Text('A'), Text('B'), Text('C'),
Text('D'), Text('E'), Text('F'),
],
)
可视化:
scss
[A] 8px [B] 8px [C]
↕️ 12px (runSpacing)
[D] 8px [E] 8px [F]
Q3: Wrap如何实现等宽子组件?
A: Wrap的子组件是自然宽度,不支持等宽。可以用以下方案:
方案1:固定宽度
dart
Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(10, (i) {
return SizedBox(
width: 100, // 固定宽度
child: Chip(label: Text('Item $i')),
);
}),
)
方案2:计算宽度
dart
LayoutBuilder(
builder: (context, constraints) {
// 计算每行3个,自动计算宽度
final itemWidth = (constraints.maxWidth - 16) / 3;
return Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(10, (i) {
return SizedBox(
width: itemWidth,
child: Chip(label: Text('$i')),
);
}),
);
},
)
Q4: Flow的性能为什么更好?
A: Flow使用转换矩阵(Transform Matrix)而不是实际移动组件:
dart
// Wrap:实际改变组件位置(重新布局)
Container(
margin: EdgeInsets.only(left: 100), // 实际移动
child: Widget(),
)
// Flow:使用转换矩阵(不重新布局)
context.paintChild(
i,
transform: Matrix4.translationValues(100, 0, 0), // 矩阵变换
)
转换矩阵优势:
- 不触发布局(Layout)阶段
- 只触发绘制(Paint)阶段
- GPU加速
- 适合动画
Q5: Wrap如何限制最大行数?
A: Wrap本身不支持限制行数,可以结合其他组件:
dart
// 方案1:用LimitedBox限制高度
LimitedBox(
maxHeight: 100, // 限制最大高度
child: SingleChildScrollView(
child: Wrap(
children: [...],
),
),
)
// 方案2:手动截取子组件
Wrap(
children: items.take(12).toList(), // 只显示前12个
)
// 方案3:用ClipRect裁剪
ClipRect(
child: Container(
height: 100,
child: Wrap(
children: [...],
),
),
)
🎯 跟着做练习
练习1:实现一个技能标签云
目标: 创建可点击的技能标签,点击后切换选中状态
步骤:
- 使用Wrap布局
- 用ChoiceChip实现选择效果
- 维护选中状态
💡 查看答案
dart
class SkillTags extends StatefulWidget {
const SkillTags({super.key});
@override
State<SkillTags> createState() => _SkillTagsState();
}
class _SkillTagsState extends State<SkillTags> {
final List<String> _skills = [
'Flutter', 'Dart', 'iOS', 'Android',
'React', 'Vue', 'Node.js', 'Python',
'Java', 'Kotlin', 'Swift', 'TypeScript',
];
final Set<String> _selected = {};
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'请选择您擅长的技能:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _skills.map((skill) {
final isSelected = _selected.contains(skill);
return ChoiceChip(
label: Text(skill),
selected: isSelected,
onSelected: (selected) {
setState(() {
if (selected) {
_selected.add(skill);
} else {
_selected.remove(skill);
}
});
},
);
}).toList(),
),
if (_selected.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
'已选择 ${_selected.length} 项:${_selected.join('、')}',
style: const TextStyle(color: Colors.blue),
),
],
],
);
}
}
练习2:实现一个自定义Flow动画
目标: 创建一个圆形排列的Flow布局
步骤:
- 继承FlowDelegate
- 在paintChildren中计算圆形位置
- 使用三角函数计算坐标
💡 查看答案
dart
import 'dart:math';
class CircleFlowDelegate extends FlowDelegate {
final double radius;
CircleFlowDelegate({this.radius = 80});
@override
void paintChildren(FlowPaintingContext context) {
final centerX = context.size.width / 2;
final centerY = context.size.height / 2;
for (int i = 0; i < context.childCount; i++) {
final angle = (2 * pi / context.childCount) * i;
final x = centerX + radius * cos(angle) - 20; // 20是子组件宽度的一半
final y = centerY + radius * sin(angle) - 20;
context.paintChild(
i,
transform: Matrix4.translationValues(x, y, 0.0),
);
}
}
@override
Size getSize(BoxConstraints constraints) {
return Size(200, 200);
}
@override
bool shouldRepaint(CircleFlowDelegate oldDelegate) {
return radius != oldDelegate.radius;
}
}
// 使用
Flow(
delegate: CircleFlowDelegate(radius: 80),
children: List.generate(
8,
(i) => Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'${i + 1}',
style: const TextStyle(color: Colors.white),
),
),
),
),
)
📋 小结
核心概念
| 组件 | 说明 | 使用场景 |
|---|---|---|
| Wrap | 自动换行布局 | 标签云、按钮组、图片网格 |
| Flow | 高性能自定义布局 | 动画、复杂布局策略 |
Wrap常用属性
| 属性 | 说明 | 常用值 |
|---|---|---|
spacing |
主轴间距 | 8.0 |
runSpacing |
纵轴间距 | 8.0 |
alignment |
主轴对齐 | start/center |
runAlignment |
纵轴对齐 | start/center |
Flow关键方法
| 方法 | 说明 |
|---|---|
paintChildren() |
计算并绘制子组件位置 |
getSize() |
返回Flow的尺寸 |
shouldRepaint() |
是否需要重绘 |
记忆技巧
- Wrap首选:90%场景用Wrap
- spacing记忆:spacing = 同行间距,runSpacing = 行间距
- Flow性能好:转换矩阵优化
- Flow很少用:除非特殊需求