鸿蒙新特性——Counter 计数器组件详解
一、引言
在移动端应用中,数量选择是最常见的交互场景之一。购物车中调整商品数量、表单中填写年龄或人数、设置中调整音量或亮度——这些场景都需要一个"增加/减少"的数值控制。在传统开发中,实现一个数量选择器需要放置两个按钮(+ 和 -)、一个文本显示区、编写点击事件、处理边界值(不能小于 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 的核心使用方式:
- 用
@State变量存储当前值 - 用 Text 组件显示当前值
- 在
onInc/onDec中修改值,同时检查边界 - 用
enableInc/enableDec控制按钮禁用状态
enableInc 和 enableDec 不仅影响按钮的视觉外观(灰色不可点击),还能防止用户在到达边界后继续点击——即使 onInc 中有边界检查,禁用的按钮仍然提供了更好的 UX 反馈。
三、Demo 设计:购物车数量管理
3.1 功能概述
Demo 是一个购物车商品数量管理页面,模拟电商 App 购物车的核心功能:
- 商品列表:6 款华为产品,每款商品展示名称、单价、当前数量
- Counter 数量调整:每个商品使用 Counter 组件进行数量增减,范围 0~99
- 实时计价:小计(单价×数量)和合计(所有选中商品总价)实时更新
- 全选/单选:通过 Checkbox 控制商品选中状态,影响总计计算
- 删除选中:一键删除所有选中的商品
- 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 状态控制,为数值增减场景提供了统一的交互方案。
核心要点回顾:
-
Counter 是"纯按钮"组件:它只负责提供加减按钮的交互,不显示当前值、不管理范围、不控制步长。这赋予了 Counter 极大的灵活性——开发者可以自由控制值的展示方式、边界条件和步长逻辑。
-
onInc/onDec 处理值变化:在回调中修改 @State 变量值、执行边界检查。对于数组中的值,需要创建新数组副本以触发 @State 更新。
-
enableInc/enableDec 控制按钮状态:两个布尔值直接控制按钮的可用/禁用。边界条件作为参数传入,按钮状态自动跟随值的变化更新。
-
Counter + Text = 完整方案:Counter 本身不显示值,需要配合 Text 组件展示当前数值。Row 容器将两者组合在一起,形成完整的数值控制单元。
-
不可变更新是必须的:在 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 应用到真实的购物车业务场景中,构建了一个完整的商品数量管理页面。这个模式可以直接作为任何电商购物车页面的起点模板。
更多推荐




所有评论(0)