在这里插入图片描述

前言

标签选择是商城应用中常见的交互组件,用于商品规格选择、筛选条件选择、兴趣标签选择等场景。一个设计良好的标签选择组件需要支持单选和多选模式,并提供清晰的选中状态反馈。本文将详细介绍如何在Flutter和OpenHarmony平台上开发标签选择组件。

标签选择的设计需要考虑标签的布局方式、选中状态的视觉表现、以及不同选择模式的交互逻辑。用户应该能够快速识别哪些标签已被选中,并方便地进行选择和取消操作。

Flutter标签数据模型

首先定义标签数据的模型结构:

class TagItem {
  final String id;
  final String label;
  final bool enabled;
  final dynamic value;

  const TagItem({
    required this.id,
    required this.label,
    this.enabled = true,
    this.value,
  });
}

enum TagSelectionMode {
  single,   // 单选
  multiple, // 多选
}

TagItem类定义了标签的基本属性。id是标签的唯一标识符,label是显示文字,enabled控制标签是否可选,value是标签携带的业务数据。TagSelectionMode枚举定义了单选和多选两种模式。这种数据模型的设计支持各种标签选择场景,具有良好的通用性。

标签选择组件

class TagSelector extends StatelessWidget {
  final List<TagItem> tags;
  final Set<String> selectedIds;
  final TagSelectionMode mode;
  final ValueChanged<Set<String>>? onChanged;
  final double spacing;
  final double runSpacing;

  const TagSelector({
    Key? key,
    required this.tags,
    required this.selectedIds,
    this.mode = TagSelectionMode.single,
    this.onChanged,
    this.spacing = 10,
    this.runSpacing = 10,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return Wrap(
      spacing: spacing,
      runSpacing: runSpacing,
      children: tags.map((tag) => _buildTag(tag)).toList(),
    );
  }
}

TagSelector组件使用Wrap实现标签的流式布局,当一行放不下时自动换行。selectedIds使用Set存储选中的标签ID,支持多选场景。mode控制单选或多选模式,spacing和runSpacing分别设置水平和垂直间距。onChanged回调在选择变化时触发,将新的选中集合传递给父组件。这种设计将选择状态的管理交给父组件,组件本身保持无状态。

标签项组件:

Widget _buildTag(TagItem tag) {
  final isSelected = selectedIds.contains(tag.id);
  final isEnabled = tag.enabled;
  
  return GestureDetector(
    onTap: isEnabled ? () => _handleTap(tag) : null,
    child: Container(
      padding: const EdgeInsets.symmetric(
        horizontal: 16,
        vertical: 8,
      ),
      decoration: BoxDecoration(
        color: _getBackgroundColor(isSelected, isEnabled),
        borderRadius: BorderRadius.circular(4),
        border: Border.all(
          color: _getBorderColor(isSelected, isEnabled),
        ),
      ),
      child: Text(
        tag.label,
        style: TextStyle(
          fontSize: 13,
          color: _getTextColor(isSelected, isEnabled),
        ),
      ),
    ),
  );
}

每个标签项根据选中状态和启用状态显示不同样式。Container设置内边距、背景色、圆角和边框。GestureDetector处理点击事件,禁用的标签点击无响应。_getBackgroundColor、_getBorderColor、_getTextColor方法根据状态返回对应的颜色值。这种设计让用户能够清晰地识别标签的当前状态。

颜色获取方法:

Color _getBackgroundColor(bool isSelected, bool isEnabled) {
  if (!isEnabled) return const Color(0xFFFAFAFA);
  if (isSelected) return const Color(0xFFFFF0F0);
  return const Color(0xFFF5F5F5);
}

Color _getBorderColor(bool isSelected, bool isEnabled) {
  if (!isEnabled) return const Color(0xFFEEEEEE);
  if (isSelected) return const Color(0xFFE53935);
  return Colors.transparent;
}

Color _getTextColor(bool isSelected, bool isEnabled) {
  if (!isEnabled) return const Color(0xFFCCCCCC);
  if (isSelected) return const Color(0xFFE53935);
  return const Color(0xFF666666);
}

三个颜色方法分别处理背景色、边框色和文字颜色。禁用状态使用最浅的灰色系,选中状态使用红色系突出显示,未选中状态使用中等灰色。选中标签使用红色边框和浅红色背景,形成明显的视觉区分。这种颜色编码帮助用户快速识别选中的标签。

点击处理逻辑

void _handleTap(TagItem tag) {
  Set<String> newSelectedIds;
  
  if (mode == TagSelectionMode.single) {
    if (selectedIds.contains(tag.id)) {
      newSelectedIds = {};
    } else {
      newSelectedIds = {tag.id};
    }
  } else {
    newSelectedIds = Set.from(selectedIds);
    if (selectedIds.contains(tag.id)) {
      newSelectedIds.remove(tag.id);
    } else {
      newSelectedIds.add(tag.id);
    }
  }
  
  onChanged?.call(newSelectedIds);
}

_handleTap方法根据选择模式处理点击逻辑。单选模式下,点击已选中的标签会取消选择,点击未选中的标签会选中并取消其他选择。多选模式下,点击会切换标签的选中状态,不影响其他标签。方法创建新的Set对象而不是修改原对象,确保状态的不可变性。这种设计符合用户对单选和多选的认知习惯。

带标题的标签组

class TagGroup extends StatelessWidget {
  final String title;
  final List<TagItem> tags;
  final Set<String> selectedIds;
  final TagSelectionMode mode;
  final ValueChanged<Set<String>>? onChanged;

  const TagGroup({
    Key? key,
    required this.title,
    required this.tags,
    required this.selectedIds,
    this.mode = TagSelectionMode.single,
    this.onChanged,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          title,
          style: const TextStyle(
            fontSize: 14,
            fontWeight: FontWeight.w500,
            color: Color(0xFF333333),
          ),
        ),
        const SizedBox(height: 12),
        TagSelector(
          tags: tags,
          selectedIds: selectedIds,
          mode: mode,
          onChanged: onChanged,
        ),
      ],
    );
  }
}

TagGroup组件在标签选择器上方添加标题,适用于商品规格选择等需要标识分组的场景。Column垂直排列标题和标签选择器,crossAxisAlignment设为start使内容左对齐。标题使用14像素中等字重,与标签形成视觉层级。这种设计让用户能够清楚地知道当前选择的是什么类型的标签。

OpenHarmony标签选择实现

@Component
struct TagSelector {
  @Prop tags: TagItemInfo[] = []
  @Prop selectedIds: Set<string> = new Set()
  @Prop mode: string = 'single'
  private onChanged: (ids: Set<string>) => void = () => {}

  build() {
    Flex({ wrap: FlexWrap.Wrap }) {
      ForEach(this.tags, (tag: TagItemInfo) => {
        this.TagItem(tag)
      })
    }
    .width('100%')
  }
}

OpenHarmony的标签选择器使用Flex组件配合FlexWrap.Wrap实现流式布局。@Prop装饰的属性从父组件接收数据,包括标签列表、选中ID集合和选择模式。ForEach遍历标签数组,为每个标签生成TagItem组件。这种实现方式与Flutter版本结构一致。

标签数据接口:

interface TagItemInfo {
  id: string
  label: string
  enabled: boolean
  value?: any
}

TypeScript接口定义了标签的数据结构。enabled使用boolean类型控制标签是否可选,value使用any类型支持任意业务数据。接口定义为组件提供了类型安全保障。

标签项ArkUI实现

@Builder
TagItem(tag: TagItemInfo) {
  Text(tag.label)
    .fontSize(13)
    .fontColor(this.getTextColor(tag))
    .padding({ left: 16, right: 16, top: 8, bottom: 8 })
    .backgroundColor(this.getBackgroundColor(tag))
    .borderRadius(4)
    .border({
      width: 1,
      color: this.getBorderColor(tag)
    })
    .margin({ right: 10, bottom: 10 })
    .onClick(() => {
      if (tag.enabled) {
        this.handleTap(tag)
      }
    })
}

getBackgroundColor(tag: TagItemInfo): string {
  if (!tag.enabled) return '#FAFAFA'
  if (this.selectedIds.has(tag.id)) return '#FFF0F0'
  return '#F5F5F5'
}

getTextColor(tag: TagItemInfo): string {
  if (!tag.enabled) return '#CCCCCC'
  if (this.selectedIds.has(tag.id)) return '#E53935'
  return '#666666'
}

getBorderColor(tag: TagItemInfo): string {
  if (!tag.enabled) return '#EEEEEE'
  if (this.selectedIds.has(tag.id)) return '#E53935'
  return 'transparent'
}

@Builder装饰器定义了标签项的构建方法。Text组件显示标签文字,通过链式调用设置样式。margin设置右边距和下边距实现标签间隔。onClick事件在标签可用时处理点击。三个颜色方法根据标签状态返回对应的颜色值,与Flutter版本逻辑一致。

点击处理ArkUI实现:

handleTap(tag: TagItemInfo) {
  let newSelectedIds = new Set(this.selectedIds)
  
  if (this.mode === 'single') {
    if (this.selectedIds.has(tag.id)) {
      newSelectedIds = new Set()
    } else {
      newSelectedIds = new Set([tag.id])
    }
  } else {
    if (this.selectedIds.has(tag.id)) {
      newSelectedIds.delete(tag.id)
    } else {
      newSelectedIds.add(tag.id)
    }
  }
  
  this.onChanged(newSelectedIds)
}

handleTap方法处理标签点击逻辑。单选模式下点击切换选中状态,多选模式下切换当前标签的选中状态。创建新的Set对象确保状态不可变性。这种实现方式与Flutter版本功能完全一致。

可展开的标签组

class ExpandableTagGroup extends StatefulWidget {
  final String title;
  final List<TagItem> tags;
  final Set<String> selectedIds;
  final ValueChanged<Set<String>>? onChanged;
  final int initialVisibleCount;

  const ExpandableTagGroup({
    Key? key,
    required this.title,
    required this.tags,
    required this.selectedIds,
    this.onChanged,
    this.initialVisibleCount = 6,
  }) : super(key: key);

  
  State<ExpandableTagGroup> createState() => _ExpandableTagGroupState();
}

class _ExpandableTagGroupState extends State<ExpandableTagGroup> {
  bool _isExpanded = false;

  
  Widget build(BuildContext context) {
    final visibleTags = _isExpanded 
      ? widget.tags 
      : widget.tags.take(widget.initialVisibleCount).toList();
    final hasMore = widget.tags.length > widget.initialVisibleCount;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(widget.title, style: const TextStyle(fontSize: 14)),
            if (hasMore)
              GestureDetector(
                onTap: () => setState(() => _isExpanded = !_isExpanded),
                child: Text(
                  _isExpanded ? '收起' : '展开',
                  style: const TextStyle(
                    fontSize: 12,
                    color: Color(0xFF999999),
                  ),
                ),
              ),
          ],
        ),
        const SizedBox(height: 12),
        TagSelector(
          tags: visibleTags,
          selectedIds: widget.selectedIds,
          onChanged: widget.onChanged,
        ),
      ],
    );
  }
}

ExpandableTagGroup组件支持标签的展开和收起,适用于标签数量较多的场景。initialVisibleCount设置初始显示的标签数量,超出部分需要点击展开查看。_isExpanded状态控制是否展开,hasMore判断是否需要显示展开按钮。这种设计在保持界面简洁的同时提供了完整的选择功能。

总结

本文详细介绍了Flutter和OpenHarmony平台上标签选择组件的开发过程。标签选择作为商城应用的常用组件,其设计质量直接影响用户的选择体验。通过单选多选模式、状态视觉反馈、可展开标签组等功能的合理设计,我们为用户提供了便捷直观的标签选择体验。在实际项目中,还可以进一步添加标签搜索、标签排序、自定义标签等功能。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐