在 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 + 切换状态 + 环闪)

为什么要这样写?

要素

说明

onTouchvs onClick

需捕获 Down/Up 两阶段,onClick只有 Up 且无法做按压缩小

keyframeAnimateTo

官方推荐替代 animateTo控制逐帧属性变化,支持多段 duration

Curve.Friction

弹回时用摩擦曲线,比线性更有"弹性"质感

Stack + Ring Progress

不额外增加布局层级,环形进度默认透明背景,平时不可见

热区

Stack 设固定 56×56,比单纯 Image 更易点中(符合人机交互建议)


四、避坑指南

问题

原因

修复

动画不执行或无关键帧效果

未通过 getUIContext()调用

必须在 aboutToAppear中保存 this.getUIContext()再调 keyframeAnimateTo

快速连点缩放错乱

Down/Up 帧相互覆盖

可加防抖标志 isAnimating,或在 Down 时先 finishAnimation

环闪看不到

ringVal初始非0 或被同步置回

Up 时先 this.ringVal = 0再启动关键帧

图标被 Progress 挡住

子组件顺序反了

Stack 先 Progress 再 Image(后渲染在上层)

想用于加购 Cart 图标

同样逻辑,去掉 Ring 或改用 Badge 数字跳动

复用相同 onTouch + scale结构即可


五、总结:商城图标点击动效 SOP

  1. onTouch而非 onClick​ 区分 Down(缩小)和 Up(弹回)

  2. @State scaleVal驱动 Image.scale,Down→0.8x,Up→1.0

  3. UIContext.keyframeAnimateTo​ 做两段关键帧(缩小帧 / 弹回帧),弹回加 Curve.Friction

  4. 可选 Ring Progress 或 Badge 数字​ 作为 Up 时的视觉确认反馈

  5. Stack 包 Icon + Feedback 层,保证热区≥44dp

核心法则:HarmonyOS 6 商城交互质感 = onTouch(Down/Up)+ keyframeAnimateTo缩放关键帧 + 微反馈层(Ring/Badge)。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐