项目演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、前言

在移动应用开发中,购物车角标是电商类、社交类应用中最常见的 UI 模式之一。微信、淘宝、京东等主流应用无一例外地采用了这一视觉范式——在购物车图标右上角显示一个红色圆形标记,用于提示用户当前购物车中的商品数量。

对于鸿蒙原生开发者而言,如何在 HarmonyOS NEXT(API 24)上使用 ArkTS 高效、优雅地实现这一布局,是掌握鸿蒙 UI 开发的重要技能。本文将从一个完整的可运行示例出发,深入剖析 Stack 布局容器与 Badge 角标组件的组合使用,详细讲解布局原理、状态管理、样式定制等核心技术。

本文面向具有一定 ArkTS 基础的开发者,假设读者已了解 @Entry@Component@State 等基础概念。通过本文的学习,读者将能够:

  1. 理解 Stack 层叠布局的核心原理
  2. 掌握 Badge 角标组件的完整 API
  3. 实现购物车角标的动态更新功能
  4. 了解 API 24 的新特性和注意事项
  5. 掌握性能优化和最佳实践

二、开发环境配置

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 })  // 手动计算位置
}

这种手动实现存在以下问题:

  1. 位置计算复杂:不同尺寸的图标需要不同的偏移量
  2. 长数字处理困难:超过 99 需要手动截断显示 “99+”
  3. 零值处理繁琐:数量为 0 时需要手动隐藏角标
  4. 样式不一致:需要手动维护角标样式与设计规范一致
  5. 无障碍支持缺失:手动实现的角标不支持无障碍属性

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 需求分析

我们需要实现一个购物车角标页面,包含以下功能:

  1. 购物车图标展示:居中显示购物车图标
  2. 角标数字显示:在图标右上角显示商品数量
  3. 数量控制按钮:提供增加、减少、清空操作
  4. 状态响应:按钮操作后角标实时更新

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 有以下优势:

  1. 层叠能力:如果将来需要在页面上叠加遮罩、弹窗等元素,Stack 天然支持
  2. 简化布局:Stack 的 alignContent 参数可以一次性设置所有子组件的对齐方式
  3. 性能优化: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    // 粗体
  }
})

关键配置说明

  1. count: this.cartCount:将角标数字与状态变量绑定,实现响应式更新
  2. position: BadgePosition.RightTop:角标显示在右上角(图标角标的标准位置)
  3. maxCount: 99:超过 99 显示 “99+”,符合电商应用的常见设计
  4. 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 的关系

当设置了 fontSizebadgeSize 小于 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 核心要点回顾

  1. Stack 布局:实现 Z 轴层叠排列,子组件按添加顺序从底向上堆叠
  2. Badge 组件:封装角标逻辑,支持数字、文字、纯点三种模式
  3. 状态管理:使用 @State 实现响应式更新
  4. API 24 特性:Alignment 枚举统一,性能优化,嵌套稳定性提升
  5. 最佳实践:控制子组件数量,避免深层嵌套,利用零值自动隐藏特性

9.2 学习路径建议

  1. 基础阶段:掌握 Stack、Column、Row 等基础布局容器的用法
  2. 进阶阶段:深入理解状态管理机制(@State、@Link、@Prop、@Provide/@Consume)
  3. 高级阶段:学习自定义组件、@Builder 装饰器、性能优化策略
  4. 实战阶段:结合实际业务场景,实现完整的购物车、消息通知等功能

9.3 常见问题排查

问题现象 可能原因 解决方案
角标不显示 count 为 0 或负数 确保 count > 0
角标显示 “99+” count 超过 maxCount 调整 maxCount 值
编译报错 “TopRight” 使用了错误的枚举值 替换为 Alignment.TopEnd
编译报错 “textColor” 使用了错误的属性名 替换为 color
图片不显示 资源路径错误 检查资源文件是否存在
角标位置错误 position 参数设置错误 确认 BadgePosition 枚举值

9.4 扩展学习建议

  1. 深入学习布局系统:了解 Flex、RelativeContainer、Grid 等其他布局容器
  2. 状态管理进阶:学习 @Link、@Provide/@Consume、AppStorage 等高级状态管理方式
  3. 组件封装:学习如何封装可复用的角标组件
  4. 动画效果:学习如何为角标添加显隐动画、数字滚动动画等效果
  5. 数据持久化:学习如何使用 Preferences、SQLite 等存储购物车数据

通过本文的学习,相信你已经掌握了使用 Stack 和 Badge 组件实现购物车角标的核心技术。在实际开发中,可以根据具体需求灵活调整布局和样式,打造出符合设计规范的高质量 UI 界面。

Logo

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

更多推荐