一、引言
在移动端应用中,数量选择是最常见的交互场景之一。购物车中调整商品数量、表单中填写年龄或人数、设置中调整音量或亮度——这些场景都需要一个"增加/减少"的数值控制。在传统开发中,实现一个数量选择器需要放置两个按钮(+ 和 -)、一个文本显示区、编写点击事件、处理边界值(不能小于 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 非常精简,只有四个方法:
interfaceCounterAttribute{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 基本用法模式
@Statecount: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 购物车数据结构
interfaceCartItem{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{constnewList:CartItem[]=[];for(leti=0;i<this.cart.length;i++){constc=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{lett=0;for(leti=0;i<this.cart.length;i++){constc=this.cart[i];if(c.selected){t=t+c.price*c.quantity;}}returnt;}总计 = 所有选中商品的(单价 × 数量)之和。取消选中某商品后,该商品不计入总计。价格计算在每次渲染时重新执行,确保始终反映最新的选中状态和数量。
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(()=>{constnewList=[];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 = 5Counter().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 应用到真实的购物车业务场景中,构建了一个完整的商品数量管理页面。这个模式可以直接作为任何电商购物车页面的起点模板。