如何在Flutter中创建与图示效果一致的相似widget?
Flutter 实现多选标签筛选弹窗
目标实现效果如下:
实现思路
- 底部弹出容器用官方自带的
showModalBottomSheet实现,自带半透明遮罩、弹出收起动画,支持配置顶部圆角匹配设计 - 标签区域用
Wrap组件做流式布局,自动处理标签换行、横向纵向间距 - 单个标签自定义容器实现选中/未选中双状态,点击时通过
setState更新选中集合 - 底部固定全宽圆角确认按钮,点击后将选中的标签列表返回给调用页面
- 自动适配设备底部安全区,避免按钮被系统栏遮挡
完整可运行代码
1. 弹窗调用示例
// 任意业务页面中触发弹窗 void _openStylePicker(BuildContext context) async { // 等待弹窗返回选中结果 final List<String>? selectedResult = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) => const TagPicker( tagList: ["Formal", "Informal", "Trendy", "Casual", "Edgy", "Bohemian", "Classic", "Minimalist", "Streetwear", "Preppy", "Sporty", "Vintage"], ), ); // 后续业务逻辑处理选中结果 if (selectedResult != null) { debugPrint("当前选中的风格标签:$selectedResult"); } }
2. 标签选择组件核心实现
class TagPicker extends StatefulWidget { /// 全部可选标签列表 final List<String> tagList; /// 默认选中的标签 final List<String>? initialSelected; const TagPicker({ super.key, required this.tagList, this.initialSelected, }); @override State<TagPicker> createState() => _TagPickerState(); } class _TagPickerState extends State<TagPicker> { late final Set<String> _selectedSet; @override void initState() { super.initState(); _selectedSet = Set.from(widget.initialSelected ?? []); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(20, 24, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // 组件标题 const Text( "Select preferred styles", style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, color: Colors.black87 ), ), const SizedBox(height: 24), // 标签流式区域 Wrap( spacing: 10, // 横向间距 runSpacing: 10, // 纵向间距 children: widget.tagList.map((tag) { final bool selected = _selectedSet.contains(tag); return GestureDetector( onTap: () { setState(() { selected ? _selectedSet.remove(tag) : _selectedSet.add(tag); }); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 9), decoration: BoxDecoration( color: selected ? Colors.blue : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: selected ? Colors.blue : Colors.grey.shade300, width: 1 ), ), child: Text( tag, style: TextStyle( fontSize: 14, color: selected ? Colors.white : Colors.black87 ), ), ), ); }).toList(), ), const SizedBox(height: 32), // 确认按钮 SizedBox( width: double.infinity, height: 48, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24) ) ), onPressed: () => Navigator.pop(context, _selectedSet.toList()), child: const Text( "APPLY", style: TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w500 ), ), ), ), // 适配底部安全区 SizedBox(height: MediaQuery.of(context).padding.bottom) ], ), ); } }
自定义调整说明
- 要改成单选模式:把存储选中状态的
Set<String>换成单个String?类型变量,点击标签时直接替换选中值即可 - 样式自定义:直接修改标签Container的边框、颜色、圆角参数,以及按钮的色值、尺寸,匹配自己的设计稿
- 标签过多适配:如果可选标签数量很大,可以在Wrap外层嵌套
SingleChildScrollView,同时给Column设置constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.7)避免弹窗过高溢出 - 增加交互:可以在标题栏右侧加关闭按钮、顶部加拖拽条,或者加标签全选/清空功能,直接在Column的children里追加对应组件即可
内容的提问来源于stack exchange,提问作者RuTiKeyOne
相关产品推荐
相关产品推荐

