You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 16:57:49