Flutter + OpenHarmony 标签选择器组件开发实战
Flutter OpenHarmony 标签选择器组件开发实战欢迎加入开源鸿蒙跨平台社区→ https://openharmonycrosplatform.csdn.net一、效果展示 运行效果预览在鸿蒙虚拟机上运行后的实际效果如下基础标签展示 四个不同颜色的标签Flutter主题色、Dart蓝色、鸿蒙红色、OpenHarmony橙色圆角矩形外观默认填充样式点击时有缩放动画效果三种标签样式对比 填充样式 纯色背景白色文字描边样式 透明背景边框黑色文字渐变样式 主题色渐变背景白色文字三种尺寸规格 小尺寸 字号12px内边距8×4中尺寸 字号14px内边距12×6大尺寸 字号16px内边距16×8带图标标签 热门橙色火焰图标⭐ 推荐金色星星图标 新品绿色新图标可删除标签 每个标签右侧显示×关闭按钮点击×按钮触发删除回调适用于标签输入场景多选标签选择器 6个职位标签前端、后端、移动端、全栈、UI设计、产品经理默认选中前端和移动端顶部显示已选择 2 项计数点击标签切换选中状态单选标签选择器 4个筛选标签最新、热门、推荐、关注只能选中一个点击自动切换使用填充样式选中效果明显标签输入功能 预设标签Flutter、鸿蒙输入框提示输入技术标签输入后按回车或点击按钮添加点击标签×按钮删除 三种样式对比图示填充样式 ┌──────────────┐ │ Flutter │ ← 纯色背景 白色文 字 └──────────────┘ 描边样式 ╔══════════════╗ ║ Flutter ║ ← 透明背景 边框 黑色文字 ╚══════════════╝ 渐变样式 ┌──────────────┐ │▓▓ Flutter ▓▓│ ← 渐变背景 白色文字 └──────────────┘二、组件概述标签是现代应用界面中最常见的元素之一用于分类、筛选、标记内容。一个优秀的标签组件需要支持多种视觉样式、交互模式和自定义能力。在 OpenHarmony 环境下开发 Flutter 应用时标签选择器组件可以广泛应用于文章分类、商品筛选、技能标签、用户标签等场景。本文将详细介绍如何在 Flutter OpenHarmony 项目中实现一个功能完善的标签选择器组件系统包括单个标签、标签选择器和标签输入框三个核心组件。三、核心功能特性本组件专为鸿蒙生态设计具备以下核心优势✅ 三种视觉样式 - 填充、描边、渐变覆盖所有设计风格✅ 三种尺寸规格 - 小、中、大适配不同布局密度✅ 单选/多选模式 - 灵活的选择策略支持最大数量限制✅ 图标支持 - 前置图标、尾部图标、删除图标✅ 可删除标签 - 一键移除适用于标签输入场景✅ 标签输入功能 - 文本输入标签展示的组合组件✅ 流畅动画效果 - 淡入缩放动画交互反馈自然四、技术实现架构4.1 标签样式枚举enum TagStyle { filled, // 填充样式 - 纯色背景 outlined, // 描边样式 - 边框透明背景 gradient // 渐变样式 - 渐变背景 }4.2 标签尺寸枚举enum TagSize { small, // 小尺寸 - 紧凑布局 medium, // 中尺寸 - 默认选择 large // 大尺寸 - 强调显示 }4.3 组件体系架构CustomTag - 单个标签组件 ├── label - 标签文本 ├── style - 视觉样式 ├── size - 尺寸规格 ├── icon - 前置图标 ├── onDeleted - 删除回调 └── selected - 选中状态 TagSelector - 标签选择器组件 ├── tags - 标签列表 ├── multiSelect - 多选模式 ├── maxSelected - 最大选择数 └── onSelectionChanged - 选择变化回调 TagInputField - 标签输入组件 ├── initialTags - 初始标签 ├── hintText - 输入提示 └── onTagsChanged - 标签变化回调五、CustomTag 单标签组件实现5.1 核心属性定义class CustomTag extends StatelessWidget { final String label; // 标签文本 final TagStyle style; // 视觉样式 final TagSize size; // 尺寸规格 final Color? color; // 自定义颜色 final Color? textColor; // 文字颜色 final IconData? icon; // 前置图标 final IconData? trailingIcon; // 尾部图标 final VoidCallback? onTap; // 点击回调 final VoidCallback? onDeleted; // 删除回调 final bool selected; // 选中状态 final bool enabled; // 是否启用 final double borderRadius; // 圆角半径 final Gradient? gradient; // 渐变配置 const CustomTag({ super.key, required this.label, this.style TagStyle.filled, this.size TagSize.medium, // ... 其他参数 }); }5.2 内容构建实现override Widget build(BuildContext context) { final isDark Theme.of(context). brightness Brightness.dark; final themeColor color ?? Theme. of(context).colorScheme.primary; final tagPadding _getPadding(); final fontSize _getFontSize(); Widget tagContent Row( mainAxisSize: MainAxisSize.min, children: [ // 前置图标 if (icon ! null) ...[ Icon(icon, size: fontSize 2, color: _getTextColor (isDark, themeColor)), const SizedBox(width: 4), ], // 标签文本 Text( label, style: TextStyle( fontSize: fontSize, fontWeight: selected ? FontWeight.w600 : FontWeight.normal, color: _getTextColor (isDark, themeColor), ), ), // 尾部图标 if (trailingIcon ! null) ...[ const SizedBox(width: 4), Icon(trailingIcon, size: fontSize 2, color: _getTextColor(isDark, themeColor)), ], // 删除按钮 if (onDeleted ! null) ...[ const SizedBox(width: 4), GestureDetector( onTap: onDeleted, child: Icon(Icons.close, size: fontSize, color: _getTextColor(isDark, themeColor)), ), ], ], ); return GestureDetector( onTap: enabled ? onTap : null, child: AnimatedContainer( duration: const Duration (milliseconds: 200), padding: tagPadding, decoration: _getDecoration (isDark, themeColor), child: tagContent, ), ).animate().fadeIn(duration: 200. ms).scale(begin: const Offset(0. 9, 0.9), end: const Offset(1, 1)); }技术要点 Row(mainAxisSize: MainAxisSize.min) 让标签宽度自适应内容AnimatedContainer 实现选中状态切换的平滑过渡GestureDetector 包裹删除图标实现独立点击区域flutter_animate 添加淡入缩放动画5.3 尺寸映射实现EdgeInsetsGeometry _getPadding() { switch (size) { case TagSize.small: return const EdgeInsets. symmetric(horizontal: 8, vertical: 4); case TagSize.medium: return const EdgeInsets. symmetric(horizontal: 12, vertical: 6); case TagSize.large: return const EdgeInsets. symmetric(horizontal: 16, vertical: 8); } } double _getFontSize() { switch (size) { case TagSize.small: return 12; case TagSize.medium: return 14; case TagSize.large: return 16; } }5.4 文字颜色计算Color _getTextColor(bool isDark, Color themeColor) { // 禁用状态灰色 if (!enabled) return isDark ? Colors.grey[600]! : Colors.grey [400]!; // 描边样式选中时主题色未选中时黑 色/白色 if (style TagStyle.outlined) { return selected ? themeColor : (isDark ? Colors.white70 : Colors.black87); } // 填充样式白色文字 if (style TagStyle.filled) { return selected ? Colors.white : (textColor ?? Colors.white); } // 默认 return textColor ?? (isDark ? Colors.white : Colors.black87); }5.5 装饰器样式实现BoxDecoration _getDecoration(bool isDark, Color themeColor) { final baseColor selected ? themeColor : (color ?? (isDark ? Colors.grey[800]! : Colors.grey [200]!)); switch (style) { case TagStyle.filled: return BoxDecoration( color: enabled ? baseColor : (isDark ? Colors.grey [800] : Colors.grey[300]), borderRadius: BorderRadius. circular(borderRadius), ); case TagStyle.outlined: return BoxDecoration( color: selected ? themeColor.withOpacity(0.1) : Colors.transparent, border: Border.all( color: enabled ? (selected ? themeColor : (isDark ? Colors.grey[600]! : Colors.grey[400]!)) : (isDark ? Colors.grey [700]! : Colors.grey [300]!), width: selected ? 2 : 1, ), borderRadius: BorderRadius. circular(borderRadius), ); case TagStyle.gradient: return BoxDecoration( gradient: gradient ?? LinearGradient( colors: [themeColor, themeColor.withOpacity(0. 7)], ), borderRadius: BorderRadius. circular(borderRadius), ); } }样式特点 filled 纯色背景选中时主题色未选中时灰色outlined 透明背景边框选中时添加浅色背景gradient 渐变背景支持自定义渐变方案六、TagSelector 标签选择器实现6.1 组件属性定义class TagSelector extends StatefulWidget { final ListString tags; // 可选标签列表 final ListString? selectedTags; // 默认选中标签 final bool multiSelect; // 多选 模式 final TagStyle style; // 标签样式 final TagSize size; // 标签尺寸 final Color? color; // 标签颜 色 final Function(ListString)? onSelectionChanged; // 选择变化回调 final bool showCount; // 显示 选择计数 final int? maxSelected; // 最大 选择数量 const TagSelector({ super.key, required this.tags, this.selectedTags, this.multiSelect true, this.style TagStyle.outlined, this.size TagSize.medium, this.color, this.onSelectionChanged, this.showCount false, this.maxSelected, }); }6.2 选择逻辑实现class _TagSelectorState extends StateTagSelector { late ListString _selectedTags; override void initState() { super.initState(); _selectedTags widget. selectedTags ?? []; } void _toggleTag(String tag) { setState(() { if (_selectedTags.contains (tag)) { // 已选中则取消 _selectedTags.remove(tag); } else { // 检查最大选择数限制 if (widget.maxSelected ! null _selectedTags. length widget. maxSelected!) { return; } // 单选模式先清空 if (!widget.multiSelect) { _selectedTags.clear(); } // 添加选中 _selectedTags.add(tag); } // 回调通知 widget.onSelectionChanged?. call(_selectedTags); }); } }选择逻辑 已选中的标签点击取消选中未选中的标签点击添加选中单选模式先清空再添加最大数量限制检查状态变化通过回调通知父组件6.3 UI 渲染实现override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 选择计数提示 if (widget.showCount) Padding( padding: const EdgeInsets. only(bottom: 8), child: Text( 已选择 ${_selectedTags. length} 项, style: TextStyle (fontSize: 12, color: Colors.grey[600]), ), ), // 标签列表 Wrap( spacing: 8, // 水平间距 runSpacing: 8, // 垂直间距 children: widget.tags.map ((tag) { return CustomTag( label: tag, style: widget.style, size: widget.size, color: widget.color, selected: _selectedTags. contains(tag), onTap: () _toggleTag (tag), ); }).toList(), ), ], ); }布局特点 Wrap 组件实现自动换行spacing 和 runSpacing 控制间距每个标签独立管理选中状态七、TagInputField 标签输入组件实现7.1 组件属性定义class TagInputField extends StatefulWidget { final ListString initialTags; // 初始标签 final Function(ListString)? onTagsChanged; // 标签变化回调 final String? hintText; // 输入提示 final TagStyle style; // 标签样式 final Color? color; // 标签颜 色 const TagInputField({ super.key, this.initialTags const [], this.onTagsChanged, this.hintText, this.style TagStyle.outlined, this.color, }); }7.2 添加和删除逻辑class _TagInputFieldState extends StateTagInputField { final TextEditingController _controller TextEditingController(); final FocusNode _focusNode FocusNode(); late ListString _tags; override void initState() { super.initState(); _tags List.from(widget. initialTags); } // 添加标签 void _addTag() { final text _controller.text. trim(); if (text.isNotEmpty !_tags. contains(text)) { setState(() { _tags.add(text); _controller.clear(); // 清 空输入框 }); widget.onTagsChanged?.call (_tags); } } // 删除标签 void _removeTag(String tag) { setState(() { _tags.remove(tag); }); widget.onTagsChanged?.call (_tags); } override void dispose() { _controller.dispose(); _focusNode.dispose(); super.dispose(); } }防重复逻辑 检查文本是否为空检查标签是否已存在添加成功后清空输入框7.3 UI 渲染实现override Widget build(BuildContext context) { final isDark Theme.of(context). brightness Brightness.dark; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 已添加的标签 Wrap( spacing: 8, runSpacing: 8, children: _tags.map((tag) { return CustomTag( label: tag, style: widget.style, color: widget.color, onDeleted: () _removeTag(tag), ); }).toList(), ), const SizedBox(height: 8), // 输入框 TextField( controller: _controller, focusNode: _focusNode, decoration: InputDecoration( hintText: widget. hintText ?? 输入标签后按回 车添加, suffixIcon: IconButton( icon: const Icon(Icons. add, size: 20), onPressed: _addTag, ), // ... 边框样式 ), onSubmitted: (_) _addTag (), // 回车提交 ), ], ); }交互方式 输入文本后按回车键添加点击右侧按钮添加点击标签×按钮删除八、OpenHarmony 平台集成指南8.1 在鸿蒙项目中引入组件# 1. 将 tag_selector_widget.dart 文 件复制到 lib/widgets/ 目录 cp tag_selector_widget.dart lib/ widgets/ # 2. 在需要使用的页面导入 import package:demo1/widgets/ tag_selector_widget.dart;8.2 在设置页面添加入口// settings_page.dart 中添加导航项 _buildNavigationTile( isDark, icon: Icons.label_outline, title: 标签选择器, subtitle: Tag组件, onTap: () { Navigator.push( context, MaterialPageRoute(builder: (context) const TagPreviewPage()), ); }, ),8.3 在鸿蒙虚拟机运行测试# 连接鸿蒙虚拟设备 flutter devices # 运行应用 flutter run -d 127.0.0.1:5555九、使用示例集锦示例1基础标签展示Wrap( spacing: 8, runSpacing: 8, children: [ CustomTag(label: Flutter), CustomTag(label: Dart, color: Colors.blue), CustomTag(label: 鸿蒙, color: Colors.red), ], )运行效果 三个不同颜色的标签横向排列。示例2带图标的标签CustomTag( label: 热门, icon: Icons.local_fire_department, color: Colors.orange, )运行效果 橙色标签左侧显示火焰图标。示例3可删除标签CustomTag( label: 标签1, onDeleted: () { // 删除逻辑 }, )运行效果 标签右侧显示×按钮点击触发删除。示例4多选标签选择器TagSelector( tags: [前端, 后端, 移动端, 全 栈], selectedTags: [前端], multiSelect: true, style: TagStyle.outlined, showCount: true, maxSelected: 3, onSelectionChanged: (tags) { print(选中: $tags); }, )运行效果 最多可选3个标签显示选择计数。示例5单选标签选择器TagSelector( tags: [最新, 热门, 推荐], multiSelect: false, style: TagStyle.filled, onSelectionChanged: (tags) { print(选中: ${tags.first}); }, )运行效果 只能选中一个标签点击自动切换。示例6标签输入框TagInputField( initialTags: [Flutter, 鸿蒙], hintText: 输入技术标签, onTagsChanged: (tags) { print(当前标签: $tags); }, )运行效果 预设两个标签可输入添加新标签。十、性能优化策略10.1 渲染优化Wrap 组件 自动换行避免溢出mainAxisSize.min 标签宽度自适应内容AnimatedContainer 高效的状态过渡动画const 构造函数 减少不必要的重建10.2 状态管理优化late 初始化 延迟初始化 selectedTagsList.from 创建副本避免引用问题及时释放 TextEditingController 和 FocusNode 在 dispose 中释放10.3 交互优化防重复检查 添加标签时检查是否已存在最大数量限制 防止用户选择过多标签独立点击区域 删除按钮使用独立的 GestureDetector十一、常见问题解答Q1: 如何让标签不可点击设置 enabled: false 即可禁用标签。Q2: 如何自定义标签的圆角大小使用 borderRadius 参数CustomTag(label: 标签, borderRadius: 8)Q3: 如何限制标签选择器的最大选择数设置 maxSelected 参数TagSelector(tags: [...], maxSelected: 3)Q4: 如何获取当前选中的标签通过 onSelectionChanged 回调获取TagSelector( tags: [...], onSelectionChanged: (tags) { print(选中: $tags); }, )Q5: 如何实现标签的渐变效果使用 gradient 参数CustomTag( label: 渐变, style: TagStyle.gradient, gradient: LinearGradient(colors: [Colors.purple, Colors.blue]), )虚拟机运行十二、总结与展望本文详细介绍了如何在 Flutter OpenHarmony 环境中开发一套功能完善的标签选择器组件系统。该组件具备以下技术亮点 完整的功能矩阵 - 3种样式 × 3种尺寸 × 3种组件 27种组合 专业的视觉设计 - 填充、描边、渐变三种风格⚡ 卓越的性能表现 - 高效渲染 内存安全 极简的使用方式 - 声明式API零学习成本 完美的平台适配 - 自动适配亮暗主题实际应用价值 文章/商品分类标签技能/兴趣标签选择筛选条件标签用户标签管理搜索历史标签后续扩展方向 支持标签拖拽排序添加标签分组功能实现标签推荐系统支持自定义标签形状添加无障碍访问支持