第4章:布局类组件 —— 4.5 流式布局(Wrap、Flow)

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的优缺点

✅ 优点
  1. 性能好

    • 使用转换矩阵(Transform Matrix)优化
    • 重绘时不实际调整组件位置
    • 适合动画场景
  2. 灵活

    • 自定义布局策略
    • 完全控制子组件位置
❌ 缺点
  1. 使用复杂

    • 需要实现 FlowDelegate
    • 手动计算每个子组件位置
  2. 不能自适应

    • 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:实现一个技能标签云

目标: 创建可点击的技能标签,点击后切换选中状态

步骤:

  1. 使用Wrap布局
  2. 用ChoiceChip实现选择效果
  3. 维护选中状态

💡 查看答案

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布局

步骤:

  1. 继承FlowDelegate
  2. 在paintChildren中计算圆形位置
  3. 使用三角函数计算坐标

💡 查看答案

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() 是否需要重绘

记忆技巧

  1. Wrap首选:90%场景用Wrap
  2. spacing记忆:spacing = 同行间距,runSpacing = 行间距
  3. Flow性能好:转换矩阵优化
  4. Flow很少用:除非特殊需求

🔗 相关资源


相关推荐
程序员老刘4 小时前
Flutter 3.38 版本更新:客户端开发者需要关注这三点?
flutter·客户端
AskHarries6 小时前
RevenueCat 接入 Google Play 订阅全流程详解(2025 最新)
android·flutter·google
不凡的凡6 小时前
flutter 管理工具fvm
flutter·harmonyos
消失的旧时光-19437 小时前
我如何理解 Flutter 本质
android·前端·flutter
旧时光_1 天前
第4章:布局类组件 —— 4.8 LayoutBuilder、AfterLayout
flutter
A懿轩A1 天前
Flutter:跨平台开发终极指南
flutter
肠胃炎1 天前
Flutter 基础组件
前端·flutter
木易士心1 天前
Flutter 网络请求深度解析
flutter
消失的旧时光-19431 天前
Flutter Scaffold 全面解析:打造页面骨架的最佳实践(附场景示例 + 踩坑分享)
前端·flutter