标签选择是 Flutter 开发中的高频交互场景,广泛用于兴趣选择、分类筛选、尺寸 / 颜色匹配等功能。手动实现标签选择需处理选中状态、布局适配、点击逻辑、边界限制等问题,重复编码导致效率低、样式不统一。本文封装的TagSelector组件,整合 "单选 / 多选切换 + 全样式自定义 + 流式布局 + 状态回调" 四大核心能力,一行代码即可实现标签选择功能,适配 90%+ 业务场景!
一、核心需求拆解
✅ 模式灵活:支持单选(分类筛选)、多选(兴趣选择)两种核心模式,按需切换✅ 样式全自定义:标签颜色、圆角、边框、间距、文本样式均可配置,适配不同设计风格✅ 边界控制:多选模式支持最大选中数量限制,避免选中过多导致 UI 混乱✅ 布局适配:内置Wrap流式布局,自动换行,适配多标签、窄屏幕场景✅ 状态可控:支持初始选中标签、选中状态实时回调,便于业务逻辑处理✅ 异常防护:初始选中标签自动去重、校验有效性,避免无效状态
二、完整代码实现(可直接复制使用)
dart
import 'package:flutter/material.dart';
// 选择模式枚举(覆盖核心场景,逻辑清晰)
enum SelectMode {
single, // 单选模式(如分类筛选、尺寸选择)
multiple // 多选模式(如兴趣选择、颜色筛选)
}
/// 通用标签选择组件(支持单选/多选、全样式自定义)
class TagSelector extends StatefulWidget {
// 必选参数:核心配置
final List<String> tags; // 标签列表(必填,支持任意字符串)
final SelectMode mode; // 选择模式(单选/多选)
final Function(List<String> selectedTags) onSelected; // 选中回调(实时返回选中结果)
// 可选参数:初始状态与限制
final List<String> initialSelected; // 初始选中标签(默认空)
final int? maxSelectCount; // 最大选中数量(仅多选模式生效,默认无限制)
// 可选参数:标签样式配置
final double tagHeight; // 标签高度(默认36px,适配主流设计)
final double horizontalPadding; // 标签水平内边距(默认16px)
final double verticalPadding; // 标签垂直内边距(默认0,与高度配合居中)
final double tagSpacing; // 标签间距(水平+垂直,默认12px)
final double borderRadius; // 标签圆角(默认18px,圆形标签可设为高度的一半)
final double borderWidth; // 标签边框宽度(默认1px)
// 可选参数:文本样式配置
final TextStyle normalTextStyle; // 未选中文本样式(默认14px,黑色87%)
final TextStyle selectedTextStyle; // 选中文本样式(默认14px,蓝色)
// 可选参数:颜色配置
final Color normalBgColor; // 未选中背景色(默认白色)
final Color selectedBgColor; // 选中背景色(默认浅蓝色)
final Color normalBorderColor; // 未选中边框色(默认浅灰色)
final Color selectedBorderColor; // 选中边框色(默认蓝色)
const TagSelector({
super.key,
required this.tags,
required this.mode,
required this.onSelected,
this.initialSelected = const [],
this.maxSelectCount,
this.tagHeight = 36.0,
this.horizontalPadding = 16.0,
this.verticalPadding = 0.0,
this.tagSpacing = 12.0,
this.borderRadius = 18.0,
this.borderWidth = 1.0,
this.normalTextStyle = const TextStyle(
color: Colors.black87,
fontSize: 14.0,
),
this.selectedTextStyle = const TextStyle(
color: Colors.blue,
fontSize: 14.0,
),
this.normalBgColor = Colors.white,
this.selectedBgColor = Color(0xFFE3F2FD),
this.normalBorderColor = Color(0xFFE0E0E0),
this.selectedBorderColor = Colors.blue,
}) : assert(
mode != SelectMode.multiple || (maxSelectCount == null || maxSelectCount >= 1),
"多选模式下,最大选中数量必须大于等于1",
);
@override
State<TagSelector> createState() => _TagSelectorState();
}
class _TagSelectorState extends State<TagSelector> {
late List<String> _selectedTags; // 选中标签列表(内部维护状态)
@override
void initState() {
super.initState();
// 初始化选中标签:去重+校验是否在标签列表中,避免无效状态
_selectedTags = widget.initialSelected
.where((tag) => widget.tags.contains(tag))
.toSet()
.toList();
// 单选模式下:初始选中最多保留1个(避免异常)
if (widget.mode == SelectMode.single && _selectedTags.length > 1) {
_selectedTags = [_selectedTags.first];
}
// 初始回调:返回初始选中状态
widget.onSelected(List.unmodifiable(_selectedTags));
}
/// 标签点击事件处理(核心逻辑)
void _onTagTap(String tag) {
setState(() {
if (widget.mode == SelectMode.single) {
// 单选模式:切换选中状态(选中当前/取消所有)
_selectedTags = _selectedTags.contains(tag) ? [] : [tag];
} else {
// 多选模式:添加/移除选中,限制最大数量
if (_selectedTags.contains(tag)) {
// 已选中:移除
_selectedTags.remove(tag);
} else {
// 未选中:判断是否超过最大数量
if (widget.maxSelectCount == null || _selectedTags.length < widget.maxSelectCount!) {
_selectedTags.add(tag);
}
}
}
// 实时触发选中回调,返回不可修改的列表(避免外部篡改)
widget.onSelected(List.unmodifiable(_selectedTags));
});
}
/// 构建单个标签(根据选中状态切换样式)
Widget _buildSingleTag(String tag) {
final isSelected = _selectedTags.contains(tag); // 是否选中
return GestureDetector(
onTap: () => _onTagTap(tag), // 绑定点击事件
child: Container(
height: widget.tagHeight,
padding: EdgeInsets.symmetric(
horizontal: widget.horizontalPadding,
vertical: widget.verticalPadding,
),
margin: EdgeInsets.only(
right: widget.tagSpacing,
bottom: widget.tagSpacing,
),
decoration: BoxDecoration(
// 背景色:选中/未选中切换
color: isSelected ? widget.selectedBgColor : widget.normalBgColor,
// 边框:颜色+宽度切换
border: Border.all(
color: isSelected ? widget.selectedBorderColor : widget.normalBorderColor,
width: widget.borderWidth,
),
// 圆角:统一配置,支持圆形标签
borderRadius: BorderRadius.circular(widget.borderRadius),
),
alignment: Alignment.center, // 文本居中
child: Text(
tag,
style: isSelected ? widget.selectedTextStyle : widget.normalTextStyle,
maxLines: 1, // 避免标签文本换行
overflow: TextOverflow.ellipsis, // 文本溢出截断
),
),
);
}
@override
Widget build(BuildContext context) {
// 流式布局:自动换行,适配多标签、窄屏幕
return Wrap(
children: widget.tags.map((tag) => _buildSingleTag(tag)).toList(),
);
}
}
三、实战使用示例(覆盖 5 大高频场景)
场景 1:多选标签(兴趣选择,个人中心)
适配用户兴趣标签选择,限制最大选中数量,风格温馨:
dart
TagSelector(
tags: ["旅行", "美食", "电影", "音乐", "运动", "阅读", "游戏", "摄影"],
mode: SelectMode.multiple,
maxSelectCount: 3, // 最多选中3个兴趣
initialSelected: ["美食", "电影"], // 初始选中2个
onSelected: (selected) {
debugPrint("用户选中的兴趣:$selected");
// 实际场景:保存用户兴趣偏好
},
// 自定义样式:粉色系
selectedBgColor: Colors.pink[100]!,
selectedBorderColor: Colors.pinkAccent,
selectedTextStyle: const TextStyle(color: Colors.pinkAccent, fontWeight: FontWeight.w500),
normalBorderColor: Colors.grey[300]!,
tagSpacing: 10,
borderRadius: 20,
),
场景 2:单选标签(分类筛选,商品列表)
适配电商商品分类筛选,默认选中 "全部",风格简洁:
dart
TagSelector(
tags: ["全部", "新品", "热销", "优惠", "好评"],
mode: SelectMode.single,
initialSelected: ["全部"], // 初始选中"全部"
onSelected: (selected) {
debugPrint("当前选中的分类:${selected.first}");
// 实际场景:筛选对应分类的商品
},
// 自定义样式:简约商务风
tagHeight: 32,
horizontalPadding: 20,
borderRadius: 16,
normalBgColor: const Color(0xFFF5F5F5),
normalBorderColor: Colors.transparent,
selectedBgColor: Colors.blue,
selectedTextStyle: const TextStyle(color: Colors.white, fontSize: 14),
),
场景 3:单选标签(尺寸选择,商品详情)
适配服装、鞋靴等尺寸选择,标签紧凑,风格实用:
dart
TagSelector(
tags: ["XS", "S", "M", "L", "XL", "XXL", "XXXL"],
mode: SelectMode.single,
onSelected: (selected) {
debugPrint("用户选中的尺寸:${selected.first}");
// 实际场景:记录用户选择的尺寸,用于下单
},
// 自定义样式:紧凑圆角
tagHeight: 38,
horizontalPadding: 18,
tagSpacing: 8,
borderRadius: 19, // 圆形标签(高度的一半)
normalBorderColor: Colors.grey[300]!,
selectedBorderColor: Colors.orange,
selectedBgColor: Colors.orange[50]!,
selectedTextStyle: const TextStyle(color: Colors.orange, fontWeight: FontWeight.w600),
),
场景 4:多选标签(颜色筛选,商品详情)
适配商品颜色筛选,无初始选中,支持最多选 2 个:
dart
TagSelector(
tags: ["黑色", "白色", "红色", "蓝色", "绿色", "黄色", "紫色"],
mode: SelectMode.multiple,
maxSelectCount: 2, // 最多选2种颜色
onSelected: (selected) {
debugPrint("用户选中的颜色:$selected");
// 实际场景:筛选对应颜色的商品
},
// 自定义样式:多彩边框
tagHeight: 34,
horizontalPadding: 16,
borderRadius: 4, // 方形圆角
borderWidth: 2,
normalTextStyle: const TextStyle(fontSize: 13),
selectedTextStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
// 动态颜色(根据标签文本匹配边框色)
normalBorderColor: Colors.grey[400]!,
selectedBorderColor: (tag) {
switch (tag) {
case "黑色": return Colors.black;
case "白色": return Colors.grey[300]!;
case "红色": return Colors.red;
case "蓝色": return Colors.blue;
case "绿色": return Colors.green;
case "黄色": return Colors.yellow[600]!;
case "紫色": return Colors.purple;
default: return Colors.grey;
}
},
),
场景 5:多选标签(标签筛选,资讯列表)
适配资讯、文章标签筛选,风格清淡,支持任意数量选中:
dart
TagSelector(
tags: ["科技", "财经", "娱乐", "体育", "健康", "教育", "职场", "育儿"],
mode: SelectMode.multiple,
onSelected: (selected) {
debugPrint("用户选中的标签:$selected");
// 实际场景:筛选对应标签的资讯内容
},
// 自定义样式:清淡风格
tagHeight: 32,
horizontalPadding: 14,
tagSpacing: 10,
borderRadius: 16,
normalBgColor: const Color(0xFFF8F8F8),
normalBorderColor: Colors.transparent,
selectedBgColor: Colors.blue[50]!,
selectedTextStyle: const TextStyle(color: Colors.blue),
),
四、核心封装技巧(让组件更易用、更稳定)
- 枚举化模式设计 :通过
SelectMode枚举明确区分单选 / 多选,逻辑清晰,扩展新模式(如半选)时只需新增枚举值,维护成本低。 - 状态安全管理:初始选中标签自动去重 + 有效性校验,避免因传入无效标签导致的 UI 异常;单选模式下强制最多选中 1 个,保证状态一致性。
- 样式全维度自定义:从尺寸、间距、圆角到颜色、文本样式,所有可见元素均可配置,适配不同 APP 的设计风格,无需修改组件源码。
- 布局自适应 :使用
Wrap流式布局,自动换行适配多标签、窄屏幕场景,避免标签溢出或布局错乱,无需手动处理换行逻辑。 - 回调安全返回 :通过
List.unmodifiable返回选中列表,避免外部篡改内部状态,保证组件状态可控。 - 边界限制保护 :多选模式支持
maxSelectCount参数,限制最大选中数量,避免选中过多标签导致 UI 拥挤,提升用户体验。
五、避坑指南(实际开发必看)
- 标签列表去重 :传入的
tags列表建议提前去重,避免重复标签导致的选中状态混乱(组件内部未处理标签去重,需外部保证)。 - 初始选中有效性 :
initialSelected中的标签必须存在于tags列表中,否则会被自动过滤(组件内部已做校验,但建议外部提前确保有效性)。 - 圆角与高度匹配 :若需实现圆形标签,需将
borderRadius设为tagHeight / 2,同时保证tagHeight为固定值(避免动态高度导致圆形变形)。 - 文本长度控制 :标签文本不宜过长(建议不超过 6 个字符),过长会导致标签过宽,影响布局美观;若需支持长文本,可适当增大
horizontalPadding或限制标签最大宽度。 - 多语言适配 :标签文本支持多语言切换,只需动态修改
tags列表即可,组件样式会自动适配。 - 长列表性能 :若标签数量极多(超过 20 个),建议搭配
SingleChildScrollView使用,避免布局高度过高导致的性能问题。
总结
TagSelector组件通过 "通用化封装 + 精细化控制",彻底解决了原生标签选择开发的痛点,实现了 "一行代码调用、全场景适配、样式统一" 的目标。无论是兴趣选择、分类筛选,还是尺寸 / 颜色匹配,都能通过灵活配置快速实现,既提升了开发效率,又保证了 APP 内标签交互的一致性和美观度。