一、引言

在移动端应用中,数量选择是最常见的交互场景之一。购物车中调整商品数量、表单中填写年龄或人数、设置中调整音量或亮度——这些场景都需要一个"增加/减少"的数值控制。在传统开发中,实现一个数量选择器需要放置两个按钮(+ 和 -)、一个文本显示区、编写点击事件、处理边界值(不能小于 0、不能大于上限)、管理按钮的禁用状态——即使最简单的"数量加减"也要几十行代码。

HarmonyOS 提供了 Counter 组件——一个专门用于数值增减控制的组件。它将递增和递减两个按钮封装为一个完整的交互单元,通过 onInc/onDec 回调处理值的变化,通过 enableInc/enableDec 自动管理按钮的禁用状态。开发者只需关注"值变了之后做什么",而不需要管理按钮的布局、样式和交互细节。

本文通过一个购物车数量管理 Demo 深入讲解 Counter 组件的核心用法:如何使用 Counter 实现商品数量的加减?如何启用和禁用增减按钮?如何通过 Counter 构建完整的购物车逻辑?

阅读完本文,你将能够:

  • 使用 Counter 组件替代手动的加减按钮布局
  • 掌握 onInc/onDec 回调处理数值变化
  • 掌握 enableInc/enableDec 控制按钮可用状态
  • 构建基于 Counter 的购物车数量管理功能
  • 理解 Counter 组件与 Text 配合展示数值的设计模式

二、Counter 组件 API 总览

2.1 构造函数

Counter()

Counter 的构造函数不接受任何参数——所有行为通过事件回调和状态控制方法完成。Counter 渲染为一个包含递增按钮和递减按钮的复合组件。

2.2 核心 API

Counter 的 API 非常精简,只有四个方法:

interface CounterAttribute {
  onInc(event: VoidCallback): CounterAttribute;
  onDec(event: VoidCallback): CounterAttribute;
  enableInc(value: boolean): CounterAttribute;
  enableDec(value: boolean): CounterAttribute;
}
方法 用途 参数
onInc 递增按钮点击回调 VoidCallback — 无参数的回调函数
onDec 递减按钮点击回调 VoidCallback — 无参数的回调函数
enableInc 控制递增按钮是否可用 boolean — true 可用,false 禁用(灰色不可点击)
enableDec 控制递减按钮是否可用 boolean — true 可用,false 禁用

2.3 Counter 的设计哲学

Counter 与 TextClock 类似,都是"单一职责"组件的代表。但 Counter 的职责范围更窄——它仅负责提供加减按钮的交互,不负责:

  • 显示当前值:Counter 不显示当前数值。开发者需要使用 Text 组件自行展示
  • 管理值范围:Counter 不内置 min/max 属性。开发者需要在 onInc/onDec 回调中检查边界
  • 管理步长:Counter 不内置 step 属性。开发者需要在回调中自行控制每次增减的量

这种"只做一件事"的设计让 Counter 的使用非常灵活——开发者完全控制值的范围、步长和显示方式。

2.4 基本用法模式

@State count: number = 5;

Column() {
  Text('' + this.count)   // 开发者自行显示当前值
    .fontSize(24)

  Counter()
    .onInc(() => {
      if (this.count < 99) { this.count++; }  // 开发者检查上限
    })
    .onDec(() => {
      if (this.count > 0) { this.count--; }   // 开发者检查下限
    })
    .enableInc(this.count < 99)   // 到达上限时禁用 + 按钮
    .enableDec(this.count > 0)    // 到达下限时禁用 - 按钮
}

这个模式展示了 Counter 的核心使用方式:

  1. @State 变量存储当前值
  2. 用 Text 组件显示当前值
  3. onInc/onDec 中修改值,同时检查边界
  4. enableInc/enableDec 控制按钮禁用状态

enableIncenableDec 不仅影响按钮的视觉外观(灰色不可点击),还能防止用户在到达边界后继续点击——即使 onInc 中有边界检查,禁用的按钮仍然提供了更好的 UX 反馈。
在这里插入图片描述

三、Demo 设计:购物车数量管理

3.1 功能概述

Demo 是一个购物车商品数量管理页面,模拟电商 App 购物车的核心功能:

  1. 商品列表:6 款华为产品,每款商品展示名称、单价、当前数量
  2. Counter 数量调整:每个商品使用 Counter 组件进行数量增减,范围 0~99
  3. 实时计价:小计(单价×数量)和合计(所有选中商品总价)实时更新
  4. 全选/单选:通过 Checkbox 控制商品选中状态,影响总计计算
  5. 删除选中:一键删除所有选中的商品
  6. Counter 演示区:3 个演示卡片展示不同的 Counter 用法

3.2 购物车数据结构

interface CartItem {
  id: number;
  name: string;
  price: number;
  image: string;
  quantity: number;
  selected: boolean;
}

每条商品包含 6 个字段:id(唯一标识)、name(商品名称)、price(单价)、image(图片占位符)、quantity(当前数量,Counter 操作的目标值)、selected(是否选中)。

3.3 Counter 在购物车中的使用

每个商品行包含一个 Counter 组件用于调整数量:

Row() {
  Text('数量: ' + item.quantity)
    .fontSize(14)
    .fontColor('#1a1a2e')
    .fontWeight(FontWeight.Bold)
    .margin({ right: 10 })

  Counter()
    .onInc(() => { this.incQty(item.id); })
    .onDec(() => { this.decQty(item.id); })
    .enableInc(item.quantity < 99)
    .enableDec(item.quantity > 0)
}

关键设计:

  • 数量显示在 Counter 左侧:Text 组件显示"数量: N",让用户清楚看到当前值
  • onInc/onDec 调用业务方法incQty(id)decQty(id) 处理数量变化的完整逻辑(创建新数组、更新 @State)
  • enableInc/enableDec 动态计算:当数量达到 99 时 + 按钮自动禁用,数量为 0 时 - 按钮自动禁用
  • 数量为 0 不删除商品:数量可以为 0(类似购物车中暂不购买但保留在列表中的商品),只有"删除选中"才会从列表移除

3.4 增减数量的不可变更新

incQty(id: number): void {
  const newList: CartItem[] = [];
  for (let i = 0; i < this.cart.length; i++) {
    const c = this.cart[i];
    if (c.id === id && c.quantity < 99) {
      newList.push({
        id: c.id, name: c.name, price: c.price,
        image: c.image, quantity: c.quantity + 1, selected: c.selected
      });
    } else {
      newList.push(c);
    }
  }
  this.cart = newList;
}

数量增加时:遍历数组 → 匹配 id 且未达上限 → 创建副本(quantity+1)→ 替换整个数组。边界检查在这里再次执行(c.quantity < 99),即使按钮已禁用,也能防止任何边缘情况。

3.5 计价逻辑

totalPrice(): number {
  let t = 0;
  for (let i = 0; i < this.cart.length; i++) {
    const c = this.cart[i];
    if (c.selected) {
      t = t + c.price * c.quantity;
    }
  }
  return t;
}

总计 = 所有选中商品的(单价 × 数量)之和。取消选中某商品后,该商品不计入总计。价格计算在每次渲染时重新执行,确保始终反映最新的选中状态和数量。

3.6 Counter 演示区

页面底部的演示区展示了三种 Counter 用法模式:

基本用法:手动边界检查,在 onInc/onDec 中用 if 语句限制范围
禁用控制:使用 enableInc/enableDec 自动管理按钮状态,边界条件直接作为参数传入
自定义步长:在 onInc/onDec 中以 5 为增量修改值,实现 step=5 的效果

这些演示展示了 Counter 的灵活性——虽然组件本身不提供 min/max/step 属性,但通过回调中的逻辑控制,可以实现任意范围和步长的计数。

3.7 页面结构

┌──────────────────────────────────────────┐
│ 🛒 购物车(深色标题栏)                   │
├──────────────────────────────────────────┤
│ 📘 Counter 组件说明卡片                   │
├──────────────────────────────────────────┤
│ 共 N 件商品                删除选中      │
├──────────────────────────────────────────┤
│ ┌────────────────────────────────────┐   │
│ │ ☑ 📱 华为 Mate 60 Pro              │   │
│ │   ¥6999  小计 ¥6999               │   │
│ │   数量: 1  [−] [+]                │   │
│ ├────────────────────────────────────┤   │
│ │ ☑ 🎧 华为 FreeBuds Pro 3          │   │
│ │   ¥1499  小计 ¥2998               │   │
│ │   数量: 2  [−] [+]                │   │
│ ├────────────────────────────────────┤   │
│ │ ...更多商品...                      │   │
│ └────────────────────────────────────┘   │
├──────────────────────────────────────────┤
│ 🧪 Counter 属性演示                      │
│ ┌──────────┐┌──────────┐┌──────────┐   │
│ │ 基本用法  ││ 禁用控制  ││ 自定义步长│   │
│ │    5     ││   20     ││    7     │   │
│ │ [−] [+] ││ [−] [+] ││ [−] [+] │   │
│ └──────────┘└──────────┘└──────────┘   │
├──────────────────────────────────────────┤
│ ☑ 全选          合计: ¥xxxxx  已选 N 件 │
└──────────────────────────────────────────┘

在这里插入图片描述

四、Counter 组件的最佳实践

4.1 Counter + Text = 完整的数值控制

Counter 的设计决定了它不能独立使用——它只提供按钮交互,不显示当前值。标准的用法是将 Counter 与 Text 组合:

Row() {
  Text('' + this.count)
    .fontSize(24)
    .fontWeight(FontWeight.Bold)

  Counter()
    .onInc(() => { ... })
    .onDec(() => { ... })
}

将 Text 放在 Counter 旁边(左侧或中间),让用户同时看到当前值和操作按钮。不要将 Counter 单独使用——没有数值显示,用户无法感知当前状态。

4.2 边界保护的"双重保险"

推荐的边界保护模式是同时使用回调中的 if 检查和 enableInc/enableDec:

Counter()
  .onInc(() => {
    if (this.count < MAX) { this.count++; }
  })
  .onDec(() => {
    if (this.count > MIN) { this.count--; }
  })
  .enableInc(this.count < MAX)
  .enableDec(this.count > MIN)
  • enableInc/enableDec:提供 UI 层面的反馈——按钮灰色、不可点击,告诉用户"已到极限"
  • onInc/onDec 中的 if 检查:提供逻辑层面的保护——即使在某些边缘情况下按钮状态未正确更新,值也不会越界
  • 这层"双重保险"在生产环境中尤其重要——UI 状态可能因复杂的异步更新而短暂不一致,逻辑层的检查是最后的安全网

4.3 不可变更新的必要性

在 ArkTS 中,@State 依赖引用比较检测变化。Counter 的 onInc/onDec 回调中,必须创建新的数据副本:

// 正确:创建新数组 + 新对象
. onInc(() => {
  const newList = [];
  for (...) {
    if (match) {
      newList.push({ ...original, quantity: original.quantity + 1 });
    } else {
      newList.push(original);
    }
  }
  this.cart = newList;  // 触发 @State 更新
})

// 错误:直接修改(不会触发 UI 更新)
. onInc(() => {
  item.quantity++;  // 直接修改 @State 数组元素——UI 不刷新
})

4.4 步长的灵活控制

虽然 Counter 没有 step 属性,但自定义步长非常简单——在回调中以任意增量修改值:

// step = 5
Counter()
  .onInc(() => {
    if (this.count < 50) { this.count += 5; }
  })
  .onDec(() => {
    if (this.count > 0) { this.count -= 5; }
  })
  .enableInc(this.count < 50)
  .enableDec(this.count >= 5)  // 注意:下限检查也要适配步长

步长为 5 时,enableDec 的判断条件是 >= 5 而非 > 0——因为减少 5 后不能小于 0。步长越大,边界条件越需要注意。

4.5 Counter 与购物车 UX

在购物车场景中,Counter 的使用有一些特殊考量:

  • 数量为 0 的处理:Demo 中数量归零不删除商品,用户可以重新加回来。实际产品中通常在数量归零时弹出确认"是否删除该商品?"
  • 避免快速点击:Counter 的 onInc/onDec 在主线程执行,频繁点击触发的数组重建在 6 条数据量级下性能无影响。但如果购物车有 100+ 商品,考虑节流处理
  • 价格实时更新:Counter 每次增减都触发 @State 更新 → 整个组件重新渲染 → totalPrice() 重新计算。对于购物车场景(通常 <50 件商品),这种计算开销可以忽略

五、完整代码结构

CounterPage (~320 行)
├── 数据模型
│   └── interface CartItem — 购物车商品结构
├── 状态变量
│   ├── @State cart: CartItem[] — 6 件商品
│   ├── @State selectAll: boolean — 全选状态
│   └── @State demoVal1/2/3 — Counter 演示值
├── 业务方法
│   ├── incQty(id) — 增加数量(不可变更新)
│   ├── decQty(id) — 减少数量(不可变更新)
│   ├── toggleItem(id) — 切换选中状态
│   ├── toggleSelectAll() — 全选/取消全选
│   ├── removeSelected() — 删除选中商品
│   ├── totalPrice() — 计算选中商品总价
│   └── totalCount() — 计算选中商品总数
├── 视图
│   ├── 标题栏 — 🛒 购物车
│   ├── 说明卡片 — Counter 组件介绍
│   ├── 统计栏 — 商品数量 + 删除选中
│   ├── 商品列表 — 6 条商品 + Counter + Checkbox
│   ├── 空购物车占位
│   ├── Counter 演示区 — 基本/禁用/步长
│   └── 底部结算栏 — 全选 + 合计 + 已选
└── (无 @Builder — 全部内联)

六、总结

本文通过一个购物车数量管理 Demo 深入讲解了 HarmonyOS 中的 Counter 计数器组件。Counter 将递增和递减按钮封装为独立的交互组件,通过 onInc/onDec 事件回调和 enableInc/enableDec 状态控制,为数值增减场景提供了统一的交互方案。

核心要点回顾:

  1. Counter 是"纯按钮"组件:它只负责提供加减按钮的交互,不显示当前值、不管理范围、不控制步长。这赋予了 Counter 极大的灵活性——开发者可以自由控制值的展示方式、边界条件和步长逻辑。

  2. onInc/onDec 处理值变化:在回调中修改 @State 变量值、执行边界检查。对于数组中的值,需要创建新数组副本以触发 @State 更新。

  3. enableInc/enableDec 控制按钮状态:两个布尔值直接控制按钮的可用/禁用。边界条件作为参数传入,按钮状态自动跟随值的变化更新。

  4. Counter + Text = 完整方案:Counter 本身不显示值,需要配合 Text 组件展示当前数值。Row 容器将两者组合在一起,形成完整的数值控制单元。

  5. 不可变更新是必须的:在 Counter 回调中修改 @State 数组时,必须创建新数组和新对象,不能直接修改原数组元素。

Counter 是 ArkUI 中"小而美"组件的代表——API 只有 4 个方法,却能覆盖购物车、表单、设置等大量场景中的数值增减需求。它不试图做所有事情(不显示值、不管范围),但这种克制的设计恰恰让它与 Text、Checkbox、Slider 等其他组件完美配合,构建出灵活而强大的交互界面。

七、扩展思考

Counter 解决了基本的数值增减交互,但在实际项目中,数量控制还有更多变化:

Counter 样式的局限性:Counter 组件不提供样式自定义 API(如按钮颜色、大小、间距)。它的外观由系统主题决定,开发者无法直接修改。对于需要高度自定义样式的场景(如电商 App 中圆形的 +/- 按钮),可能需要回到自定义布局方案。

长按连续加减:Counter 的 onInc/onDec 在点击时触发一次。需要长按连续加减的场景(如长按 + 按钮持续增加数量),需要自行处理长按手势和定时器逻辑。

与本机 TextInput 配合:某些购物车允许用户直接在输入框中输入数量。Counter 按钮可以作为输入框旁边的辅助控件——+ 和 - 按钮修改输入框的值,输入框也可以直接输入数字。

库存同步:真实购物车中,商品数量的上限由库存决定。Demo 中使用固定的上限 99,实际项目中需要从后端获取库存数据并动态设置 enableInc 条件(item.quantity < stockCount)。

Counter 与高级 Counter(arkui.advanced):HarmonyOS 还提供了 @ohos.arkui.advanced.Counter 中的 CounterComponent,支持 INLINE(内联显示值)、LIST(带标签)、COMPACT(紧凑型)和 INLINE_DATE(日期型)四种类型,以及内置的 min/max/step/value 属性和 onChange 回调。对于需要内置值展示和范围管理的场景,高级 Counter 是更完整的选择。

理解 Counter 的定位——轻量级加减按钮组件——是正确使用它的关键。它不是"数值输入组件",而是"数值增减交互组件"。值的展示、边界的管理、步长的控制都由开发者掌控,这使得 Counter 在保持简单的同时具备强大的适应性。

通过本文的 Demo——购物车数量管理,你将 Counter 的 onInc/onDec/enableInc/enableDec 应用到真实的购物车业务场景中,构建了一个完整的商品数量管理页面。这个模式可以直接作为任何电商购物车页面的起点模板。

Logo

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

更多推荐