Files
2026-09-04 15:22:20 +08:00

5.1 KiB
Raw Permalink Blame History

【Demo】Wrap 自动换行布局 + Chip 标签家族

源文件:lib/pages/demo/wrap_demo.dart | 分类:基础布局 一句话:Row 一行排不下就溢出报错,Wrap 会自动换行 —— 做"标签流"必备。


一、这个 Demo 解决什么问题

"搜索历史标签"、"科目标签"、"技能标签"这类数量不固定的横向排列场景:

  • Row 一行排不下会直接溢出报错(黄色条纹警告),不会换行;
  • Wrap 空间不够时自动换到下一行,是标签流的正解。

本 Demo 还完整演示了 Flutter 的 Chip 标签家族:Chip / ActionChip / FilterChip, 以及一个像素级可控的自绘标签 CustomTag(设计稿要求精确高度/间距时的选择)。


二、怎么运行 / 演示内容

runApp(const MaterialApp(home: WrapDemoPage()));

页面是一个 Wrap,里面混排了 8 种 Chip 样式 + 自绘 CustomTag: 带图标、可删除、可点击、选中态、深色底、胶囊圆角……一屏看完所有变体。


三、核心知识点(新手向)

1. Wrap 的核心属性

Wrap(
  direction: Axis.horizontal, // 排布方向(默认水平)
  spacing: 24,   // 同一行内 item 之间的水平间距
  runSpacing: 20, // 行与行之间的垂直间距(run = "行/批")
  children: [...],
)

2. 对比记忆

组件 行为
Row 横向排列,溢出报错,不换行
Wrap 横向排列,空间不足自动换行
Column 纵向排列,溢出报错(想纵向换行用 Wrap(direction: Axis.vertical))

3. Chip 家族速查(Flutter 3.x 注意!)

组件 用途 必填回调
Chip 纯展示 / 可删除标签 onDeleted 可选
ActionChip 可点击的标签(旧 Chip.onPressed 已移除) onPressed
FilterChip 带选中态的标签(旧 Chip.selected 已移除) onSelected
// 可删除
Chip(
  label: const Text("Flutter"),
  deleteIcon: Icon(Icons.close, size: 16.w),
  onDeleted: () => print("删除 Flutter"),
)

// 可点击(Flutter 3.x 用 ActionChip)
ActionChip(
  label: const Text("UniApp"),
  onPressed: () => print("点击 UniApp"),
)

// 选中态(Flutter 3.x 用 FilterChip)
FilterChip(
  label: const Text("JavaScript"),
  selected: true,
  selectedColor: const Color(0xFF4CAF50),
  onSelected: (_) {},
)

4. 自绘标签 CustomTag 为什么存在

标准 Chip 的高度/内边距是 Material 规范定的,设计稿要求"固定 24px 高、图标与文字精确 4px"时, Chip 调不精确 —— 这时用 Container + Row 手写一个,完全可控:

  • InkWell 提供点击涟漪(borderRadius 让涟漪跟随圆角);
  • TextStyle(height: 1) 去掉文字默认行高,避免把容器撑高。

四、关键代码剖析

// 自绘标签骨架(像素级可控的核心结构)
return Material(
  color: Colors.transparent, // 消除 InkWell 默认的白底
  child: InkWell(
    onTap: onTap,
    borderRadius: BorderRadius.circular(4.r),
    child: Container(
      height: 24.h, // 精确高度
      padding: EdgeInsets.only(left: 6.w, right: 6.w),
      decoration: BoxDecoration(
        color: const Color(0xFFEDF2FF),
        borderRadius: BorderRadius.circular(4.r),
        border: Border.all(color: const Color(0xFFD6E4FF)),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (icon != null) ...[
            Icon(icon, size: 12.w, color: iconColor),
            SizedBox(width: 4.w), // 图标与文字精确 4px
          ],
          Text(label, style: TextStyle(fontSize: 12.sp, height: 1)),
          if (onDeleted != null) ...[
            SizedBox(width: 2.w),
            GestureDetector(onTap: onDeleted, child: Icon(Icons.close, size: 12.w)),
          ],
        ],
      ),
    ),
  ),
)

要点:

  • Material + InkWell 组合才有水波纹;InkWell 直接包 Container 时涟漪会被容器背景盖住;
  • 用 if (xx != null) ...[] 实现"可选 icon / 可选删除叉"——传 null 就不渲染。

五、踩坑记录

  1. Chip.onPressed / Chip.selected 在 Flutter 3.x 已移除,分别改用 ActionChip / FilterChip(旧代码会编译报错)。
  2. Wrap 外层要给它有界的宽度(比如直接当 body 用,或放 Padding 里),否则换行无从谈起。
  3. 自绘标签里文字务必 height: 1,否则默认行高会让"24px 高"变成"30px+ 高"。
  4. print 只适合 Demo 调试,真实项目用 debugPrint 或日志组件(见 dio_demo 的注释)。

六、扩展思考

  • Flow:Wrap 的高阶版,自定义换行算法(间距均匀分布、瀑布流),性能更好但代码量大;
  • 标签流 + 搜索历史的真实场景:数据存本地(shared_preferences)→ 读取渲染 Wrap → 点击标签直接搜索;
  • Chip 的 shape: StadiumBorder() = 胶囊全圆角,avatar 放小图标、labelPadding 控制文字内边距;
  • 本项目 theme_demo 里的品牌切换、note_picker_demo 里的"加号格",都用了 Wrap/网格思路,可对照学习。