Flutter 通用标签组件 TagWidget:单选 / 多选 + 流式布局 + 全样式自定义
Flutter标签组件封装方案:实现多功能标签控件 本文介绍了一个高度可定制的Flutter标签组件TagWidget,解决了原生开发中标签组件重复开发、样式混乱的问题。该组件整合了四大核心功能: 支持单选/多选模式切换 流式布局自动适配 可关闭标签功能 全样式自定义能力 组件特点包括: 内置选中状态管理,简化使用流程 基于Wrap实现自动换行布局 支持深色模式适配 提供丰富的样式配置选项 处理了
·
在 Flutter 开发中,标签(Tag)是筛选分类、标签展示、状态标记的高频组件。原生无现成标签组件,重复开发易导致样式混乱、布局适配差。本文封装的 TagWidget 整合 “单选 / 多选 + 流式布局 + 可关闭标签 + 全样式自定义” 四大核心能力,适配标签筛选、标签展示、状态标记等场景,一行代码即可集成。
一、核心优势(精准解决开发痛点)
- 单选 / 多选自由切换:支持单选(互斥选中)、多选(任意组合),内置选中状态管理,无需外部控制
- 流式布局自动适配:标签自动换行,支持最大行数限制,适配不同屏幕宽度与标签数量
- 样式全维度自定义:标签颜色、圆角、边框、文本样式、选中状态样式均可配置,贴合 APP 主题
- 功能灵活扩展:支持可关闭标签(带删除图标)、禁用状态、自定义标签内容(图标 + 文本)
- 适配性强体验优:自动适配深色模式,标签点击区域充足,选中状态反馈清晰
二、核心配置速览(关键参数一目了然)
| 配置分类 | 核心参数 | 核心作用 |
|---|---|---|
| 必选配置 | tags: List<String>、onSelected: Function(List<String>) |
标签列表、选中回调(多选返回列表,单选返回单个元素列表) |
| 功能配置 | isMultiple、initialSelected、isClosable、maxLines、isDisabled |
单选 / 多选、初始选中、是否可关闭、最大行数、整体禁用 |
| 样式配置 | tagHeight、tagPadding、borderRadius、tagStyle、selectedStyle |
标签高度、内边距、圆角、文本样式、选中样式 |
| 适配配置 | adaptDarkMode、spacing、runSpacing、disabledColor |
深色模式适配、标签间距、行间距、禁用颜色 |
三、生产级完整代码(可直接复制,开箱即用)
dart
import 'package:flutter/material.dart';
/// 通用标签组件(支持单选/多选、流式布局、可关闭)
class TagWidget extends StatefulWidget {
// 必选参数
final List<String> tags; // 标签列表(必须非空且唯一)
final Function(List<String>) onSelected; // 选中回调
// 功能配置
final bool isMultiple; // 是否多选(默认true)
final List<String> initialSelected; // 初始选中标签(默认空)
final bool isClosable; // 标签是否可关闭(默认false)
final int? maxLines; // 最大行数(默认无限制,超出滚动)
final bool isDisabled; // 整体禁用(默认false)
final bool disableUnselected; // 仅启用已选中标签(默认false)
// 样式配置
final double tagHeight; // 标签高度(默认32)
final EdgeInsetsGeometry tagPadding; // 标签内边距(默认水平16)
final double borderRadius; // 标签圆角(默认16)
final Color borderColor; // 标签边框颜色(默认浅灰)
final Color bgColor; // 标签背景色(默认白色)
final Color selectedBgColor; // 选中标签背景色(默认蓝色)
final Color selectedBorderColor; // 选中标签边框颜色(默认蓝色)
final TextStyle? tagStyle; // 标签文本样式
final TextStyle? selectedTagStyle; // 选中标签文本样式
final Color closeIconColor; // 关闭图标颜色(默认灰色)
final Color selectedCloseIconColor; // 选中标签关闭图标颜色(默认白色)
final double closeIconSize; // 关闭图标大小(默认16)
// 布局配置
final double spacing; // 标签水平间距(默认8)
final double runSpacing; // 标签垂直间距(默认8)
final MainAxisAlignment mainAxisAlignment; // 主轴对齐方式(默认起始对齐)
// 适配配置
final bool adaptDarkMode; // 是否适配深色模式(默认true)
const TagWidget({
super.key,
required this.tags,
required this.onSelected,
// 功能配置
this.isMultiple = true,
this.initialSelected = const [],
this.isClosable = false,
this.maxLines,
this.isDisabled = false,
this.disableUnselected = false,
// 样式配置
this.tagHeight = 32.0,
this.tagPadding = const EdgeInsets.symmetric(horizontal: 16),
this.borderRadius = 16.0,
this.borderColor = const Color(0xFFE0E0E0),
this.bgColor = Colors.white,
this.selectedBgColor = Colors.blue,
this.selectedBorderColor = Colors.blue,
this.tagStyle,
this.selectedTagStyle,
this.closeIconColor = const Color(0xFF999999),
this.selectedCloseIconColor = Colors.white,
this.closeIconSize = 16.0,
// 布局配置
this.spacing = 8.0,
this.runSpacing = 8.0,
this.mainAxisAlignment = MainAxisAlignment.start,
// 适配配置
this.adaptDarkMode = true,
}) : assert(tags.isNotEmpty, "标签列表不可为空"),
assert(initialSelected.every(tags.contains), "初始选中标签必须在标签列表中");
@override
State<TagWidget> createState() => _TagWidgetState();
}
class _TagWidgetState extends State<TagWidget> {
late List<String> _selectedTags;
@override
void initState() {
super.initState();
// 初始化选中标签(去重)
_selectedTags = widget.initialSelected.toSet().toList();
// 单选模式下最多选中1个
if (!widget.isMultiple && _selectedTags.length > 1) {
_selectedTags = [_selectedTags.first];
}
}
/// 深色模式颜色适配
Color _adaptDarkMode(Color lightColor, Color darkColor) {
if (!widget.adaptDarkMode) return lightColor;
return MediaQuery.platformBrightnessOf(context) == Brightness.dark
? darkColor
: lightColor;
}
/// 标签点击逻辑
void _onTagTap(String tag) {
if (widget.isDisabled) return;
setState(() {
if (widget.isMultiple) {
// 多选:切换选中状态
_selectedTags.contains(tag)
? _selectedTags.remove(tag)
: _selectedTags.add(tag);
} else {
// 单选:直接替换选中标签
_selectedTags = [tag];
}
// 触发选中回调
widget.onSelected(List.unmodifiable(_selectedTags));
});
}
/// 标签关闭逻辑
void _onTagClose(String tag, TapDownDetails details) {
details.stopPropagation(); // 阻止关闭事件触发标签点击
if (widget.isDisabled) return;
setState(() {
// 移除标签并更新选中状态
widget.tags.remove(tag);
_selectedTags.remove(tag);
widget.onSelected(List.unmodifiable(_selectedTags));
});
}
/// 构建单个标签
Widget _buildSingleTag(String tag) {
final isSelected = _selectedTags.contains(tag);
final isDisabled = widget.isDisabled || (widget.disableUnselected && !isSelected);
// 深色模式适配颜色
final adaptedBorderColor = isSelected
? _adaptDarkMode(widget.selectedBorderColor, Colors.blueAccent)
: _adaptDarkMode(widget.borderColor, const Color(0xFF444444));
final adaptedBgColor = isSelected
? _adaptDarkMode(widget.selectedBgColor, const Color(0xFF3A5F88))
: _adaptDarkMode(widget.bgColor, const Color(0xFF333333));
final adaptedTagStyle = isSelected
? (widget.selectedTagStyle ?? TextStyle(
fontSize: 14,
color: _adaptDarkMode(Colors.white, Colors.white70),
))
: (widget.tagStyle ?? TextStyle(
fontSize: 14,
color: _adaptDarkMode(Colors.black87, Colors.white70),
));
final adaptedCloseIconColor = isSelected
? _adaptDarkMode(widget.selectedCloseIconColor, Colors.white)
: _adaptDarkMode(widget.closeIconColor, const Color(0xFF777777));
// 标签内容(文本+关闭图标)
final List<Widget> tagContent = [
Text(
tag,
style: adaptedTagStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
];
if (widget.isClosable) {
tagContent.addAll([
const SizedBox(width: 6),
GestureDetector(
onTapDown: (details) => _onTagClose(tag, details),
child: Icon(
Icons.close,
size: widget.closeIconSize,
color: adaptedCloseIconColor,
),
),
]);
}
return GestureDetector(
onTap: isDisabled ? null : () => _onTagTap(tag),
child: Container(
height: widget.tagHeight,
padding: widget.tagPadding,
decoration: BoxDecoration(
color: adaptedBgColor,
border: Border.all(color: adaptedBorderColor),
borderRadius: BorderRadius.circular(widget.borderRadius),
),
alignment: Alignment.center,
child: Row(
mainAxisSize: MainAxisSize.min,
children: tagContent,
),
),
);
}
@override
Widget build(BuildContext context) {
// 构建标签列表
final tagWidgets = widget.tags.map((tag) => _buildSingleTag(tag)).toList();
// 流式布局(支持最大行数限制)
return Wrap(
spacing: widget.spacing,
runSpacing: widget.runSpacing,
alignment: widget.mainAxisAlignment,
children: tagWidgets,
);
}
}
四、三大高频场景落地示例(直接复制到项目可用)
场景 1:多选标签(筛选分类 - 兴趣标签)
适用场景:用户兴趣选择、商品标签筛选、多条件筛选
dart
// 用户注册兴趣标签选择
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("选择兴趣标签(可多选)", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const SizedBox(height: 12),
TagWidget(
tags: const ["科技", "财经", "娱乐", "体育", "健康", "教育", "汽车", "旅行"],
onSelected: (selected) {
debugPrint('选中兴趣标签:$selected');
// 实际业务:保存用户兴趣标签
saveUserInterests(selected);
},
isMultiple: true,
initialSelected: const ["科技", "体育"],
tagHeight: 34,
borderRadius: 17,
bgColor: const Color(0xFFF8F8F8),
selectedBgColor: const Color(0xFFE6F7FF),
selectedBorderColor: Colors.blueAccent,
tagStyle: const TextStyle(fontSize: 14, color: Color(0xFF333333)),
selectedTagStyle: const TextStyle(fontSize: 14, color: Colors.blueAccent),
spacing: 10,
runSpacing: 10,
),
],
);
场景 2:单选标签(状态选择 - 订单状态)
适用场景:订单状态筛选、性别选择、单一条件筛选
dart
// 订单列表状态筛选
TagWidget(
tags: const ["全部", "待付款", "待发货", "待收货", "已完成"],
onSelected: (selected) {
debugPrint('选中订单状态:${selected.first}');
// 实际业务:筛选对应状态的订单
filterOrdersByStatus(selected.first);
},
isMultiple: false,
initialSelected: const ["全部"],
tagHeight: 36,
borderRadius: 4,
borderColor: const Color(0xFFE0E0E0),
selectedBgColor: Colors.orangeAccent,
selectedBorderColor: Colors.orangeAccent,
tagStyle: const TextStyle(fontSize: 14, color: Color(0xFF666666)),
selectedTagStyle: const TextStyle(fontSize: 14, color: Colors.white),
spacing: 12,
mainAxisAlignment: MainAxisAlignment.center,
);
场景 3:可关闭标签(已选标签展示)
适用场景:已选筛选条件展示、标签编辑、动态标签管理
dart
// 商品筛选已选标签展示
Column(
children: [
const Text("已选筛选条件", style: TextStyle(fontSize: 14, color: Color(0xFF999999))),
const SizedBox(height: 8),
TagWidget(
tags: _selectedFilters, // 已选筛选条件列表(如["价格<500", "热销"])
onSelected: (selected) {
// 多选模式下,关闭标签后触发回调
debugPrint('当前选中筛选条件:$selected');
_selectedFilters = selected;
// 重新筛选商品
filterGoods(_selectedFilters);
},
isMultiple: true,
isClosable: true,
initialSelected: _selectedFilters,
tagHeight: 30,
borderRadius: 15,
bgColor: const Color(0xFFF0F7FF),
borderColor: Colors.blueAccent.withOpacity(0.3),
tagStyle: const TextStyle(fontSize: 13, color: Colors.blueAccent),
closeIconColor: Colors.blueAccent,
spacing: 8,
),
],
);
五、核心封装技巧(复用成熟设计思路)
- 选中状态内置管理:组件内部维护选中标签列表,无需外部状态管理,简化使用流程
- 流式布局适配:基于
Wrap实现标签自动换行,支持间距配置,适配不同屏幕宽度 - 事件隔离:可关闭标签的关闭事件通过
stopPropagation阻止触发标签点击,交互更精准 - 样式分层配置:普通 / 选中状态样式独立配置,支持细粒度自定义,兼顾通用性与个性化
- 边界条件处理:初始选中标签去重、单选模式限制选中数量、禁用状态自动切换样式,避免 UI 异常
六、避坑指南(解决 90% 开发痛点)
- 标签唯一性:确保
tags列表中标签唯一,避免选中状态混乱(尤其是多选模式) - 初始选中校验:
initialSelected中的标签必须存在于tags中,否则不生效 - 可关闭标签注意:
isClosable为 true 时,标签会自动从列表中移除,需确保tags是可变列表(如List<String>而非const List<String>) - 深色模式兼容:自定义颜色时通过
_adaptDarkMode方法适配,避免颜色冲突导致不可见 - 布局高度控制:标签数量过多时,可通过
maxLines限制最大行数,配合SingleChildScrollView实现滚动,避免布局溢出 - 禁用状态逻辑:
disableUnselected为 true 时,仅已选中标签可点击,适用于 “已选条件不可取消” 等场景
更多推荐

所有评论(0)