在 HarmonyOS 6 电商或购物比价类应用中,"❤️收藏"和"🛒加入购物车"图标若只有静态点击反馈,会显得生硬且缺乏质感。理想的交互是:手指按下时图标微微缩小(蓄力感),抬起时弹回并伴随环形进度闪烁(确认感)。
官方购物比价行业实践指出,可通过onTouch监听TouchType.Down/Up结合UIContext.keyframeAnimateTo关键帧动画实现该效果。本文将抛开冗余代码,从零构建一个可复用的商城图标点击动效组件。
一、现象:点击无反馈或动画僵硬
1. 常见错误写法
// ❌ 只用 onClick + animateTo,无法感知"按下/松开",无按压蓄力感 Image($r('app.media.ic_favorite')) .onClick(() => { animateTo({ duration: 200 }, () => { /* 只改变状态 */ }) })onClick无法区分按下和松开,scale变化突兀没有"按下去缩小→松开弹回"的连贯帧
2. 根因揭秘
组件需同时监听:
TouchType.Down:触发缩小(scale ~0.85)+ 计数/状态变更
TouchType.Up:恢复原始尺寸 + 关键帧环闪提示
使用
getUIContext().keyframeAnimateTo()代替旧版animateTo以获得更精确的关键帧控制(ArkUI 推荐做法)
二、完整实现——商城收藏图标按压动效
以下代码可直接放入pages/FavIconAnimPage.ets运行:
// pages/FavIconAnimPage.ets @Entry @Component struct FavIconAnimPage { // 图标容器宽高(控制热区) @State boxSize: number = 56 // 缩放比例:按下缩小,松开复原 @State scaleVal: number = 1.0 // 环形进度(用于 Up 时的闪烁反馈) @State ringVal: number = 0 // 是否已收藏 @State isFav: boolean = false // 点击次数示意 @State clickCount: number = 0 private uiCtx: UIContext | undefined = undefined aboutToAppear(): void { this.uiCtx = this.getUIContext() } build() { Column({ space: 30 }) { Text('HarmonyOS 6 商城图标点击动效') .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ top: 40 }) Text(`已收藏次数:${this.clickCount}`) .fontSize(14) .fontColor('#666') // ===== 核心:Stack 叠放图标 + 环形进度 ===== Stack() { // ① 底层:触摸反馈环形进度(Up时闪一下) Progress({ value: this.ringVal, total: 100, type: ProgressType.Ring }) .width(this.boxSize + 10) .height(this.boxSize + 10) .color('#FF5722') .style({ strokeWidth: 2.5, enableSmoothEffect: true }) // ② 上层:收藏图标(带缩放动画) Image(this.isFav ? $r('app.media.ic_favorite_fill') : $r('app.media.ic_favorite')) .width(28) .height(28) .fillColor(this.isFav ? '#FF5722' : '#888') .scale({ x: this.scaleVal, y: this.scaleVal }) // ★ 核心触摸事件 .onTouch((e: TouchEvent) => { if (e.type === TouchType.Down) { // 按下:缩小蓄力 this.uiCtx?.keyframeAnimateTo({ iterations: 1 }, [ { duration: 80, event: () => { this.scaleVal = 0.82 } } ]) } if (e.type === TouchType.Up) { // 切换收藏状态 this.isFav = !this.isFav this.clickCount++ // 松开:弹回原尺寸 this.uiCtx?.keyframeAnimateTo({ iterations: 1 }, [ { duration: 120, curve: Curve.Friction, event: () => { this.scaleVal = 1.0 } } ]) // 环形进度快闪(0→100→0) this.ringVal = 0 this.uiCtx?.keyframeAnimateTo({ iterations: 1 }, [ { duration: 150, event: () => { this.ringVal = 100 } }, { duration: 100, event: () => { this.ringVal = 0 } } ]) } }) } .width(this.boxSize) .height(this.boxSize) .justifyContent(FlexAlign.Center) Text('👆 按下缩小 / 松开弹回 + 环闪') .fontSize(12) .fontColor('#999') } .width('100%') .height('100%') .backgroundColor('#F6F7FB') .justifyContent(FlexAlign.Center) } }三、关键结构解读
Stack ├── Progress (Ring) ← 环形触摸反馈,Up时 0→100→0 └── Image (收藏图标) ← scale 绑定 @State scaleVal └── onTouch(Down → 缩到0.82 / Up → 弹回1.0 + 切换状态 + 环闪)为什么要这样写?
要素 | 说明 |
|---|---|
| 需捕获 Down/Up 两阶段, |
| 官方推荐替代 |
| 弹回时用摩擦曲线,比线性更有"弹性"质感 |
| 不额外增加布局层级,环形进度默认透明背景,平时不可见 |
热区 | Stack 设固定 |
四、避坑指南
问题 | 原因 | 修复 |
|---|---|---|
动画不执行或无关键帧效果 | 未通过 | 必须在 |
快速连点缩放错乱 | Down/Up 帧相互覆盖 | 可加防抖标志 |
环闪看不到 |
| Up 时先 |
图标被 Progress 挡住 | 子组件顺序反了 | Stack先 Progress 再 Image(后渲染在上层) |
想用于加购 Cart 图标 | 同样逻辑,去掉 Ring 或改用 Badge 数字跳动 | 复用相同 |
五、总结:商城图标点击动效 SOP
用
onTouch而非onClick 区分 Down(缩小)和 Up(弹回)@State scaleVal驱动 Image.scale,Down→0.8x,Up→1.0UIContext.keyframeAnimateTo 做两段关键帧(缩小帧 / 弹回帧),弹回加Curve.Friction可选 Ring Progress 或 Badge 数字 作为 Up 时的视觉确认反馈
Stack 包 Icon + Feedback 层,保证热区≥44dp
核心法则:HarmonyOS 6 商城交互质感 =
onTouch(Down/Up)+keyframeAnimateTo缩放关键帧 + 微反馈层(Ring/Badge)。