不止淡入淡出:keyframeAnimateTo 做一个多阶段点赞动画【鸿蒙心迹】

大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
前言
点赞按钮看起来只是一个很小的交互,但如果希望点击之后不是简单地“变红”或“放大一下”,而是经历按压、旋转放大、回弹三个连续阶段,普通的一段式动画就开始显得不够用了。
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 没有达到预期,可以按这个顺序检查:
- 先确认 API 版本。 ArkTS
keyframeAnimateTo从 API version 11 开始支持,不要把更低 API 环境和当前文档混在一起。 - 确认调用对象。 它是
UIContext的接口,当前官方示例使用this.getUIContext().keyframeAnimateTo(...)。 - 确认属性是否真正绑定到了状态变量。 只在普通局部变量里修改值,不一定能够形成预期的 UI 状态更新。
- 检查每个关键帧的 duration。
duration描述的是当前这一段,不是整条时间轴上的绝对时刻。 - 检查 event 是否真的产生属性变化。 如果前后目标值相同,就没有可观察的过渡。
- 再看重复点击。 如果动画偶尔表现异常,先确认是不是一次动画尚未结束又启动了下一次。
- 最后检查属性选型。 只是做视觉缩放、旋转、平移时,优先考虑
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 更值得考虑的地方。
参考资料
-
《关键帧动画(keyframeAnimateTo)》
华为开发者联盟
链接:HarmonyOS 官方 keyframeAnimateTo API 文档 -
《优化动画性能》
华为开发者联盟
链接:HarmonyOS 官方动画使用与性能指导 -
《实现一个左右抖动的动画效果》
华为开发者联盟 FAQ
链接:HarmonyOS 官方 ArkUI FAQ:关键帧动画实现左右抖动 -
《如何实现组件依次展开和淡入的动画效果》
华为开发者联盟 FAQ
链接:HarmonyOS 官方 ArkUI FAQ:keyframeAnimateTo 分段动画示例 -
《动画概述》
华为开发者联盟
链接:HarmonyOS 官方 ArkUI Native 动画概述 -
《ArkUI_NativeAnimateAPI_1》
华为开发者联盟
链接:HarmonyOS 官方 ArkUI Native 动画 API
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐



所有评论(0)