一、引言

在移动应用界面中,角标/徽章(Badge)是一个小但无处不在的 UI 元素。社交 App 的消息列表上那个红色的未读数字、购物 App 底部 Tab 栏上购物车的商品数量角标、邮件客户端的收件箱未读计数、系统通知栏的应用角标——它们都以一个微小的圆形贴片附着在图标或列表项上,用最简洁的方式传递"这里有新的内容"的信息。

虽然角标看起来只是一个"带数字的小圆点",但在传统开发中实现它并非易事。你需要计算角标相对于宿主元素的位置(通常是右上角),处理数字过长时的显示策略(超过 99 显示"99+“还是”…"),管理空状态的显示逻辑(未读数为 0 时不显示角标),以及确保角标在不同屏幕密度下的大小一致。如果每个需要角标的场景都手写这些逻辑,代码会迅速膨胀。

而 HarmonyOS 提供了 Badge 组件——一个专用于徽章/角标的容器组件。它包裹任意子组件,自动在指定位置渲染一个带有数字或文本的角标。支持自定义位置(右上/右下/右侧)、颜色、大小、最大显示数字(超出以"99+"展示),并且当数值为 0 或空字符串时角标自动隐藏。

本文通过一个"消息中心"Demo 深入讲解 Badge 组件的核心用法:角标的位置如何设置?颜色和大小如何定制?如何与列表联动实现"标为已读"清除角标?以及 Badge 在实际业务中的最佳实践。

阅读完本文,你将能够:

  • 使用 Badge 组件为任意 UI 元素添加角标
  • 掌握 Badge 的位置(BadgePosition)和样式配置
  • 实现消息列表的未读角标与交互联动
  • 理解 Badge 的显示/隐藏逻辑和 maxCount 截断策略
  • 构建完整的消息中心页面

二、Badge 组件 API 总览

2.1 构造函数

Badge(options: BadgeOptions)
interface BadgeOptions {
  value: string;          // 角标显示的文本内容
  position?: BadgePosition; // 角标位置,默认 RightTop
  style?: BadgeStyle;     // 角标样式配置
  maxCount?: number;      // 最大显示数字,默认 99
}
参数 类型 默认值 说明
value string 必填 角标内容,"0"或空字符串表示不显示角标
position BadgePosition RightTop 角标相对宿主元素的方位
style BadgeStyle 默认样式 角标的颜色和大小配置
maxCount number 99 数字超过此值时显示"99+"

2.2 BadgePosition 枚举

enum BadgePosition {
  Right,     // 右侧居中
  RightTop,  // 右上角(默认,最常用)
  Left       // 左侧居中
}
位置 视觉效果 适用场景
RightTop 角标在宿主右上角,略微溢出 消息列表头像、Tab 图标角标
Right 角标在宿主右侧居中 列表项尾部状态标记
Left 角标在宿主左侧居中 特殊布局需求

绝大多数场景使用默认的 RightTop,它符合用户"角标在右上角"的认知习惯。RightLeft 适用于非标准的布局需求。

2.3 BadgeStyle 对象

interface BadgeStyle {
  badgeColor?: ResourceColor; // 角标背景颜色,默认红色
  badgeSize?: number | string; // 角标大小,默认 12vp
}
属性 类型 默认值 说明
badgeColor ResourceColor #FA2A2D(红色) 角标圆形的背景色
badgeSize number/string 16vp 角标圆形的直径

badgeSize 控制角标圆形的直径。对于头像上的角标,建议 14-18vp;对于图标上的角标,建议 12-16vp。角标内的文字大小会根据 badgeSize 自动缩放。

2.4 value 参数的特殊逻辑

value 是 Badge 最核心且微妙的参数。它的取值直接决定了角标的显示行为:

value 值 显示效果
"5" 显示数字 5
"99" 显示数字 99
"123" 如果 maxCount=99,显示"99+"
"0" 不显示角标
"" 不显示角标
"New" 显示文本"New"
"!" 显示感叹号标点

关键逻辑:当 value"0" 或空字符串 "" 时,Badge 会自动隐藏角标。这一设计让未读计数为 0 时无需额外条件判断即可自动隐藏角标,大大简化了业务代码。

2.5 Badge 作为容器组件

Badge 是一个容器组件——它包裹(sling)一个子组件,角标附着在这个子组件之上:

Badge({ value: '5', position: BadgePosition.RightTop }) {
  // 任意子组件——图标、头像、文本等
  Image($r('app.media.icon'))
    .width(48).height(48)
}

这意味着 Badge 不改变子组件的固有布局和尺寸,它只是在上方叠加了一个角标。子组件可以是任何 ArkUI 组件:ImageTextRowColumn,甚至嵌套的复杂组件。
在这里插入图片描述

三、Demo 设计:消息中心

3.1 功能概述

Demo 是一个"消息中心",模拟即时通讯应用的消息列表页面:

  1. 未读总览卡片:顶部蓝色卡片显示总未读数(带 Badge),以及"全部已读"按钮
  2. 消息列表:8 条模拟消息,每条显示头像(带未读角标)、联系人名、预览、时间
  3. 点击标为已读:点击消息项清除该条角标,总计数同步刷新
  4. 徽章配置:切换角标位置(右上/右下/右侧)、切换角标颜色(红/蓝/绿/橙)

3.2 交互点

# 交互 说明
1 点击消息项 将该条消息标为已读,角标消失,总计数 -N
2 全部已读 一键清除所有未读角标,总计数归零
3 角标位置切换 右上 / 右下 / 右侧三种位置实时切换
4 角标颜色切换 红 / 蓝 / 绿 / 橙四种颜色实时切换

四、完整代码实现

在这里插入图片描述

4.1 数据模型与状态

interface MessageItem {
  id: number;
  name: string;
  avatar: string;
  preview: string;
  time: string;
  unread: number;
}

@State messages: MessageItem[] = [
  { id: 1, name: '张小明', avatar: '#1677FF', preview: '明天的会议改到下午3点可以吗?', time: '刚刚', unread: 5 },
  { id: 2, name: '李设计', avatar: '#FF4D4F', preview: '新的设计稿已经上传到Figma了', time: '5分钟前', unread: 0 },
  { id: 3, name: '王开发', avatar: '#52C41A', preview: '这个bug我修好了,你测一下', time: '10分钟前', unread: 99 },
  { id: 4, name: '赵产品', avatar: '#FF9800', preview: 'PRD已经更新了,加了一个新需求', time: '30分钟前', unread: 2 },
  // ... 更多消息项
];
@State totalUnread: number = 0;
@State badgePosition: BadgePosition = BadgePosition.RightTop;
@State badgeColor: string = '#FF4D4F';

每条消息的 unread 字段决定角标显示的数值。totalUnread 是所有消息未读数的总和,显示在顶部总览卡片中作为总角标。

4.2 角标值计算

getBadgeValue(count: number): string {
  if (count === 0) {
    return '';
  }
  if (count > this.badgeMaxCount) {
    return this.badgeMaxCount.toString().concat('+');
  }
  return count.toString();
}

count 为 0 时返回空字符串 ""——根据 Badge 的 value 逻辑,空字符串会使角标自动隐藏。当 count 超过 maxCount(默认 99)时返回 "99+",否则返回数字的字符串形式。

4.3 标为已读的实现

markAsRead(id: number): void {
  const newList: MessageItem[] = [];
  for (let i = 0; i < this.messages.length; i++) {
    if (this.messages[i].id === id) {
      newList.push({
        id: this.messages[i].id,
        name: this.messages[i].name,
        avatar: this.messages[i].avatar,
        preview: this.messages[i].preview,
        time: this.messages[i].time,
        unread: 0  // 清零
      });
    } else {
      newList.push(this.messages[i]);
    }
  }
  this.messages = newList;
  this.updateTotal();
}

由于 ArkTS 的 @State 要求不可变更新,不能直接修改数组元素,而是需要创建新数组。markAsRead 遍历消息列表,将匹配 ID 的消息项 unread 设为 0(替换为新对象),其余项保持不变。最后将新数组赋值给 this.messages 触发 UI 刷新。

updateTotal() 遍历所有消息累加 unread,更新 totalUnread 状态——顶部总览卡片的总角标随之刷新。

4.4 消息列表项

ForEach(this.messages, (msg: MessageItem) => {
  Column() {
    Row() {
      Badge({
        value: this.getBadgeValue(msg.unread),
        position: this.badgePosition,
        style: { badgeSize: 16, badgeColor: this.badgeColor }
      }) {
        Row()
          .width(44).height(44)
          .borderRadius(22)
          .backgroundColor(msg.avatar)
          .justifyContent(FlexAlign.Center)
      }

      Column() {
        Row() {
          Text(msg.name)
            .fontSize(15).fontColor('#1a1a2e')
            .fontWeight(FontWeight.Medium).layoutWeight(1)
          Text(msg.time)
            .fontSize(11).fontColor('#BBBBCC')
        }
        Text(msg.preview)
          .fontSize(13).fontColor('#9999AA')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .layoutWeight(1).margin({ left: 12 })
    }
    .width('100%')
    .padding({ top: 14, bottom: 14, left: 4, right: 4 })
    .onClick(() => { this.markAsRead(msg.id); })
  }
  .width('100%')
  .border({ width: { bottom: 0.5 }, color: '#F2F3F5' })
})

每个消息项是一个水平布局:Badge 包裹彩色圆形头像 → 联系人名 + 消息预览。点击整行触发 markAsRead,该条消息的未读角标消失。

Badge 的 positionstyle.badgeColor 都绑定到 @State 变量,用户在配置面板切换时所有消息项的角标同步更新。

4.5 总览卡片与 Badge

Row() {
  Badge({
    value: this.getBadgeValue(this.totalUnread),
    position: BadgePosition.RightTop,
    style: { badgeSize: 18, badgeColor: '#FF4D4F' }
  }) {
    Image($r('sys.symbol.message'))
      .width(28).height(28)
      .fillColor('#FFFFFF')
  }
  .margin({ right: 12 })

  Column() {
    Text('未读消息'.concat(' ', this.totalUnread.toString(), ' 条'))
      .fontSize(16).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
    Text('点击消息项即可标为已读')
      .fontSize(12).fontColor('#FFFFFF88')
  }
  // ...
  Text('全部已读')
    .fontSize(12).fontColor('#FFFFFF')
    .onClick(() => { this.markAllRead(); })
}

顶部卡片使用系统符号 sys.symbol.message 作为图标,用 Badge 包裹显示总未读数。getBadgeValue(this.totalUnread) 在总数为 0 时返回空字符串,角标自动消失。"全部已读"按钮调用 markAllRead() 将所有消息的 unread 设为 0。

4.6 角标位置和颜色配置

// 位置切换
ForEach(['右上', '右下', '右侧'], (pos: string) => {
  Text(pos)
    .onClick(() => {
      if (pos === '右上') { this.badgePosition = BadgePosition.RightTop; }
      if (pos === '右下') { this.badgePosition = BadgePosition.Right; }
      if (pos === '右侧') { this.badgePosition = BadgePosition.Right; }
    })
})

// 颜色切换
ForEach(['#FF4D4F', '#1677FF', '#52C41A', '#FF9800'], (c: string) => {
  Row()
    .width(24).height(24).borderRadius(12)
    .backgroundColor(c)
    .border({ width: this.badgeColor === c ? 3 : 0, color: c })
    .onClick(() => { this.badgeColor = c; })
})

颜色配置以圆形色块的方式展示,选中态通过加粗边框(border.width: 3)表示。这种"颜色选择器"的设计既直观又节省空间。

五、关键技术点详解

5.1 Badge 的位置与溢出策略

Badge 的角标相对于宿主元素的定位方式为:角标圆心位于宿主元素的指定方位边缘。以 RightTop 为例,角标的圆心在宿主元素的右上角顶点附近,由于角标是一个圆形,它会部分溢出到宿主元素范围之外——这正是我们期望的"角标贴在上方"的效果。

角标的溢出量等于角标的半径(badgeSize / 2)。例如 badgeSize: 16 时,角标会在右上角向外溢出约 8vp。这意味着在布局时需要为 Badge 宿主元素留出适当的 margin 或 padding,避免角标被父容器裁剪。

// 推荐:为 Badge 宿主留出角标溢出空间
.margin({ top: 4, right: 4 })

5.2 value 空值逻辑的妙用

Badge 最巧妙的设计是 value 为空字符串或 "0" 时自动隐藏角标。这一特性在业务代码中非常实用——开发者不需要在每次使用 Badge 时都加一层条件判断:

// 不需要这样写(Badge 自动处理了零值隐藏)
if (unreadCount > 0) {
  Badge({ value: unreadCount.toString() }) { ... }
} else {
  // 仅显示宿主元素,无角标
}

// 直接这样写即可
Badge({ value: this.getBadgeValue(unreadCount) }) { ... }

getBadgeValue 方法返回空字符串(未读数为 0)、“99+”(超过 99)或数字字符串——Badge 内部自动处理了这三种情况的显示逻辑。

5.3 Badge 的"仅圆点"模式

在某些场景下(如"有新的系统通知但不需要显示具体数量"),需要一个不带数字的红色小圆点。可以通过设置 value: ' '(一个空格)或利用小到不可见的文字来实现,但更规范的做法是设置一个极小的值。不过,ArkUI 的 Badge 组件不原生支持"dot-only"模式。变通方案是设置 value: '' 时角标隐藏,而需要圆点时设置一个不可见字符(如零宽空格)。

对于大多数业务场景,"0 不显示、>0 显示数字"的方案已经足够。

5.4 @State 数组的不可变更新

Demo 中的 markAsRead 方法展示了 ArkTS 中 @State 数组的标准更新模式——不可变替换:

// 错误做法(直接修改,ArkTS 编译器会报错或 UI 不刷新)
this.messages[idx].unread = 0;

// 正确做法(创建新数组和新对象)
const newList: MessageItem[] = [];
for (let i = 0; i < this.messages.length; i++) {
  if (this.messages[i].id === id) {
    newList.push({ /* 展开字段, unread: 0 */ });
  } else {
    newList.push(this.messages[i]);
  }
}
this.messages = newList;

这种模式在 ArkTS 中非常常见:遍历 → 判断 → 创建新对象 → 推入新数组 → 赋值给 @State 变量。虽然代码量比命令式修改多一些,但它保证了 UI 的可靠刷新和状态的不可变性。

5.5 Badge 与系统符号的组合使用

Demo 的顶部卡片使用了系统符号(System Symbol)sys.symbol.message 作为图标:

Image($r('sys.symbol.message'))
  .width(28).height(28)
  .fillColor('#FFFFFF')

系统符号是一组内置的 SF Symbol 风格图标,通过 $r('sys.symbol.xxx') 引用。它们不需要额外引入资源文件,且颜色通过 .fillColor() 控制。Badge 包裹这个图标后,角标自动出现在图标的右上角。

六、运行效果

6.1 初始状态

进入"消息中心"页面,顶部蓝色卡片展示总未读角标(包裹消息图标)+ “未读消息 110 条”(5+0+99+2+0+1+0+3=110)。下方 8 条消息,每条显示彩色圆形头像(带红色未读角标)、联系人名、消息预览和时间。

张小明显示角标"5",王开发显示角标"99+ "(因 unread=99 达到 maxCount 阈值),李设计和项目群的未读为 0,角标不显示。

6.2 标为已读

点击王开发的消息行 → 该行角标消失(unread 从 99 变为 0),顶部总角标从 110 变为 11。再点击张小明 → 角标消失,总计数从 11 变为 6。

6.3 一键全部已读

点击"全部已读"按钮 → 所有消息项的角标消失,顶部总角标自动隐藏(totalUnread 归零,getBadgeValue(0) 返回空字符串)。

6.4 角标配置调整

在配置面板点击"右下" → 所有消息项的角标位置从右上角变为右下角居中。点击绿色色块 → 所有角标颜色从红色变为绿色 #52C41A。切换回"右上" + 红色,恢复正常外观。

七、总结

本文通过一个"消息中心"实战 Demo,深入讲解了 HarmonyOS Badge 徽章组件的核心用法:

  1. Badge 作为容器组件:包裹任意子组件,在指定位置渲染角标
  2. BadgePosition:RightTop(右上,默认)/ Right(右侧居中)/ Left(左侧居中)三种位置
  3. BadgeStylebadgeColor(背景色)和 badgeSize(直径)控制角标外观
  4. value 空值隐藏"""0" 时角标自动不显示,简化零值判断代码
  5. maxCount 截断:数字超过 maxCount 显示"99+",避免角标过宽
  6. 与列表联动:通过 @State 数组不可变更新实现"标为已读→角标消失"的交互闭环

Badge 组件虽然 API 简洁,但在实际应用中承载了大量微妙的 UI 逻辑——位置计算、数字截断、零值隐藏——这些逻辑如果由开发者手写,既容易出错又占用精力。Badge 将这些细节封装进组件内部,让开发者专注于业务逻辑而非角标渲染。这正是 ArkUI"组件化封装"设计哲学的体现。

从消息列表到购物车角标,从邮件未读到通知提醒,Badge 以最简洁的方式传递"这里有新内容"的信息。希望本文能帮助你在实际项目中高效运用 Badge 组件。


本文基于 HarmonyOS NEXT API 24 编写,代码经 DevEco Studio 6.1.1 编译验证通过。

Logo

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

更多推荐