一、引言

在移动端 UI 中,角标(Badge)是最常见的信息提示元素之一。无论是微信的消息红点、淘宝的购物车数量、抖音的未读通知,还是 App 图标上的更新提示——角标以最小的视觉空间传递最关键的状态信息。一个红色圆点或白色数字,就能让用户在 0.1 秒内感知到"有新内容需要关注"。

在传统开发中,实现角标需要手动叠加组件:用 Stack 容器将角标视图定位到目标组件的右上角,手动计算偏移量,手动处理数字格式化(99+),手动管理显示/隐藏逻辑。看似简单的角标,实际写起来却是一堆布局代码和条件判断。

HarmonyOS NEXT 提供了 Badge 组件——一个专为角标场景设计的声明式组件。开发者只需指定角标类型(数字/圆点/文本)、样式和位置,Badge 会自动完成定位、格式化和管理。本文通过一个消息通知中心 Demo 深入讲解 Badge 的核心用法。

阅读完本文,你将能够:

  • 使用 Badge 实现数字角标、圆点角标和文本角标
  • 掌握 BadgePosition 的位置控制
  • 理解 badgeColorbadgeSizefontSize 等样式参数
  • 实现未读消息计数和全部已读功能
  • 理解 Badge 组件在消息系统中的实际应用模式

二、Badge 组件 API 总览

2.1 构造函数

Badge(options: BadgeOptions)

Badge 通过构造函数接收配置对象,所有参数都通过 BadgeOptions 传入:

interface BadgeOptions {
  value: string;       // 角标显示内容,空字符串表示圆点模式
  style?: BadgeStyle;  // 角标样式配置
  position?: BadgePosition;  // 角标位置
}

2.2 BadgeStyle 样式配置

interface BadgeStyle {
  badgeColor?: ResourceColor;  // 角标背景颜色
  badgeSize?: number;          // 角标大小(圆点模式下的直径)
  fontSize?: number;           // 数字/文本模式下字体大小
  fontColor?: ResourceColor;   // 角标文字颜色
  fontWeight?: number | FontWeight;  // 字重
}

关键参数说明:

参数 用途 默认值 说明
badgeColor 角标背景色 红色 数字和圆点角标的背景填充色
badgeSize 圆点直径 6vp 仅在 value: ''(圆点模式)时生效
fontSize 文字大小 10vp 影响数字角标的数字大小
fontColor 文字颜色 白色 角标文字的颜色

2.3 BadgePosition 位置枚举

enum BadgePosition {
  RightTop,      // 右上角(默认)
  Right,         // 右侧居中
  Left,          // 左侧居中
  LeftTop,       // 左上角
  LeftBottom,    // 左下角
  RightBottom    // 右下角
}

最常用的是 BadgePosition.RightTop——大多数应用的消息角标放在头像右上角。

2.4 三种角标模式

根据 value 参数的不同,Badge 有三种显示模式:

// 数字角标 — value 为非空字符串
Badge({ value: '99+', style: { badgeColor: '#FF3B30', fontSize: 10 }, position: BadgePosition.RightTop })

// 圆点角标 — value 为空字符串
Badge({ value: '', style: { badgeColor: '#FF3B30', badgeSize: 8 }, position: BadgePosition.RightTop })

// 文本角标 — value 为非数字字符串
Badge({ value: 'NEW', style: { badgeColor: '#9C27B0', fontSize: 9 }, position: BadgePosition.RightTop })

三种模式的区分逻辑:

  • value 为空字符串 → 圆点角标,显示为指定 badgeSize 的圆形色块
  • value 为非空字符串 → 内容角标,显示指定文字,背景为 badgeColor 色块
  • 数字角标是内容角标的特例——value 为数字字符串,通常配合 fontSize 控制大小
    在这里插入图片描述

三、Demo 设计:消息通知中心

3.1 功能概述

Demo 是一个消息通知中心页面,模拟类微信消息列表的交互体验:

  1. 角标类型展示区:3 个演示卡片,分别展示数字角标、圆点角标和文本角标的效果
  2. 消息列表区:8 条模拟消息,每条消息根据状态显示不同类型的角标
  3. 未读统计:顶部实时显示未读消息总数
  4. 单条已读:点击消息将该条消息标记为已读
  5. 全部已读:一键清空所有未读状态

3.2 消息数据结构

每条消息用一个接口定义,包含标题、内容、时间、未读状态和角标信息:

interface MessageItem {
  id: number;
  title: string;
  content: string;
  time: string;
  unread: boolean;
  count: number;   // 未读数(>0 时显示数字角标)
  dot: boolean;     // 是否显示圆点角标(未读但无具体数量时)
  tag: string;      // 消息分类标签
}

角标显示逻辑:

  • 消息未读 + count > 0 → 显示数字角标(如"3"、“5”)
  • 消息未读 + dot = true → 显示圆点角标(适合"有人关注你"这类无法计数的消息)
  • 消息已读 → 不显示任何角标

这种设计覆盖了消息系统中两种典型的未读场景:可计数的(如"3 条回复")和不可计数的(如"有人关注了你")。

3.3 角标类型演示

页面顶部有 3 个演示卡片,用 Stack + Badge 组合展示三种角标:

@Builder
badgeDemo(label: string, col: string, mode: string) {
  Column() {
    Stack() {
      // 56×56 的占位方块
      Column()
        .width(56).height(56)
        .borderRadius(BorderRadius.MD)
        .backgroundColor('#F2F3F5')

      if (mode === 'count') {
        Badge({ value: '99+', style: { badgeColor: col, fontSize: 10 }, position: BadgePosition.RightTop })
          .width(56).height(56)
      } else if (mode === 'dot') {
        Badge({ value: '', style: { badgeColor: col, badgeSize: 8 }, position: BadgePosition.RightTop })
          .width(56).height(56)
      } else {
        Badge({ value: 'NEW', style: { badgeColor: col, fontSize: 9 }, position: BadgePosition.RightTop })
          .width(56).height(56)
      }
    }

    Text(label)
      .fontSize(11).fontColor('#9999AA')
      .margin({ top: 6 })
  }
  .alignItems(HorizontalAlign.Center)
  .width('30%')
}

关键点:Badge 必须放在 Stack 容器中才能正确定位。Badge 在 Stack 内部通过 position 参数确定相对于 Stack 的位置。Badge 的 width/height 应设置为与下方组件相同的尺寸,以确保定位基准正确。

3.4 消息列表中的条件角标

在消息列表中,每条消息的头像区域使用条件渲染来决定显示哪种角标:

Stack() {
  // 头像占位区域
  Column()
    .width(44).height(44)
    .borderRadius(BorderRadius.FULL)
    .backgroundColor(this.tagColor(item.tag) + '20')

  Text(item.tag)
    .fontSize(11)
    .fontColor(this.tagColor(item.tag))
    .fontWeight(FontWeight.Bold)

  // 条件角标渲染
  if (item.unread && item.count > 0) {
    Badge({ value: '' + item.count, style: { badgeColor: '#FF3B30', fontSize: 10 }, position: BadgePosition.RightTop })
      .width(44).height(44)
  } else if (item.unread && item.dot) {
    Badge({ value: '', style: { badgeColor: '#FF3B30', badgeSize: 8 }, position: BadgePosition.RightTop })
      .width(44).height(44)
  }
}

注意细节:

  • '' + item.count — 将 number 转为 string,Badge 的 value 只接受 string 类型
  • 条件判断链 if/else if — 数字角标优先于圆点角标,count > 0 时不显示圆点
  • Badge 的宽高设置为 44×44 与头像一致,让角标定位在头像区域的右上角

3.5 标记已读的不可变更新

单条标记已读使用不可变数组更新模式:

markAsRead(id: number): void {
  const newList: MessageItem[] = [];
  for (let i = 0; i < this.messages.length; i++) {
    const m = this.messages[i];
    if (m.id === id) {
      newList.push({
        id: m.id, title: m.title, content: m.content,
        time: m.time, unread: false, count: 0,
        dot: false, tag: m.tag
      });
    } else {
      newList.push(m);
    }
  }
  this.messages = newList;
  this.countTotal();
}

在 ArkTS 中,@State 变量的更新检测基于引用比较。直接修改 this.messages[i].unread = false 不会触发 UI 刷新。必须创建新数组(包含修改后的元素副本),再将新数组赋值给 this.messages

countTotal() 在每次标记已读后重新统计未读数——遍历所有消息,将未读消息的 count(或至少 1 条)累加到 unreadTotal

3.6 全部已读功能

markAllRead(): void {
  const newList: MessageItem[] = [];
  for (let i = 0; i < this.messages.length; i++) {
    const m = this.messages[i];
    newList.push({
      id: m.id, title: m.title, content: m.content,
      time: m.time, unread: false, count: 0,
      dot: false, tag: m.tag
    });
  }
  this.messages = newList;
  this.unreadTotal = 0;
}

全部已读比单条已读更简单——遍历所有消息,创建 unread/count/dot 全部置为 false/0 的副本,赋值触发刷新。由于已知结果为 0,所以直接赋 this.unreadTotal = 0 而无需调用 countTotal()

3.7 页面结构

┌──────────────────────────────────────────┐
│ 📬 消息中心(深色标题栏)                  │
├──────────────────────────────────────────┤
│ 📘 Badge 组件说明卡片                     │
├──────────────────────────────────────────┤
│ 未读消息 N 条            全部已读         │
├──────────────────────────────────────────┤
│ ┌────────────────────────────────────┐   │
│ │ 角标类型                            │   │
│ │ ┌────────┐ ┌────────┐ ┌────────┐  │   │
│ │ │ 99+    │ │ ●      │ │ NEW    │  │   │
│ │ │ 数字   │ │ 圆点   │ │ 文本   │  │   │
│ │ └────────┘ └────────┘ └────────┘  │   │
│ └────────────────────────────────────┘   │
├──────────────────────────────────────────┤
│ ┌────────────────────────────────────┐   │
│ │ [系统] 系统通知          10:32     │   │
│ │ 您的账号在新设备上登录...     ③    │   │
│ ├────────────────────────────────────┤   │
│ │ [互动] 评论回复          09:15     │   │
│ │ 张三回复了你的文章...       ⑤    │   │
│ ├────────────────────────────────────┤   │
│ │ [互动] 点赞提醒          昨天      │   │
│ │ 你的评论获得了128个赞       ●     │   │
│ ├────────────────────────────────────┤   │
│ │ ...更多消息...                     │   │
│ └────────────────────────────────────┘   │
└──────────────────────────────────────────┘

在这里插入图片描述

四、Badge 组件的最佳实践

4.1 数字角标的阈值处理

数字角标的常见需求是超过一定数量显示"99+"。这个逻辑需要开发者在传入 value 前自行处理:

// 推荐:在数据层处理阈值
function formatBadgeValue(count: number): string {
  if (count > 99) return '99+';
  if (count > 0) return '' + count;
  return '';
}

// 使用时
Badge({ value: formatBadgeValue(item.count), style: { badgeColor: '#FF3B30', fontSize: 10 }, position: BadgePosition.RightTop })

Badge 组件本身不处理数字阈值——它原样显示传入的 value 字符串。如果 count 是 128,而你传入了 '128',Badge 就会显示三位数字,角标变得过宽。应在数据层做阈值截断。

4.2 圆点角标与数字角标的选择

两种角标各有适用场景:

  • 数字角标:适合可量化的未读通知——“3 条评论”、“5 个赞”、“2 条私信”。数字给用户具体的预期,告诉用户"有多少内容需要处理"。
  • 圆点角标:适合不可量化的通知——“有人关注了你”、“账号在新设备登录”。这类事件只有"有"和"无"两种状态,用圆点表示更合适。

在 Demo 中,两种角标通过条件判断共同存在:count > 0 时用数字角标,count === 0 && dot 时用圆点角标。

4.3 Badge 与 Stack 的配合

Badge 必须放在 Stack 容器中才能正常工作。Badge 的定位是相对于 Stack 的边界进行的:

Stack() {
  // 被标注的目标组件
  Image($r('app.media.avatar'))
    .width(44).height(44)

  // 角标组件
  Badge({ value: '3', style: { badgeColor: '#FF3B30', fontSize: 10 }, position: BadgePosition.RightTop })
    .width(44).height(44)
}

Badge 的 width/height 应与目标组件相同——这确保 Badge 的定位基准与目标组件对齐。如果 Badge 不设置宽高,它的定位基准是 Stack 的完整区域,可能导致角标偏移。

4.4 条件渲染与角标

角标的显示/隐藏应通过 ArkUI 的条件渲染语法(if/else)而非 visibility 属性实现:

// 推荐:条件渲染
if (item.unread) {
  Badge({ value: '' + item.count, style: { badgeColor: '#FF3B30', fontSize: 10 }, position: BadgePosition.RightTop })
    .width(44).height(44)
}

// 不推荐:visibility 控制
Badge({ value: '' + item.count, style: { badgeColor: '#FF3B30', fontSize: 10 }, position: BadgePosition.RightTop })
  .width(44).height(44)
  .visibility(item.unread ? Visibility.Visible : Visibility.Hidden)

条件渲染在角标不存在时不创建组件节点,比 visibility 隐藏更节省渲染资源。此外,条件渲染让角标的显示逻辑更直观——在代码中一眼就能看出"什么时候显示角标"。

4.5 角标与无障碍

对于消息通知类页面,角标的可访问性设计值得考虑:

  • 数字角标的 value 应使用清晰的数字,避免仅用图标或纯色圆点
  • 配合朗读文本:Badge 本身不提供无障碍语义,但可以在外层 Stack 添加 .accessibilityText() 描述
  • 颜色不是唯一的区分方式:圆点和数字角标用不同的形状(圆点 vs 胶囊形)区分类型

4.6 性能优化

如果消息列表很长(50+ 条),每条消息都有条件角标渲染,需要注意:

  1. 使用 ForEach 的 key 函数:Demo 中 ForEach(this.messages, ..., item => '' + item.id) 的 key 函数帮助框架最小化 DOM 更新
  2. 避免内联对象:Badge 的 style 对象如果每次都重新创建,会产生不必要的 diff。但 ArkUI 框架在组件层面已做优化,大多数场景不需要额外处理
  3. 已读/未读切换:标记已读时创建新的数组副本 → 赋值给 @State → 框架 diff 后发现部分消息的角标条件变化 → 只更新变化的部分。这是标准且高效的 ArkUI 渲染模式

五、完整代码结构

BadgePage (~270 行)
├── 数据模型
│   └── interface MessageItem — 消息数据结构
├── 状态变量
│   ├── @State messages: MessageItem[] — 8 条模拟消息
│   └── @State unreadTotal: number — 未读消息总数
├── 业务方法
│   ├── countTotal() — 统计未读数量
│   ├── markAsRead(id) — 标记单条已读
│   └── markAllRead() — 标记全部已读
├── 辅助方法
│   └── tagColor(tag) — 消息分类颜色映射
├── 视图
│   ├── 标题栏 — 📬 消息中心
│   ├── 说明卡片 — Badge 组件介绍
│   ├── 状态栏 — 未读计数 + 全部已读按钮
│   ├── 角标类型演示区 — 数字/圆点/文本三种角标
│   └── 消息列表 — 8 条消息 + 条件角标
│       ├── 头像区 Stack — 标签 + Badge
│       └── 内容区 Column — 标题 + 时间 + 正文
└── @Builder badgeDemo() — 角标演示卡片

六、总结

本文通过一个消息通知中心 Demo 深入讲解了 HarmonyOS NEXT 中的 Badge 角标组件。Badge 将传统的手动 Stack 叠加 + 手动定位方案封装为声明式组件,通过 value 参数区分三种角标模式,通过 BadgePosition 控制位置,通过 BadgeStyle 配置样式。

核心要点回顾:

  1. 三种角标模式:数字角标(value: '99+')、圆点角标(value: '')、文本角标(value: 'NEW'),分别适用于可量化未读数、不可量化通知和状态标签三种场景。

  2. Badge 依赖 Stack:Badge 必须放在 Stack 容器中,通过 BadgePosition 确定在 Stack 中的位置。Badge 的宽高应与目标组件一致,确保定位基准对齐。

  3. 条件渲染控制显示:使用 ArkUI 的 if/else 语法控制角标的显示/隐藏,比 visibility 控制更高效且代码意图更清晰。

  4. 不可变更新模式:标记已读时必须创建新数组副本再赋值,因为 @State 基于引用比较检测变化。

  5. 数字阈值自行处理:Badge 不自动截断大数字(如显示 99+),开发者在传入 value 前自行格式化。

Badge 是消息通知体系中的核心视觉元素——它用最少的空间传递最关键的信息。在 HarmonyOS NEXT 中,Badge 组件让这个看似简单的 UI 元素变得真正简单:一行声明替代了手动布局、手动定位和手动格式化的十几行代码。

七、扩展思考

Badge 解决了基础的角标展示需求,但在实际项目中,角标的使用还有更多变化:

动态角标更新:本文 Demo 中角标通过 @State 数组驱动,标记已读后重建数组触发更新。在真实应用中,角标数据可能来自推送通知、消息同步或 WebSocket 实时更新——每次数据到达时更新 @State 数组即可,角标自动跟随刷新。

自定义角标形状:Badge 默认是圆形/胶囊形背景。如果需要三角形角标、带状角标或特殊形状角标,Badge 组件本身不支持自定义形状——需要回到手动 Stack + 自定义绘制方案。

组合角标:某些场景需要在同一位置显示多个角标(如同时显示数字和 NEW 标签)。单一 Badge 组件只能显示一个角标内容——多角标场景需要嵌套多个 Badge(不推荐,会重叠)或回到自定义布局。

角标动画:Badge 组件本身不提供显示/隐藏动画。如果需要角标弹入弹出效果(如微信的红点出现动画),需要在父组件层面使用 .transition().animation() 实现。

桌面角标:Android/iOS 桌面图标角标通过系统 API 设置(NotificationManager),与 Badge 组件无关。Badge 只用于应用内 UI 角标展示。

理解 Badge 的定位是正确使用它的关键:它是应用内 UI 级角标组件,解决了"在任意组件右上角显示数字/圆点/文本标记"的通用需求,但它不是系统级桌面角标的替代方案,也不支持自定义形状和动画。

通过本文的 Demo ——消息通知中心,你将 Badge 的三种模式和条件渲染模式应用到实践中,构建了一个完整的消息列表界面。这个页面可以作为任何需要消息中心的应用的起点模板。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐