Stack 实现购物车角标鸿蒙 HarmonyOS ArkTS 原生学习
项目演示




一、前言
在移动应用开发中,购物车角标是电商类、社交类应用中最常见的 UI 模式之一。微信、淘宝、京东等主流应用无一例外地采用了这一视觉范式——在购物车图标右上角显示一个红色圆形标记,用于提示用户当前购物车中的商品数量。
对于鸿蒙原生开发者而言,如何在 HarmonyOS NEXT(API 24)上使用 ArkTS 高效、优雅地实现这一布局,是掌握鸿蒙 UI 开发的重要技能。本文将从一个完整的可运行示例出发,深入剖析 Stack 布局容器与 Badge 角标组件的组合使用,详细讲解布局原理、状态管理、样式定制等核心技术。
本文面向具有一定 ArkTS 基础的开发者,假设读者已了解 @Entry、@Component、@State 等基础概念。通过本文的学习,读者将能够:
- 理解 Stack 层叠布局的核心原理
- 掌握 Badge 角标组件的完整 API
- 实现购物车角标的动态更新功能
- 了解 API 24 的新特性和注意事项
- 掌握性能优化和最佳实践
二、开发环境配置
2.1 环境要求
| 项目 | 要求 |
|---|---|
| 操作系统 | Windows 10+ / macOS 13+ |
| IDE | DevEco Studio 5.1+ |
| SDK | HarmonyOS NEXT API 24(HarmonyOS 6.2) |
| 构建工具 | hvigor(内置在 DevEco Studio 中) |
| 目标设备 | 手机 / 平板 / 模拟器 |
2.2 工程结构
MyApplication32/
├── AppScope/ # 应用级配置
│ └── resources/ # 应用级资源文件
├── entry/ # 主模块
│ └── src/main/
│ ├── ets/
│ │ ├── entryability/ # Ability 生命周期管理
│ │ └── pages/ # 页面目录
│ │ └── Index.ets # ★ 核心页面
│ └── resources/ # 模块级资源
│ └── base/
│ ├── element/ # 资源配置(颜色、尺寸、字符串)
│ └── media/ # 媒体资源(图片、音频、视频)
├── build-profile.json5 # 应用级构建配置
└── hvigorfile.ts # 构建脚本
2.3 SDK 版本配置
在 build-profile.json5 中配置目标 SDK 版本为 API 24:
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.2.0(24)",
"compatibleSdkVersion": "6.2.0(24)",
"runtimeOS": "HarmonyOS"
}
]
}
}
2.4 资源文件准备
在 entry/src/main/resources/base/media/ 目录下放置购物车图标文件,建议命名为 cart.png。如果没有购物车图标,可以使用项目中已有的图标资源(如 startIcon.png)作为替代。
三、核心组件详解
3.1 Stack — 层叠布局容器
Stack 是 ArkUI 提供的层叠布局容器,其核心特点是:子组件按照在代码中的书写顺序从底向上依次堆叠,后声明的组件在 Z 轴上覆盖先声明的组件。
3.1.1 基本概念
Stack 的工作原理可以类比为桌面上叠放的纸张:
Z 轴方向(从上往下看)
┌─────────────────────┐
│ 顶层子组件(最后声明) │ ← Z-index 最高
├─────────────────────┤
│ 中间层子组件 │
├─────────────────────┤
│ 底层子组件(最先声明) │ ← Z-index 最低
└─────────────────────┘
这种布局方式在以下场景中非常有用:
- 在图片上叠加文字或按钮
- 在图标上叠加角标或状态指示器
- 在卡片底部叠加半透明遮罩
- 在页面顶层覆盖加载动画或弹窗
3.1.2 核心特性
| 特性 | 说明 |
|---|---|
| 无需导入 | Stack 是 ArkUI 全局内置类型,无需 import 声明 |
| Z 轴层叠 | 子组件按添加顺序从下往上叠放 |
| 对齐控制 | 通过 alignContent 参数控制子组件默认对齐方式 |
| 尺寸自适应 | 无内容时尺寸为 0×0,有内容时自适应最大子组件尺寸 |
| 性能优化 | API 24 中子组件超过 10 个时布局计算性能提升约 30% |
3.1.3 构造函数参数
Stack(value?: { alignContent?: Alignment })
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
alignContent |
Alignment |
Alignment.Center |
子组件的默认对齐方式 |
3.1.4 Alignment 对齐枚举
API 24 中统一使用 Alignment 枚举,StackAlign 已废弃。Alignment 共 9 个取值:
| 对齐值 | 位置 | 示意图 |
|---|---|---|
Alignment.TopStart |
左上角 | ⬆️⬅️ |
Alignment.Top |
顶部居中 | ⬆️⏹️ |
Alignment.TopEnd |
右上角 | ⬆️➡️ |
Alignment.Start |
左侧居中 | ⏹️⬅️ |
Alignment.Center |
正中心 | ⏹️⏹️ |
Alignment.End |
右侧居中 | ⏹️➡️ |
Alignment.BottomStart |
左下角 | ⬇️⬅️ |
Alignment.Bottom |
底部居中 | ⬇️⏹️ |
Alignment.BottomEnd |
右下角 | ⬇️➡️ |
九宫格示意图:
TopStart Top TopEnd
┌─────────────────────────┐
│ • │ • │ • │
Start│ • │ • │ • │End
│ • │ • │ • │
└─────────────────────────┘
BottomStart Bottom BottomEnd
3.1.5 基本用法示例
@Entry
@Component
struct StackBasicExample {
build() {
Stack({ alignContent: Alignment.Center }) {
// 底层:蓝色背景
Row()
.width(200)
.height(200)
.backgroundColor('#E6F7FF')
// 中层:白色矩形
Column()
.width(150)
.height(150)
.backgroundColor('#FFFFFF')
.borderRadius(12)
// 顶层:文字
Text('层叠布局')
.fontSize(24)
.fontColor('#1890FF')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height('100%')
}
}
3.1.6 子组件定位方式
Stack 中的子组件可以通过以下三种方式定位:
方式一:使用 .position() 绝对定位
Stack() {
Image($r('app.media.background'))
.width('100%')
.height('100%')
Button('跳过')
.position({ x: 20, y: 20 }) // 相对于 Stack 左上角偏移
}
方式二:使用 .align() 对齐定位
Stack() {
Image($r('app.media.background'))
.width('100%')
.height('100%')
Button('登录')
.align(Alignment.BottomCenter) // 底部居中
}
方式三:依赖 Stack 的 alignContent 参数
Stack({ alignContent: Alignment.TopEnd }) {
Image($r('app.media.banner'))
.width('100%')
.height(200)
Text('限时特惠') // 自动对齐到右上角
}
3.2 Badge — 系统内置角标组件
Badge 是 HarmonyOS NEXT 内置的角标容器,专门用于在子元素的特定位置显示数字标记、文字标记或纯色圆点。
3.2.1 为什么需要 Badge 组件
在没有 Badge 组件时,实现角标通常需要手动组合多个组件:
// 手动实现角标(不推荐)
Stack() {
Image($r('app.media.cart'))
.width(60)
.height(60)
// 手动创建角标
Text('3')
.fontSize(12)
.fontColor('#FFFFFF')
.width(20)
.height(20)
.borderRadius(10)
.backgroundColor('#FF4D4F')
.textAlign(TextAlign.Center)
.position({ x: 45, y: -5 }) // 手动计算位置
}
这种手动实现存在以下问题:
- 位置计算复杂:不同尺寸的图标需要不同的偏移量
- 长数字处理困难:超过 99 需要手动截断显示 “99+”
- 零值处理繁琐:数量为 0 时需要手动隐藏角标
- 样式不一致:需要手动维护角标样式与设计规范一致
- 无障碍支持缺失:手动实现的角标不支持无障碍属性
Badge 组件通过封装这些逻辑,提供了简洁的 API:
// 使用 Badge 组件(推荐)
Badge({ count: 3, position: BadgePosition.RightTop }) {
Image($r('app.media.cart'))
.width(60)
.height(60)
}
3.2.2 构造函数
Badge 提供两种构造方式,分别用于数字角标和文字角标:
方式一:数字角标
Badge(value: BadgeParamWithNumber)
方式二:文字角标
Badge(value: BadgeParamWithString)
3.2.3 参数说明
BadgeParamWithNumber(数字角标参数):
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
count |
number |
是 | - | 角标数字,0 时自动隐藏 |
position |
BadgePosition |
否 | RightTop |
角标位置 |
maxCount |
number |
否 | 99 | 最大显示数字,超过显示 “maxCount+” |
style |
BadgeStyle |
否 | - | 角标样式配置 |
BadgeParamWithString(文字角标参数):
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
value |
string |
是 | - | 角标文字内容 |
position |
BadgePosition |
否 | RightTop |
角标位置 |
style |
BadgeStyle |
否 | - | 角标样式配置 |
3.2.4 BadgePosition 位置枚举
| 枚举值 | 说明 | 适用场景 |
|---|---|---|
BadgePosition.RightTop |
右上角(默认) | 图标角标、Tab 角标 |
BadgePosition.Right |
右侧居中 | 列表项数量指示 |
BadgePosition.RightBottom |
右下角 | 底部标签 |
BadgePosition.LeftTop |
左上角 | 图片左上角标签 |
BadgePosition.Left |
左侧居中 | 左侧状态标记 |
BadgePosition.LeftBottom |
左下角 | 底部标签 |
3.2.5 BadgeStyle 样式配置
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
badgeColor |
ResourceColor |
Color.Red |
角标背景颜色 |
badgeSize |
number |
16 | 角标尺寸(vp) |
color |
ResourceColor |
Color.White |
角标文字颜色 |
fontSize |
number |
10 | 角标文字大小(fp) |
fontWeight |
number | FontWeight |
- | 角标文字粗细 |
borderColor |
ResourceColor |
- | 角标边框颜色 |
borderWidth |
number |
- | 角标边框宽度 |
重要注意事项:API 24 中 BadgeStyle 的文字颜色属性是
color而非textColor,使用textColor会导致编译错误。这是一个常见的坑,需要特别注意。
3.2.6 三种角标模式
模式一:数字角标(最常用)
数字角标用于显示未读数量、购物车商品数等数值信息:
// 购物车角标
Badge({ count: this.cartCount, position: BadgePosition.RightTop }) {
Image($r('app.media.cart'))
.width(60)
.height(60)
}
模式二:文字角标
文字角标用于显示标签、状态等文本信息:
// 新品标签
Badge({ value: 'NEW', position: BadgePosition.RightTop }) {
Image($r('app.media.product'))
.width(100)
.height(100)
}
模式三:纯点角标(空字符串)
纯点角标用于显示二元状态(如在线/离线、已读/未读):
// 未读状态指示
Badge({ value: '', position: BadgePosition.RightTop }) {
Image($r('app.media.message'))
.width(40)
.height(40)
}
3.2.7 关键特性
特性一:零值自动隐藏
当 count 为 0 时,角标自动隐藏,无需手动判断:
// count = 0 时角标不可见
Badge({ count: 0, position: BadgePosition.RightTop }) {
Image($r('app.media.cart'))
}
特性二:超限自动格式化
当 count 超过 maxCount 时,自动显示为 “maxCount+”:
// count = 150,maxCount = 99 → 显示 "99+"
Badge({ count: 150, maxCount: 99, position: BadgePosition.RightTop }) {
Image($r('app.media.cart'))
}
特性三:显隐动画
从 API version 12 开始,Badge 组件显隐时自动支持 scale 动效,无需额外配置。
特性四:无障碍支持
Badge 组件内置无障碍属性,支持屏幕阅读器等辅助技术。
四、购物车角标完整实现
4.1 需求分析
我们需要实现一个购物车角标页面,包含以下功能:
- 购物车图标展示:居中显示购物车图标
- 角标数字显示:在图标右上角显示商品数量
- 数量控制按钮:提供增加、减少、清空操作
- 状态响应:按钮操作后角标实时更新
4.2 布局设计
页面结构层次:
┌─────────────────────────────────┐
│ Stack (全屏容器,居中对齐) │
│ ┌─────────────────────────┐ │
│ │ Column (垂直排列) │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Badge (角标) │ │ │
│ │ │ Image (图标) │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Row (操作按钮) │ │ │
│ │ │ - + 数量 │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Button (清空) │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
4.3 完整代码
// ArkUI 组件(Stack、Column、Row、Badge、Image、Button、Text)及枚举(Alignment、HorizontalAlign、FontWeight、BadgePosition)均为全局内置,无需显式导入
@Entry
@Component
struct Index {
/**
* 购物车商品数量状态变量
* 使用 @State 装饰器实现响应式状态管理,值变化时自动触发UI更新
* 默认值为 3,展示初始角标效果
*/
@State cartCount: number = 3;
/**
* 增加购物车数量
* 每次点击增加1,最多增加到99(与 maxCount 保持一致)
*/
addToCart(): void {
if (this.cartCount < 99) {
this.cartCount++;
}
}
/**
* 减少购物车数量
* 每次点击减少1,最少减少到0
*/
removeFromCart(): void {
if (this.cartCount > 0) {
this.cartCount--;
}
}
/**
* 清空购物车
* 将数量重置为0,角标会自动隐藏(Badge 组件特性)
*/
clearCart(): void {
this.cartCount = 0;
}
build() {
/**
* Stack 布局容器:实现全屏居中布局
* alignContent: Alignment.Center — 所有子组件垂直水平居中
* 作为页面根容器,包裹整个页面内容
*/
Stack({ alignContent: Alignment.Center }) {
/**
* Column 容器:垂直排列页面元素
* space: 60 — 子组件之间的垂直间距为 60vp
* 将购物车图标区域和操作按钮区域垂直排列
*/
Column({ space: 60 }) {
/**
* Badge 组件:实现数字角标功能
* 包裹在购物车图标外层,自动在右上角显示数字
*
* 参数说明:
* - count: 角标显示的数字,当 count <= 0 时角标自动隐藏
* - position: 角标位置,BadgePosition.RightTop 表示右上角
* - maxCount: 角标最大显示数字,超过此值显示为 "maxCount+"
* - style: 角标样式配置对象
*/
Badge({
count: this.cartCount,
position: BadgePosition.RightTop,
maxCount: 99,
style: {
/** 角标背景颜色:红色(#FF4D4F) */
badgeColor: '#FF4D4F',
/** 角标尺寸:20vp */
badgeSize: 20,
/** 角标内文字颜色:白色(#FFFFFF) */
color: '#FFFFFF',
/** 角标内文字大小:12fp */
fontSize: 12,
/** 角标边框颜色:白色(#FFFFFF) */
borderColor: '#FFFFFF',
/** 角标边框宽度:2vp */
borderWidth: 2,
/** 角标文字粗细:粗体 */
fontWeight: FontWeight.Bold
}
}) {
/**
* 购物车图标:使用 Image 组件显示购物车图片
* 通过 $r('app.media.startIcon') 引用 resources 资源目录中的图片
* 尺寸设置为 80×80vp
*/
Image($r('app.media.startIcon'))
.width(80)
.height(80)
}
/**
* 操作按钮区域:提供增加、减少购物车数量的交互按钮
* Row({ space: 20 }) — 水平排列,子组件之间间距 20vp
*/
Row({ space: 20 }) {
/**
* 减少按钮:圆形红色按钮
* 点击触发 removeFromCart() 方法
* 样式:60×60vp,圆角 30vp,红色背景,白色文字
*/
Button('-')
.width(60)
.height(60)
.fontSize(30)
.fontColor('#FFFFFF')
.backgroundColor('#FF6B6B')
.borderRadius(30)
.onClick(() => {
this.removeFromCart();
})
/**
* 数量显示文本:展示当前购物车数量
* 样式:20fp,中等粗细,深灰色文字
*/
Text(`购物车数量: ${this.cartCount}`)
.fontSize(20)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
/**
* 增加按钮:圆形绿色按钮
* 点击触发 addToCart() 方法
* 样式:60×60vp,圆角 30vp,绿色背景,白色文字
*/
Button('+')
.width(60)
.height(60)
.fontSize(30)
.fontColor('#FFFFFF')
.backgroundColor('#52C41A')
.borderRadius(30)
.onClick(() => {
this.addToCart();
})
}
/**
* 清空按钮:圆角矩形红色按钮
* 点击触发 clearCart() 方法
* 样式:200×48vp,圆角 24vp,红色背景,白色文字
*/
Button('清空购物车')
.width(200)
.height(48)
.fontSize(18)
.fontColor('#FFFFFF')
.backgroundColor('#FF4D4F')
.borderRadius(24)
.onClick(() => {
this.clearCart();
})
}
/**
* Column 容器样式:
* - width('100%') — 宽度占满父容器
* - alignItems(HorizontalAlign.Center) — 子组件水平居中对齐
*/
.width('100%')
.alignItems(HorizontalAlign.Center)
}
/**
* Stack 容器样式:
* - height('100%') — 高度占满屏幕
* - width('100%') — 宽度占满屏幕
*/
.height('100%')
.width('100%')
/**
* 页面背景色:浅灰色(#F5F5F5)
* 提供柔和的视觉背景
*/
.backgroundColor('#F5F5F5')
}
}
4.4 代码解读
4.4.1 状态管理
@State cartCount: number = 3;
@State 装饰器是 ArkUI 中最基础的状态管理方式,其核心特性:
- 响应式:当
cartCount的值发生变化时,框架会自动触发 UI 更新 - 局部作用域:状态仅在当前组件内部生效
- 初始值:必须为状态变量提供初始值(此处为 3)
在 ArkTS 中,状态管理是实现交互的核心机制。除了 @State,还有以下常用的状态装饰器:
| 装饰器 | 作用域 | 数据流 | 适用场景 |
|---|---|---|---|
@State |
组件内部 | 单向 | 组件内部状态 |
@Prop |
父子组件 | 单向(父→子) | 父组件向子组件传递状态 |
@Link |
父子组件 | 双向 | 父子组件共享状态 |
@Provide |
跨层级 | 单向(祖先→后代) | 全局状态传递 |
@Consume |
跨层级 | 单向(祖先→后代) | 接收全局状态 |
4.4.2 Stack 布局的使用
Stack({ alignContent: Alignment.Center }) {
Column({ space: 60 }) {
Badge({ ... }) {
Image(...)
}
// ... 其他组件
}
}
外层 Stack 的作用是实现全屏居中布局,将所有内容垂直水平居中显示。
为什么使用 Stack 而不是 Column 作为根容器?
虽然 Column 也可以实现居中布局,但 Stack 有以下优势:
- 层叠能力:如果将来需要在页面上叠加遮罩、弹窗等元素,Stack 天然支持
- 简化布局:Stack 的
alignContent参数可以一次性设置所有子组件的对齐方式 - 性能优化:Stack 的布局计算比 Column 更简单高效
4.4.3 Badge 配置详解
Badge({
count: this.cartCount, // 绑定状态变量
position: BadgePosition.RightTop, // 右上角位置
maxCount: 99, // 最大显示99
style: {
badgeColor: '#FF4D4F', // 红色背景
badgeSize: 20, // 20vp尺寸
color: '#FFFFFF', // 白色文字
fontSize: 12, // 12fp字号
borderColor: '#FFFFFF', // 白色边框
borderWidth: 2, // 2vp边框
fontWeight: FontWeight.Bold // 粗体
}
})
关键配置说明:
count: this.cartCount:将角标数字与状态变量绑定,实现响应式更新position: BadgePosition.RightTop:角标显示在右上角(图标角标的标准位置)maxCount: 99:超过 99 显示 “99+”,符合电商应用的常见设计borderColor: '#FFFFFF'和borderWidth: 2:添加白色边框,使角标在浅色图标上更加醒目
4.4.4 事件绑定
Button('-')
.onClick(() => {
this.removeFromCart();
})
使用箭头函数绑定点击事件,调用组件方法修改状态。当状态变化时,框架自动重新渲染相关 UI。
这种事件绑定方式是 ArkTS 的标准做法,具有以下特点:
- 简洁性:使用
.onClick()链式调用绑定事件 - 类型安全:事件处理器的参数类型由框架自动推断
- 响应式:状态变化自动触发 UI 更新
4.5 运行效果
| 操作 | 效果 |
|---|---|
| 初始状态 | 购物车图标右上角显示红色角标 “3” |
| 点击 “+” | 角标数字增加(3→4→5…) |
| 点击 “-” | 角标数字减少(3→2→1→0) |
| 点击 “清空购物车” | 角标数字变为 0,角标自动隐藏 |
| count > 99 | 角标显示 “99+” |
五、进阶应用场景
5.1 场景一:Tab 底部导航角标
在实际应用中,购物车角标通常出现在底部导航栏中。以下是一个完整的底部导航实现:
// 注意:以下示例中的图片资源名为示意,实际运行时需替换为项目中存在的资源
@Entry
@Component
struct TabBarExample {
@State cartCount: number = 5;
@State messageCount: number = 12;
@State selectedIndex: number = 0;
build() {
Column() {
// 页面内容区域
Stack() {
// 根据 selectedIndex 显示不同页面内容
if (this.selectedIndex === 0) {
Text('首页')
.fontSize(32)
.fontColor('#333333')
} else if (this.selectedIndex === 1) {
Text('分类')
.fontSize(32)
.fontColor('#333333')
} else if (this.selectedIndex === 2) {
Text('购物车')
.fontSize(32)
.fontColor('#333333')
} else {
Text('我的')
.fontSize(32)
.fontColor('#333333')
}
}
.layoutWeight(1)
.width('100%')
// 底部导航栏
Row({ space: 0 }) {
// 首页
this.TabItem('首页', $r('app.media.home'), 0)
// 分类
this.TabItem('分类', $r('app.media.category'), 1)
// 购物车(带角标)
Stack({ alignContent: Alignment.Center }) {
Column({ space: 4 }) {
Badge({ count: this.cartCount, position: BadgePosition.RightTop }) {
Image($r('app.media.cart'))
.width(24)
.height(24)
.fillColor(this.selectedIndex === 2 ? '#FF4D4F' : '#999999')
}
Text('购物车')
.fontSize(10)
.fontColor(this.selectedIndex === 2 ? '#FF4D4F' : '#999999')
}
}
.layoutWeight(1)
.height(56)
.onClick(() => {
this.selectedIndex = 2;
})
// 我的(带消息角标)
Stack({ alignContent: Alignment.Center }) {
Column({ space: 4 }) {
Badge({ count: this.messageCount, position: BadgePosition.RightTop }) {
Image($r('app.media.profile'))
.width(24)
.height(24)
.fillColor(this.selectedIndex === 3 ? '#FF4D4F' : '#999999')
}
Text('我的')
.fontSize(10)
.fontColor(this.selectedIndex === 3 ? '#FF4D4F' : '#999999')
}
}
.layoutWeight(1)
.height(56)
.onClick(() => {
this.selectedIndex = 3;
})
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder TabItem(title: string, icon: Resource, index: number) {
Stack({ alignContent: Alignment.Center }) {
Column({ space: 4 }) {
Image(icon)
.width(24)
.height(24)
.fillColor(this.selectedIndex === index ? '#FF4D4F' : '#999999')
Text(title)
.fontSize(10)
.fontColor(this.selectedIndex === index ? '#FF4D4F' : '#999999')
}
}
.layoutWeight(1)
.height(56)
.onClick(() => {
this.selectedIndex = index;
})
}
}
5.2 场景二:商品列表角标
在商品列表中,可以使用 Badge 显示商品标签(如 “热销”、“新品”):
// 注意:以下示例中的图片资源名为示意,实际运行时需替换为项目中存在的资源
@Entry
@Component
struct ProductListExample {
@State products: Array<{ name: string; price: number; tag: string; image: Resource }> = [
{ name: '智能手机 Pro', price: 4999, tag: '热销', image: $r('app.media.product1') },
{ name: '无线蓝牙耳机', price: 599, tag: '新品', image: $r('app.media.product2') },
{ name: '智能手表', price: 1299, tag: '', image: $r('app.media.product3') },
{ name: '平板电脑', price: 3999, tag: '限时特惠', image: $r('app.media.product4') }
];
build() {
Column() {
Text('商品列表')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 16 })
.alignSelf(ItemAlign.Start)
.padding({ left: 16 })
List({ space: 16 }) {
ForEach(this.products, (item: { name: string; price: number; tag: string; image: Resource }) => {
ListItem() {
Stack({ alignContent: Alignment.TopStart }) {
// 商品卡片
Column({ space: 8 }) {
Stack() {
Image(item.image)
.width('100%')
.height(150)
.borderRadius(8)
// 标签角标(仅当有标签时显示)
if (item.tag.length > 0) {
Badge({
value: item.tag,
position: BadgePosition.LeftTop,
style: {
badgeColor: item.tag === '热销' ? '#FF4D4F' :
item.tag === '新品' ? '#52C41A' : '#FA8C16',
fontSize: 10,
badgeSize: 24
}
}) {
Text('')
.width(0)
.height(0)
}
}
}
Text(item.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Text(`¥${item.price}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FF4D4F')
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(8)
.padding(12)
.shadow({ radius: 4, color: '#00000010', offsetY: 2 })
}
}
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
5.3 场景三:头像在线状态
使用 Badge 显示用户在线状态:
// 注意:以下示例中的图片资源名为示意,实际运行时需替换为项目中存在的资源
@Entry
@Component
struct AvatarStatusExample {
@State users: Array<{ name: string; isOnline: boolean; avatar: Resource }> = [
{ name: '张三', isOnline: true, avatar: $r('app.media.avatar1') },
{ name: '李四', isOnline: false, avatar: $r('app.media.avatar2') },
{ name: '王五', isOnline: true, avatar: $r('app.media.avatar3') }
];
build() {
Column() {
Text('联系人')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 16 })
.alignSelf(ItemAlign.Start)
.padding({ left: 16 })
List({ space: 12 }) {
ForEach(this.users, (item: { name: string; isOnline: boolean; avatar: Resource }) => {
ListItem() {
Row({ space: 12 }) {
// 头像(带在线状态角标)
Badge({
value: '',
position: BadgePosition.RightBottom,
style: {
badgeColor: item.isOnline ? '#52C41A' : '#CCCCCC',
badgeSize: 12,
borderColor: '#FFFFFF',
borderWidth: 2
}
}) {
Image(item.avatar)
.width(48)
.height(48)
.borderRadius(24)
}
// 信息
Column({ space: 4 }) {
Text(item.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Text(item.isOnline ? '在线' : '离线')
.fontSize(12)
.fontColor(item.isOnline ? '#52C41A' : '#999999')
}
.layoutWeight(1)
// 箭头
Image($r('app.media.arrow'))
.width(20)
.height(20)
.fillColor('#CCCCCC')
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor('#FFFFFF')
}
})
}
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
六、API 24 新特性与注意事项
6.1 Stack 组件新特性
特性一:对齐枚举统一
API 24 中 StackAlign 枚举已废弃,统一使用 Alignment 枚举:
// API 24 之前(已废弃)
Stack({ alignContent: StackAlign.TopRight })
// API 24(推荐)
Stack({ alignContent: Alignment.TopEnd })
特性二:性能优化
当 Stack 包含超过 10 个子组件时,布局计算性能提升约 30%。这得益于框架对嵌套布局的优化处理。
特性三:嵌套稳定性
多层 Stack 嵌套的尺寸溢出问题已修复。在之前的版本中,嵌套 Stack 可能会出现尺寸计算错误,API 24 中已解决此问题。
特性四:渲染性能提升
API 24 对 Stack 的渲染流程进行了优化,减少了不必要的重绘和布局计算。
6.2 Badge 组件注意事项
注意一:样式属性名称
API 24 中 BadgeStyle 的文字颜色属性是 color 而非 textColor:
// 正确
Badge({ style: { color: '#FFFFFF' } })
// 错误(编译失败)
Badge({ style: { textColor: '#FFFFFF' } })
注意二:资源类型
从 API version 20 开始,fontSize 支持 ResourceStr 类型:
// 使用资源引用
Badge({ style: { fontSize: $r('app.float.badge_font_size') } })
// 使用数字
Badge({ style: { fontSize: 12 } })
注意三:badgeSize 与 fontSize 的关系
当设置了 fontSize 且 badgeSize 小于 fontSize 时,badgeSize 将按照 fontSize 生效:
// badgeSize 会自动调整为 14(与 fontSize 一致)
Badge({ style: { badgeSize: 10, fontSize: 14 } })
注意四:子组件要求
Badge 组件必须包含子组件,空 body 会导致编译错误:
// 错误(编译失败)
Badge({ count: 5 }) { }
// 正确
Badge({ count: 5 }) {
Image($r('app.media.icon'))
}
6.3 常见编译错误
错误一:Alignment.TopRight 不存在
Error: Property 'TopRight' does not exist on type 'typeof Alignment'
原因:API 24 中 Alignment 枚举使用 TopEnd 代替 TopRight
解决方案:将 Alignment.TopRight 替换为 Alignment.TopEnd
错误二:BadgeStyle 中 textColor 不存在
Error: Property 'textColor' does not exist on type 'BadgeStyle'
原因:BadgeStyle 中文字颜色属性名为 color
解决方案:将 textColor 替换为 color
错误三:缺少图片资源
Error: Resource 'app.media.shopping_cart' does not exist
原因:资源目录中不存在对应的图片文件
解决方案:检查资源目录,确保图片文件存在,或使用项目中已有的图片资源
错误四:Badge 缺少子组件
Error: Badge must have a child component
原因:Badge 组件必须包含子组件
解决方案:在 Badge 中添加子组件
七、最佳实践与性能优化
7.1 Stack 布局最佳实践
实践一:控制子组件数量
虽然 API 24 对 Stack 性能进行了优化,但过多的子组件仍然会影响性能。建议每个 Stack 的子组件数量不超过 10 个。
实践二:避免深层嵌套
深层嵌套的 Stack 会增加布局计算复杂度。建议嵌套层数不超过 3 层。
实践三:显式设置尺寸
当子组件使用百分比尺寸时,Stack 必须有固定尺寸:
// 正确
Stack() {
Image($r('app.media.background'))
.width('100%') // 需要父容器有固定尺寸
.height('100%')
}
.width(300)
.height(300)
// 错误(子组件尺寸无法计算)
Stack() {
Image($r('app.media.background'))
.width('100%')
.height('100%')
}
实践四:按 Z 轴顺序编写代码
按照"底层 → 中层 → 顶层"的顺序编写子组件,便于代码阅读和维护:
Stack() {
// 底层:背景
Image($r('app.media.background'))
// 中层:内容
Text('内容')
// 顶层:遮罩/按钮
Button('操作')
}
实践五:使用 .renderGroup() 优化动画
对于包含复杂子组件的动画,将其设置为 renderGroup(true),减少渲染批次:
Stack() {
// 复杂动画组件
}
.renderGroup(true)
7.2 Badge 组件最佳实践
实践一:使用状态变量绑定 count
将角标数量与 @State、@Link、@Prop 等状态变量绑定,实现响应式更新:
@State cartCount: number = 0;
Badge({ count: this.cartCount }) {
Image($r('app.media.cart'))
}
实践二:利用零值自动隐藏特性
无需手动判断 count 是否为 0,Badge 组件会自动处理:
// 推荐:直接绑定状态变量
Badge({ count: this.cartCount }) {
Image($r('app.media.cart'))
}
// 不推荐:手动判断
if (this.cartCount > 0) {
Badge({ count: this.cartCount }) {
Image($r('app.media.cart'))
}
} else {
Image($r('app.media.cart'))
}
实践三:合理设置 maxCount
根据业务场景设置合适的最大显示数字:
// 购物车:通常最大显示99
Badge({ count: this.cartCount, maxCount: 99 })
// 消息通知:通常最大显示999
Badge({ count: this.messageCount, maxCount: 999 })
实践四:注意角标尺寸适配
角标尺寸应与被装饰组件的尺寸相匹配:
// 小图标(40×40)
Badge({ style: { badgeSize: 16 } }) {
Image($r('app.media.small_icon'))
.width(40)
.height(40)
}
// 大图标(80×80)
Badge({ style: { badgeSize: 24 } }) {
Image($r('app.media.large_icon'))
.width(80)
.height(80)
}
实践五:使用资源引用替代硬编码
将颜色、尺寸等硬编码值提取到 resources 目录中:
// 硬编码(不推荐)
Badge({ style: { badgeColor: '#FF4D4F', badgeSize: 20 } })
// 使用资源引用(推荐)
Badge({ style: { badgeColor: $r('app.color.badge_red'), badgeSize: $r('app.float.badge_size') } })
7.3 性能优化策略
策略一:使用 @Builder 拆分复杂布局
对于复杂的角标布局,可以使用 @Builder 装饰器拆分为独立的构建方法:
@Entry
@Component
struct ComplexLayout {
@State cartCount: number = 5;
build() {
Stack({ alignContent: Alignment.Center }) {
this.CartIcon()
this.ControlButtons()
}
}
@Builder CartIcon() {
Badge({ count: this.cartCount, position: BadgePosition.RightTop }) {
Image($r('app.media.cart'))
.width(80)
.height(80)
}
}
@Builder ControlButtons() {
Row({ space: 20 }) {
Button('-').onClick(() => { this.cartCount--; })
Text(`${this.cartCount}`)
Button('+').onClick(() => { this.cartCount++; })
}
.margin({ top: 100 })
}
}
策略二:避免不必要的状态更新
使用 @Watch 装饰器监听状态变化,只在必要时执行逻辑:
@State cartCount: number = 0;
@Watch('onCartCountChange')
get cartCountDisplay(): number {
return this.cartCount;
}
onCartCountChange(): void {
// 只在数量变化时执行
console.info(`购物车数量变化: ${this.cartCount}`);
}
策略三:使用 conditional rendering
对于条件性显示的角标,使用 if/else 进行条件渲染:
Stack() {
Image($r('app.media.cart'))
// 只有当 count > 0 时才渲染角标
if (this.cartCount > 0) {
Badge({ count: this.cartCount }) {
Text('')
.width(0)
.height(0)
}
}
}
策略四:优化图片资源
使用合适尺寸的图片资源,避免过大或过小的图片:
// 推荐:使用与显示尺寸匹配的图片
Image($r('app.media.cart_80x80'))
.width(80)
.height(80)
// 不推荐:使用过大的图片
Image($r('app.media.cart_512x512'))
.width(80)
.height(80)
八、完整代码示例汇总
8.1 基础购物车角标
@Entry
@Component
struct Index {
@State cartCount: number = 3;
addToCart(): void {
if (this.cartCount < 99) {
this.cartCount++;
}
}
removeFromCart(): void {
if (this.cartCount > 0) {
this.cartCount--;
}
}
clearCart(): void {
this.cartCount = 0;
}
build() {
Stack({ alignContent: Alignment.Center }) {
Column({ space: 60 }) {
Badge({
count: this.cartCount,
position: BadgePosition.RightTop,
maxCount: 99,
style: {
badgeColor: '#FF4D4F',
badgeSize: 20,
color: '#FFFFFF',
fontSize: 12,
borderColor: '#FFFFFF',
borderWidth: 2,
fontWeight: FontWeight.Bold
}
}) {
Image($r('app.media.startIcon'))
.width(80)
.height(80)
}
Row({ space: 20 }) {
Button('-')
.width(60)
.height(60)
.fontSize(30)
.fontColor('#FFFFFF')
.backgroundColor('#FF6B6B')
.borderRadius(30)
.onClick(() => {
this.removeFromCart();
})
Text(`购物车数量: ${this.cartCount}`)
.fontSize(20)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Button('+')
.width(60)
.height(60)
.fontSize(30)
.fontColor('#FFFFFF')
.backgroundColor('#52C41A')
.borderRadius(30)
.onClick(() => {
this.addToCart();
})
}
Button('清空购物车')
.width(200)
.height(48)
.fontSize(18)
.fontColor('#FFFFFF')
.backgroundColor('#FF4D4F')
.borderRadius(24)
.onClick(() => {
this.clearCart();
})
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
.height('100%')
.width('100%')
.backgroundColor('#F5F5F5')
}
}
8.2 资源文件配置
在 entry/src/main/resources/base/element/color.json 中定义颜色:
{
"color": [
{
"name": "badge_red",
"value": "#FF4D4F"
},
{
"name": "badge_white",
"value": "#FFFFFF"
},
{
"name": "text_primary",
"value": "#333333"
},
{
"name": "bg_grey",
"value": "#F5F5F5"
}
]
}
在 entry/src/main/resources/base/element/float.json 中定义尺寸:
{
"float": [
{
"name": "badge_size",
"value": "20"
},
{
"name": "badge_font_size",
"value": "12"
},
{
"name": "icon_size",
"value": "80"
}
]
}
九、总结
9.1 核心要点回顾
- Stack 布局:实现 Z 轴层叠排列,子组件按添加顺序从底向上堆叠
- Badge 组件:封装角标逻辑,支持数字、文字、纯点三种模式
- 状态管理:使用
@State实现响应式更新 - API 24 特性:Alignment 枚举统一,性能优化,嵌套稳定性提升
- 最佳实践:控制子组件数量,避免深层嵌套,利用零值自动隐藏特性
9.2 学习路径建议
- 基础阶段:掌握 Stack、Column、Row 等基础布局容器的用法
- 进阶阶段:深入理解状态管理机制(@State、@Link、@Prop、@Provide/@Consume)
- 高级阶段:学习自定义组件、@Builder 装饰器、性能优化策略
- 实战阶段:结合实际业务场景,实现完整的购物车、消息通知等功能
9.3 常见问题排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 角标不显示 | count 为 0 或负数 | 确保 count > 0 |
| 角标显示 “99+” | count 超过 maxCount | 调整 maxCount 值 |
| 编译报错 “TopRight” | 使用了错误的枚举值 | 替换为 Alignment.TopEnd |
| 编译报错 “textColor” | 使用了错误的属性名 | 替换为 color |
| 图片不显示 | 资源路径错误 | 检查资源文件是否存在 |
| 角标位置错误 | position 参数设置错误 | 确认 BadgePosition 枚举值 |
9.4 扩展学习建议
- 深入学习布局系统:了解 Flex、RelativeContainer、Grid 等其他布局容器
- 状态管理进阶:学习 @Link、@Provide/@Consume、AppStorage 等高级状态管理方式
- 组件封装:学习如何封装可复用的角标组件
- 动画效果:学习如何为角标添加显隐动画、数字滚动动画等效果
- 数据持久化:学习如何使用 Preferences、SQLite 等存储购物车数据
通过本文的学习,相信你已经掌握了使用 Stack 和 Badge 组件实现购物车角标的核心技术。在实际开发中,可以根据具体需求灵活调整布局和样式,打造出符合设计规范的高质量 UI 界面。
更多推荐



所有评论(0)