# 【Demo】Wrap 自动换行布局 + Chip 标签家族 > 源文件:`lib/pages/demo/wrap_demo.dart` | 分类:基础布局 > 一句话:Row 一行排不下就溢出报错,Wrap 会自动换行 —— 做"标签流"必备。 --- ## 一、这个 Demo 解决什么问题 "搜索历史标签"、"科目标签"、"技能标签"这类**数量不固定**的横向排列场景: - `Row` 一行排不下会**直接溢出报错**(黄色条纹警告),不会换行; - `Wrap` 空间不够时**自动换到下一行**,是标签流的正解。 本 Demo 还完整演示了 Flutter 的 **Chip 标签家族**:`Chip` / `ActionChip` / `FilterChip`, 以及一个**像素级可控的自绘标签 `CustomTag`**(设计稿要求精确高度/间距时的选择)。 --- ## 二、怎么运行 / 演示内容 ```dart runApp(const MaterialApp(home: WrapDemoPage())); ``` 页面是一个 Wrap,里面混排了 8 种 Chip 样式 + 自绘 CustomTag: 带图标、可删除、可点击、选中态、深色底、胶囊圆角……一屏看完所有变体。 --- ## 三、核心知识点(新手向) ### 1. Wrap 的核心属性 ```dart 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` | ```dart // 可删除 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)` 去掉文字默认行高,避免把容器撑高。 --- ## 四、关键代码剖析 ```dart // 自绘标签骨架(像素级可控的核心结构) 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/网格思路,可对照学习。