大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

点赞按钮看起来只是一个很小的交互,但如果希望点击之后不是简单地“变红”或“放大一下”,而是经历按压、旋转放大、回弹三个连续阶段,普通的一段式动画就开始显得不够用了。

ArkUI 提供的 keyframeAnimateTo 正适合处理这种场景:把一次完整动效拆成若干关键帧,每一段分别设置持续时间、动画曲线和状态变化。华为官方当前的动画使用指导也把关键帧动画定义为通过 UIContext 的 keyframeAnimateTo 指定多个关键帧状态,实现分段动画。

这次就用一个最小的点赞按钮,把“缩放 → 旋转放大 → 回弹”完整串起来,同时处理动画结束状态以及连续点击问题。

一、为什么不用一个 animateTo 直接做完

如果目标只是:

scale: 1.0 → 1.2

那么 animateTo 已经足够。

但点赞动效通常不是单一的起点到终点,而更像:

初始状态
   ↓
阶段 1:快速缩小,形成按压感
   ↓
阶段 2:放大 + 轻微旋转
   ↓
阶段 3:回弹到正常大小和角度

这里真正重要的不是“同时改变多少个属性”,而是这些状态需要按时间顺序依次出现。

如果用多个 animateTo 再手工串回调,动画阶段越多,控制逻辑越容易散开。keyframeAnimateTo 则直接把这些阶段组织成关键帧数组。

官方最新动画指导给出的关键帧案例也是这个思路:先把动画拆成若干阶段,再分别设置每段的 duration、曲线以及状态变化闭包。

所以,这个点赞动画的技术切入点可以进一步缩小为:

使用 ArkUI keyframeAnimateTo 对 scale 和 rotate 两个图形变换属性进行分阶段控制,并通过结束回调和业务状态位处理重复点击。

这比单纯演示“怎么调用 API”更接近真实交互组件的写法。

二、先把官方规则弄清楚

keyframeAnimateTo 属于 ArkUI 动画能力,是 UIContext 提供的接口。官方当前 API 页面仍保留该接口;ArkTS 侧接口从 API version 11 开始支持,元服务从 API version 12 开始支持。系统能力为 SystemCapability.ArkUI.ArkUI.Full。

当前官方资料中,关键帧动画仍然作为 ArkUI 的正式动画能力列出,并没有被标记为废弃;官方 2026 年更新的动画使用指导和 FAQ 也继续使用 this.getUIContext().keyframeAnimateTo(...)。

本文这个最小场景不访问相机、位置、振动、网络等受权限控制的系统能力,因此不需要为了关键帧动画额外配置权限,也不需要修改 module.json5 的 requestPermissions。

几个与本文直接相关的点可以先整理清楚:

项目本文使用情况
UI 框架ArkUI
开发语言ArkTS
核心接口UIContext.keyframeAnimateTo
ArkTS 起始版本API version 11
元服务支持API version 12 起
系统能力SystemCapability.ArkUI.ArkUI.Full
关键帧参数duration、curve、event
整体参数iterations、onFinish 等
权限本例无需额外权限
module.json5本例无需新增配置

官方当前动画性能指导还明确把 scale、rotate、translate 等列为可以用于属性动画的图形变换属性,并建议在能够通过图形变换完成效果时,尽量避免为了动画频繁改变布局属性。

这也是本文不用修改点赞图标 width、height,而选择 scale 的原因。

【建议插图1:华为开发者联盟 keyframeAnimateTo API 页面中接口及版本信息】

三、搭一个最小点赞场景

为了让示例尽量容易复现,这里不依赖图片资源,直接使用一个心形字符作为演示对象。

最终希望得到这样的状态:

未点赞
  ♥  scale=1.0  rotate=0

点击
  ↓
第一阶段:scale=0.82
  ↓
第二阶段:scale=1.20  rotate=12°
  ↓
第三阶段:scale=1.0   rotate=0°
  ↓
动画结束

业务状态和动画状态要分开。

点赞与否由:

@State private liked: boolean = false;

控制。

动画表现由:

@State private heartScale: number = 1.0;
@State private heartRotate: number = 0;

控制。

此外再准备:

private isAnimating: boolean = false;

它不负责界面渲染,只负责阻止一次动画尚未结束时再次启动同一套关键帧。

这个区分很重要:liked 是业务状态,heartScale / heartRotate 是视觉状态,isAnimating 是交互控制状态。

四、核心代码实现

下面代码按照官方 keyframeAnimateTo 的调用形式组织为最小示例。代码重点不是心形图标本身,而是三个关键帧如何共同修改同一组状态变量。

@Entry
@Component
struct LikeKeyframeDemo {
  @State private liked: boolean = false;
  @State private heartScale: number = 1.0;
  @State private heartRotate: number = 0;

  private isAnimating: boolean = false;

  private playLikeAnimation(): void {
    if (this.isAnimating) {
      return;
    }

    this.isAnimating = true;
    this.liked = !this.liked;

    // 明确本轮动画的起始状态
    this.heartScale = 1.0;
    this.heartRotate = 0;

    this.getUIContext().keyframeAnimateTo(
      {
        iterations: 1,
        onFinish: () => {
          this.heartScale = 1.0;
          this.heartRotate = 0;
          this.isAnimating = false;
        }
      },
      [
        {
          // 第一阶段:快速缩小,形成按压感
          duration: 120,
          curve: Curve.EaseOut,
          event: () => {
            this.heartScale = 0.82;
            this.heartRotate = 0;
          }
        },
        {
          // 第二阶段:放大并轻微旋转
          duration: 160,
          curve: Curve.EaseOut,
          event: () => {
            this.heartScale = 1.20;
            this.heartRotate = 12;
          }
        },
        {
          // 第三阶段:回到稳定状态
          duration: 220,
          curve: Curve.EaseInOut,
          event: () => {
            this.heartScale = 1.0;
            this.heartRotate = 0;
          }
        }
      ]
    );
  }

  build() {
    Column({ space: 20 }) {
      Text(this.liked ? '♥' : '♡')
        .fontSize(72)
        .fontColor(this.liked ? '#E84057' : '#7A7A7A')
        .scale({
          x: this.heartScale,
          y: this.heartScale
        })
        .rotate({
          angle: this.heartRotate
        })
        .onClick(() => {
          this.playLikeAnimation();
        })

      Text(this.liked ? '已点赞' : '点击点赞')
        .fontSize(16)
        .fontColor('#666666')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这里没有使用额外 import,也没有引入第三方动画库。

真正需要关注的是 keyframeAnimateTo 的第二个参数:它不是描述三个互相独立的动画,而是描述同一次动画过程中的三个连续目标状态。华为官方 FAQ 中也采用相同结构,通过多个包含 duration 和 event 的关键帧依次修改状态。

【建议插图2:DevEco Studio 中点击前、第二关键帧放大旋转、动画结束后三个状态对比】

五、三个阶段到底是怎么连接起来的

1. duration 是“这一段”的时间

这是 keyframeAnimateTo 很容易理解错的地方。

例如:

[
  { duration: 120, ... },
  { duration: 160, ... },
  { duration: 220, ... }
]

表示三段动画分别持续 120ms、160ms、220ms,而不是三个关键帧分别发生在整个动画的 120ms、160ms、220ms 位置。

因此这个示例的一轮关键帧总时长为:

120 + 160 + 220 = 500ms

官方关键帧示例同样是分别为每一段指定独立 duration,这正是关键帧动画适合制作“多节奏”微交互的原因。

2. event 改的是目标状态

第一段:

event: () => {
  this.heartScale = 0.82;
}

并不是要求开发者自己计算 1.0、0.99、0.98……直到 0.82。

开发者声明这一段结束时需要达到的状态,动画框架负责产生中间的连续变化。

第二段同理:

event: () => {
  this.heartScale = 1.20;
  this.heartRotate = 12;
}

于是状态关系变成:

scale: 0.82 → 1.20
rotate: 0 → 12

第三段再回到:

scale: 1.0
rotate: 0

这和逐帧计算动画并不是一回事。官方资料将 keyframeAnimateTo 归类为属性动画相关能力,而真正需要逐帧感知和控制时,还有 Animator 一类更细粒度的方案。

3. 为什么这里用 scale,而不是改 width / height

点赞图标只是视觉上变大、变小,没有改变页面真实布局尺寸的需求。

所以:

.scale({
  x: this.heartScale,
  y: this.heartScale
})

比反复修改:

.width(...)
.height(...)

更符合这个场景。

官方动画性能指导专门比较了布局属性和图形变换属性,并建议能够通过图形变换实现的动画尽量使用图形变换,以减少不必要的布局计算。

对点赞、收藏、按钮反馈、头像轻动效这一类微交互来说,这个选择尤其自然。

六、动画结束回调不只是拿来打日志

onFinish 在这个示例里承担两个任务。

一个是兜底恢复视觉状态:

this.heartScale = 1.0;
this.heartRotate = 0;

另一个是释放点击锁:

this.isAnimating = false;

这样动画播放期间和播放结束之后的职责就很清楚:

点击
 ↓
isAnimating = true
 ↓
执行三个关键帧
 ↓
onFinish
 ↓
恢复稳定状态
 ↓
isAnimating = false

官方 Native 动画资料同样把“全局动画结束回调”列为关键帧动画支持的能力之一。

实际业务中,这个回调还可以用于衔接后续纯 UI 状态,但不要把耗时任务塞进动画结束逻辑。

七、中途再次点击,应该怎么处理

点赞按钮还有一个实际问题:500ms 动画没有结束,用户又点了一次怎么办?

如果不做控制,第二次调用又会启动新的关键帧状态变化,业务上的 liked 也可能再次翻转。最后看到的问题未必是“动画不能播放”,而可能是视觉状态和业务状态越来越难判断。

本文采用最简单的一种策略:

if (this.isAnimating) {
  return;
}

也就是动画期间忽略重复点击。

它适合持续时间很短、重复触发没有业务价值的微交互。

但这不是所有业务的唯一处理方式。

如果需求是“用户连续点击多少次,就必须准确记录多少次”,就不能简单丢弃输入。这时应该把业务事件处理与动画播放拆开:业务计数照常处理,动画则根据产品设计决定重启、排队还是合并。

如果动画本身还要求暂停、停止、恢复、精确控制进度,那么 keyframeAnimateTo 这种触发式关键帧动画也未必是最佳抽象。官方 Native 动画文档明确区分了触发式 animateTo / keyframeAnimateTo 与可持久控制的 Animator:需要反复启动、停止、监听状态等精细生命周期控制时,应考虑 Animator 类能力。

所以,“重复点击怎么处理”首先是交互策略问题,然后才是动画 API 问题。

【建议插图3:快速连续点击点赞按钮时,通过 isAnimating 阻止动画重复启动的调用流程图】

八、几个容易出现的理解偏差

把关键帧当成 setTimeout

不建议用多个 setTimeout 人工拼:

120ms 后改状态
280ms 后再改状态
500ms 后恢复

关键帧动画本身已经提供了分段时长和状态描述。再额外维护一套计时逻辑,只会让动画生命周期更分散。

把业务状态和动画状态绑成一个变量

liked 表示是否点赞。

heartScale 表示当前视觉缩放。

它们不是一回事。

如果直接让一个变量同时承担业务含义和动画中间态,后续增加取消点赞、网络请求或状态同步时会很难维护。

为了“放大”直接修改布局尺寸

视觉缩放优先考虑 scale。官方性能指导也建议,在效果能够通过图形变换属性实现时,尽量减少对布局属性的频繁动画修改。

忽略重复触发

动画代码本身只有十几行,但真实组件通常必须考虑重复点击。

至少应该明确一种策略:忽略、重启、排队或者独立播放,而不是让多次调用自然叠在一起。

九、实际项目中可以这样排查

如果 keyframeAnimateTo 没有达到预期,可以按这个顺序检查:

  1. 先确认 API 版本。 ArkTS keyframeAnimateTo 从 API version 11 开始支持,不要把更低 API 环境和当前文档混在一起。
  2. 确认调用对象。 它是 UIContext 的接口,当前官方示例使用 this.getUIContext().keyframeAnimateTo(...)。
  3. 确认属性是否真正绑定到了状态变量。 只在普通局部变量里修改值,不一定能够形成预期的 UI 状态更新。
  4. 检查每个关键帧的 duration。 duration 描述的是当前这一段,不是整条时间轴上的绝对时刻。
  5. 检查 event 是否真的产生属性变化。 如果前后目标值相同,就没有可观察的过渡。
  6. 再看重复点击。 如果动画偶尔表现异常,先确认是不是一次动画尚未结束又启动了下一次。
  7. 最后检查属性选型。 只是做视觉缩放、旋转、平移时,优先考虑 scale、rotate、translate 这类图形变换,而不是先改布局尺寸。

【建议插图4:DevEco Studio 中在三个 event 内增加调试日志,观察关键帧状态设置顺序】

十、关键帧动画适合什么业务

keyframeAnimateTo 最适合的并不是“所有复杂动画”,而是能够明确拆成几个连续状态的属性动画。

例如:

点赞:按压 → 放大 → 回弹
收藏:缩小 → 翻转 → 恢复
错误提示:左移 → 右移 → 回中
按钮反馈:下压 → 释放 → 回弹
卡片强调:放大 → 停留 → 恢复

华为官方动画使用指导中的左右抖动案例,本质上也是把一次视觉反馈拆成多个连续状态。

反过来,如果只有:

A → B

一段普通过渡,animateTo 往往更直接。

如果需要每一帧参与业务计算,或者需要暂停、恢复、seek、停止等更细粒度控制,则应该继续评估 Animator 等能力,而不是强行把所有逻辑塞进关键帧。

开发经验总结

这个点赞示例真正值得迁移到其他项目里的,不是 0.82、1.20 或 12° 这些具体参数,而是动画的组织方式。

第一,把视觉需求先拆成状态,不要一上来就写动画 API。

第二,keyframeAnimateTo 的优势是能够给多个连续阶段分别设置 duration、curve 和状态变化,适合有明确节奏变化的微交互。官方当前文档仍把它作为 ArkUI 的关键帧动画能力持续维护。

第三,业务状态、视觉状态、交互控制状态最好分开。点赞与否、图标当前缩放多少、动画能不能再次启动,是三个不同的问题。

第四,能通过 scale、rotate、translate 完成的纯视觉动画,不必为了效果去频繁改变组件布局尺寸。

第五,多阶段动画一定要考虑“动画还没结束时再次触发怎么办”。对于本文这种 500ms 左右的短微交互,锁定本轮动画、结束后释放,是一种结构简单的处理方式;如果业务不能丢点击,则需要进一步把业务事件与视觉动画解耦。

一个动画从“能动”变成“好维护”,往往就是从这些状态边界开始的。

如果你的项目里也有点赞、收藏、关注、错误提醒之类的小动效,可以先看看它到底是一个简单的 A→B 过渡,还是已经自然形成了三四个阶段。后者通常就是 keyframeAnimateTo 更值得考虑的地方。

参考资料

  1. 《关键帧动画(keyframeAnimateTo)》
    华为开发者联盟
    链接:HarmonyOS 官方 keyframeAnimateTo API 文档

  2. 《优化动画性能》
    华为开发者联盟
    链接:HarmonyOS 官方动画使用与性能指导

  3. 《实现一个左右抖动的动画效果》
    华为开发者联盟 FAQ
    链接:HarmonyOS 官方 ArkUI FAQ:关键帧动画实现左右抖动

  4. 《如何实现组件依次展开和淡入的动画效果》
    华为开发者联盟 FAQ
    链接:HarmonyOS 官方 ArkUI FAQ:keyframeAnimateTo 分段动画示例

  5. 《动画概述》
    华为开发者联盟
    链接:HarmonyOS 官方 ArkUI Native 动画概述

  6. 《ArkUI_NativeAnimateAPI_1》
    华为开发者联盟
    链接:HarmonyOS 官方 ArkUI Native 动画 API

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

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

更多推荐