在 Flutter 开发中,标签(Tag)是筛选分类、标签展示、状态标记的高频组件。原生无现成标签组件,重复开发易导致样式混乱、布局适配差。本文封装的 TagWidget 整合 “单选 / 多选 + 流式布局 + 可关闭标签 + 全样式自定义” 四大核心能力,适配标签筛选、标签展示、状态标记等场景,一行代码即可集成。

一、核心优势(精准解决开发痛点)

  1. 单选 / 多选自由切换:支持单选(互斥选中)、多选(任意组合),内置选中状态管理,无需外部控制
  2. 流式布局自动适配:标签自动换行,支持最大行数限制,适配不同屏幕宽度与标签数量
  3. 样式全维度自定义:标签颜色、圆角、边框、文本样式、选中状态样式均可配置,贴合 APP 主题
  4. 功能灵活扩展:支持可关闭标签(带删除图标)、禁用状态、自定义标签内容(图标 + 文本)
  5. 适配性强体验优:自动适配深色模式,标签点击区域充足,选中状态反馈清晰

二、核心配置速览(关键参数一目了然)

配置分类 核心参数 核心作用
必选配置 tags: List<String>onSelected: Function(List<String>) 标签列表、选中回调(多选返回列表,单选返回单个元素列表)
功能配置 isMultipleinitialSelectedisClosablemaxLinesisDisabled 单选 / 多选、初始选中、是否可关闭、最大行数、整体禁用
样式配置 tagHeighttagPaddingborderRadiustagStyleselectedStyle 标签高度、内边距、圆角、文本样式、选中样式
适配配置 adaptDarkModespacingrunSpacingdisabledColor 深色模式适配、标签间距、行间距、禁用颜色

三、生产级完整代码(可直接复制,开箱即用)

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,
    ),
  ],
);

五、核心封装技巧(复用成熟设计思路)

  1. 选中状态内置管理:组件内部维护选中标签列表,无需外部状态管理,简化使用流程
  2. 流式布局适配:基于 Wrap 实现标签自动换行,支持间距配置,适配不同屏幕宽度
  3. 事件隔离:可关闭标签的关闭事件通过 stopPropagation 阻止触发标签点击,交互更精准
  4. 样式分层配置:普通 / 选中状态样式独立配置,支持细粒度自定义,兼顾通用性与个性化
  5. 边界条件处理:初始选中标签去重、单选模式限制选中数量、禁用状态自动切换样式,避免 UI 异常

六、避坑指南(解决 90% 开发痛点)

  1. 标签唯一性:确保 tags 列表中标签唯一,避免选中状态混乱(尤其是多选模式)
  2. 初始选中校验initialSelected 中的标签必须存在于 tags 中,否则不生效
  3. 可关闭标签注意isClosable 为 true 时,标签会自动从列表中移除,需确保 tags 是可变列表(如 List<String> 而非 const List<String>
  4. 深色模式兼容:自定义颜色时通过 _adaptDarkMode 方法适配,避免颜色冲突导致不可见
  5. 布局高度控制:标签数量过多时,可通过 maxLines 限制最大行数,配合 SingleChildScrollView 实现滚动,避免布局溢出
  6. 禁用状态逻辑disableUnselected 为 true 时,仅已选中标签可点击,适用于 “已选条件不可取消” 等场景

https://openharmonycrossplatform.csdn.net/content

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐