HarmonyOS 动画实战:从「喵屿」看提醒与删除动效的三种实现
-
- 引言
- 一、鸿蒙端属性动画简介
-
- [1. 基本属性动画(animation)](#1. 基本属性动画(animation))
- [2. 显式动画(animateTo)](#2. 显式动画(animateTo))
- [3. 关键帧动画(keyframeAnimateTo)](#3. 关键帧动画(keyframeAnimateTo))
- 二、喵屿项目中三种动画实现
-
- [2.1 删除模式抖动(setInterval + animation)](#2.1 删除模式抖动(setInterval + animation))
- [2.2 飞入垃圾桶(animateTo)](#2.2 飞入垃圾桶(animateTo))
- [2.3 表单校验抖动(keyframeAnimateTo)](#2.3 表单校验抖动(keyframeAnimateTo))
- 三、总结与最佳实践
引言
在移动应用体验设计中,动画不仅仅是视觉点缀,更是用户与界面交互的情感纽带。恰当的动画能降低认知负担、增强操作反馈、提升产品质感,尤其在表单校验、删除确认等高频场景中,动画的缺失往往会让用户感到"卡顿"或"不确定"。然而,如何在不同平台上高效实现自然、连贯且可复用的动效,始终是前端开发的挑战之一。
「喵屿」App 作为一款宠物管理类应用,在 HarmonyOS 平台上深度实践了 ArkUI 动画体系,针对批量删除提示 、单个删除确认 、表单校验失败三种典型场景,分别采用持续抖动、飞入目标和关键帧抖动等动效方案。本文提炼自该 App 真实代码,展示动画实现细节,帮助开发者快速在 HarmonyOS 项目中落地高品质动效。
一、鸿蒙端属性动画简介
属性动画的核心是通过动画接口驱动组件属性变化,ArkUI 提供了三种核心动画接口------animation、animateTo、keyframeAnimateTo,分别适用于不同场景,可灵活实现各类属性动画效果。
| 动画接口 | 作用域 | 原理 | 使用场景 |
|---|---|---|---|
| animation | 组件通过属性接口绑定的属性变化 | 自动识别组件的可动画属性变化,为其添加动画;组件接口调用自上而下执行,animation 仅作用于其上方的属性调用,支持为多个属性设置不同动画参数 | 对多个可动画属性配置不同动画参数的场景,需为单个组件的不同属性设置差异化动画 |
| animateTo | 闭包内改变属性引起的界面变化 | 通用函数,对比闭包执行前与闭包内状态变量引起的 UI 差异,为差异部分添加动画;支持多次调用、嵌套使用,闭包内所有属性变化遵循相同动画参数 | 对多个可动画属性配置相同动画参数的场景,需要嵌套动画的场景;多段动画循环可通过 playMode 和 iterations 实现 |
| keyframeAnimateTo | 多个闭包内改变属性引起的分段属性动画 | 通用函数,每一段闭包中的状态变量与前一次状态的差异,单独生成一段动画;支持多次调用,不推荐嵌套,每段动画可单独配置参数 | 同一属性需要实现连续多个动画的场景,避免多次创建动画导致的衔接卡顿 |
1. 基本属性动画(animation)
animation 是最简洁的属性动画实现方式,直接在组件上设置 animation 属性,即可为组件的可动画属性变化添加动画效果,无需额外复杂配置,适合简单场景的快速实现。
核心步骤:声明状态变量 → 将状态变量绑定到组件可动画属性 → 设置 animation 动画参数 → 触发状态变量变化,触发动画。
typescript
import { curves } from '@kit.ArkUI';
@Entry
@Component
struct AttrAnimationDemo3 {
// 控制动画状态的变量
@State animate: boolean = false;
// 声明相关状态变量,绑定组件可动画属性
@State rotateValue: number = 0; // 组件一旋转角度
@State translateX: number = 0; // 组件二水平偏移量
@State opacityValue: number = 1; // 组件透明度
// 构建 UI 界面
build() {
Row() {
// 组件一:旋转+透明度动画
Column() {
}
// 绑定透明度属性
.opacity(this.opacityValue)
// 绑定旋转属性
.rotate({ angle: this.rotateValue })
// 设置 animation 属性,配置动画参数
.animation({
curve: curves.springMotion() // 使用弹簧动画曲线
})
// 样式设置
.backgroundColor('#317AF7')
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.borderRadius(30)
// 点击事件处理
.onClick(() => {
// 切换动画状态
this.animate = !this.animate;
// 修改状态变量,触发属性变化,进而触发动画
this.rotateValue = this.animate ? 90 : 0; // 旋转动画:90度/0度
this.translateX = this.animate ? 50 : 0; // 组件二偏移动画:50px/0px
this.opacityValue = this.animate ? 0.6 : 1; // 透明度动画:0.6/1
})
// 组件二:平移+透明度动画
Column() {
}
// 样式设置
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.backgroundColor('#D94838')
.borderRadius(30)
// 绑定透明度属性
.opacity(this.opacityValue)
// 绑定平移属性
.translate({ x: this.translateX })
// 设置 animation 属性,配置动画参数
.animation({
curve: curves.springMotion() // 使用弹簧动画曲线
})
}
// 容器样式设置
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
说明:点击组件一后,状态变量发生变化,组件一的 rotate 属性、组件二的 translate 和 opacity 属性随之变化,animation 接口自动为这些属性变化添加弹簧动画,实现平滑过渡。
2. 显式动画(animateTo)
animateTo 是显式动画接口,通过传入动画参数和闭包函数,闭包内所有属性变化将遵循统一的动画参数,适合需要为多个组件、多个属性设置相同动画效果的场景,支持嵌套使用,灵活性更高。
接口定义:
typescript
animateTo(value: AnimateParam, event: () => void): void
参数说明:value 为动画参数配置(AnimateParam 对象),event 为闭包函数,闭包内的状态变量变化所引发的 UI 差异,将自动应用该动画参数。
typescript
import { curves } from '@kit.ArkUI';
@Entry
@Component
struct AttrAnimateToDemo2 {
// 控制动画状态的变量
@State animate: boolean = false;
// 声明相关状态变量
@State rotateValue: number = 0; // 组件一旋转角度
@State translateX: number = 0; // 组件二水平偏移量
@State opacityValue: number = 1; // 组件透明度
// 构建 UI 界面
build() {
Row() {
// 组件一
Column() {
}
// 绑定旋转属性
.rotate({ angle: this.rotateValue })
// 样式设置
.backgroundColor('#317AF7')
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.borderRadius(30)
// 点击事件处理
.onClick(() => {
// 调用 animateTo 接口,配置动画参数和闭包
this.getUIContext()?.animateTo({
curve: curves.springMotion() // 使用弹簧动画曲线
}, () => {
// 切换动画状态
this.animate = !this.animate;
// 闭包内修改状态变量,所有属性变化遵循相同动画参数
this.rotateValue = this.animate ? 90 : 0; // 组件一旋转:90度/0度
this.opacityValue = this.animate ? 0.6 : 1; // 组件二透明度:0.6/1
this.translateX = this.animate ? 50 : 0; // 组件二平移:50px/0px
})
})
// 组件二
Column() {
}
// 样式设置
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.backgroundColor('#D94838')
.borderRadius(30)
// 绑定透明度属性
.opacity(this.opacityValue)
// 绑定平移属性
.translate({ x: this.translateX })
}
// 容器样式设置
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
说明:与 animation 接口不同,animateTo 将动画参数统一配置在接口中,闭包内所有属性变化(旋转、平移、透明度)均使用相同的弹簧曲线,无需为每个组件单独设置 animation,适合批量控制动画效果。
3. 关键帧动画(keyframeAnimateTo)
keyframeAnimateTo 用于实现分段动画,通过传入整体动画参数和关键帧数组,每一段关键帧可单独配置时长、曲线等参数,适合同一属性需要连续执行多个动画的场景,避免多次创建动画导致的衔接卡顿。
接口定义:
typescript
keyframeAnimateTo(param: KeyframeAnimateParam, keyframes: Array<KeyframeState>): void
参数说明:param 为关键帧动画整体参数(如重复次数、延迟、结束回调);keyframes 为关键帧数组,每一项包含当前关键帧的时长、曲线、闭包事件(属性变化逻辑)。
typescript
@Entry
@Component
struct KeyframeAnimateToDemo {
// 声明相关状态变量
@State rotateValue: number = 0; // 组件一旋转角度
@State translateX: number = 0; // 组件二水平偏移量
@State opacityValue: number = 1; // 组件透明度
// 构建 UI 界面
build() {
Row() {
// 组件一
Column() {
}
// 绑定旋转属性
.rotate({ angle: this.rotateValue })
// 样式设置
.backgroundColor('#317AF7')
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.borderRadius(30)
// 点击事件处理
.onClick(() => {
// 调用 keyframeAnimateTo 接口,配置整体参数和关键帧数组
this.getUIContext()?.keyframeAnimateTo({
iterations: 1 // 整体动画重复次数
}, [
{
// 第一段关键帧:800ms,弹簧曲线,属性变化
duration: 800, // 动画持续时间:800ms
curve: curves.springMotion(), // 动画曲线:弹簧曲线
event: () => {
// 第一段动画的属性变化
this.rotateValue = 90; // 组件一顺时针旋转90度
this.opacityValue = 0.6; // 组件二透明度降低
this.translateX = 50; // 组件二向右偏移50px
}
},
{
// 第二段关键帧:500ms,缓出曲线,属性恢复
duration: 500, // 动画持续时间:500ms
curve: Curve.EaseOut, // 动画曲线:缓出曲线
event: () => {
// 第二段动画的属性变化
this.rotateValue = 0; // 组件一恢复初始角度
this.opacityValue = 1; // 组件二透明度恢复
this.translateX = 0; // 组件二恢复初始位置
}
}
]);
})
// 组件二
Column() {
}
// 样式设置
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.backgroundColor('#D94838')
.borderRadius(30)
// 绑定透明度属性
.opacity(this.opacityValue)
// 绑定平移属性
.translate({ x: this.translateX })
}
// 容器样式设置
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
说明:点击组件一后,将依次执行两段关键帧动画:第一段 800ms 实现组件旋转、偏移和透明度变化,第二段 500ms 实现属性恢复,两段动画衔接自然,无卡顿,适合需要多阶段动画的场景(如复杂的组件交互反馈)。

二、喵屿项目中三种动画实现
「喵屿」App 在宠物管理、表单校验等场景中大量使用了动画来提升交互体验。以下是三种核心动画模式的实现详解。
2.1 删除模式抖动(setInterval + animation)
场景:管理互动宠物页面进入删除模式时,网格中的宠物图标持续左右摆动,提示用户可点击删除。
核心技术 :setInterval 定时切换角度状态 + .rotate() + .animation() 平滑插值。
typescript
// 状态变量
@State shakeAngle: number = 0;
private shakeTimer: number = -1;
// 启动抖动:每 100ms 在 +1.5° 和 -1.5° 之间交替
startShake(): void {
if (this.shakeTimer !== -1) return; // 防重入
let direction = 1;
this.shakeTimer = setInterval(() => {
this.shakeAngle = direction * 1.5; // 交替正负角度
direction *= -1;
}, 100);
}
// 停止抖动:清除定时器,复位角度
stopShake(): void {
if (this.shakeTimer !== -1) {
clearInterval(this.shakeTimer);
this.shakeTimer = -1;
}
this.shakeAngle = 0;
}
UI 绑定 :在 GridItem 上应用 rotate + animation,仅对非删除中的宠物生效。
typescript
GridItem() {
// ...宠物图片...
}
.rotate({
angle: (this.isDeleteMode && this.deletingUri !== pet.uri) ? this.shakeAngle : 0
})
.animation({ duration: 80, curve: Curve.Linear })
工作原理:
setInterval每 100ms 将shakeAngle设置为 +1.5 或 -1.5.animation({ duration: 80 })在状态变化间进行 80ms 的匀速插值- 80ms 动画 + 20ms 静置 = 100ms 完整周期,产生快速左右摆动效果
Curve.Linear保证往返速度一致,视觉上均匀抖动
2.2 飞入垃圾桶(animateTo)
场景:用户确认删除宠物后,宠物图片从原位置飞向垃圾桶图标,同时缩小到 0.08 倍并淡出,500ms 后执行实际删除。
核心技术 :getUIContext().animateTo() 命令式动画,同时驱动 scale、opacity、translate。
步骤 1:计算目标位置
typescript
async animateDelete(index: number): Promise<void> {
const pet = this.customPets[index];
this.deletingUri = pet.uri; // 标记为"正在删除",在原网格中隐藏
// 获取被删宠物在屏幕上的位置
const itemPos = this.itemPositions[pet.uri];
// 计算垃圾桶图标的中心坐标
let targetX = 0;
let targetY = -120; // 默认向上偏移
if (itemPos && this.trashIconX > 0) {
const trashCX = this.trashIconX + 12; // 垃圾桶中心 X
const trashCY = this.trashIconY + 12; // 垃圾桶中心 Y
const itemCX = itemPos.x + itemPos.w / 2; // 宠物中心 X
const itemCY = itemPos.y + itemPos.h / 2; // 宠物中心 Y
targetX = trashCX - itemCX; // 水平位移
targetY = trashCY - itemCY; // 垂直位移
}
步骤 2:执行动画(scale + opacity + translate)
typescript
// 初始化动画起始状态
this.deleteAnimTranslateX = 0;
this.deleteAnimTranslateY = 0;
this.deleteAnimScale = 1;
this.deleteAnimOpacity = 1;
// animateTo:500ms EaseOut 驱动三个属性同时变化
this.getUIContext().animateTo(
{ duration: 500, curve: Curve.EaseOut },
() => {
this.deleteAnimScale = 0.08; // 缩小到 8%
this.deleteAnimOpacity = 0; // 完全透明
this.deleteAnimTranslateX = targetX; // 水平移动到垃圾桶
this.deleteAnimTranslateY = targetY; // 垂直移动到垃圾桶
}
);
// 500ms 后执行实际的数据删除
setTimeout(() => {
this.executeDelete(index);
}, 500);
}
步骤 3:Overlay 层渲染 --- 在页面最上层放置一个与原始位置相同的 Image,应用动画状态:
typescript
if (this.deletingUri) {
Image(this.deletingUri)
.width(this.deleteOverlayW)
.height(this.deleteOverlayH)
.objectFit(ImageFit.Cover)
.position({ x: this.deleteOverlayX, y: this.deleteOverlayY })
.scale({ x: this.deleteAnimScale, y: this.deleteAnimScale }) // 缩放到 0.08
.opacity(this.deleteAnimOpacity) // 淡出到 0
.translate({ x: this.deleteAnimTranslateX, y: this.deleteAnimTranslateY })
.hitTestBehavior(HitTestMode.None) // 不拦截其他手势
}
设计要点:
- 在原始网格中将宠物设为
Visibility.Hidden,同时在 overlay 层渲染相同的图片 animateTo闭包内一次修改多个属性,框架自动并行驱动Curve.EaseOut(快入慢出)让动画结束时有"落入"垃圾桶的视觉感受- 动画持续 500ms,
setTimeout同步等待 500ms 后执行executeDelete()
具体效果:

2.3 表单校验抖动(keyframeAnimateTo)
场景:用户在添加/编辑表单中未填写必填字段点击保存时,输入框或提示文字左右抖动,视觉提示用户。
核心技术 :uiContext.keyframeAnimateTo({ iterations: 2 }, [ ... ]) 两帧关键帧动画。
标准实现(水平抖动):
typescript
@State nameTranslateX: number = 0;
startAnimation(): void {
const uiContext = this.getUIContext?.();
if (!uiContext) return;
this.nameTranslateX = 0;
// iterations: 2 = 两帧 × 2 次 = 完整来回
uiContext.keyframeAnimateTo({ iterations: 2 }, [
{
// 帧 1:100ms 内 translateX 从 0 到 5
duration: 100,
event: () => { this.nameTranslateX = 5; }
},
{
// 帧 2:100ms 内 translateX 从 5 回到 0
duration: 100,
event: () => { this.nameTranslateX = 0; }
}
]);
}
UI 绑定 :将 translateX 应用到需要抖动的元素:
typescript
TextInput({ placeholder: "请输入宠物名称", text: $$this.name })
.translate({ x: this.nameTranslateX })
// ...
.position({ x: this.nameTranslateX, y: 0 })
变体 1 --- 垂直抖动(疫苗/驱虫列表项):
仅方向不同(translateY 代替 translateX),时长加倍(200ms),曲线更平滑:
typescript
startAnimation(): void {
const uiContext = this.getUIContext?.();
if (!uiContext) return;
this.translateY = 0;
uiContext.keyframeAnimateTo({ iterations: 2 }, [
{
duration: 200,
curve: Curve.Smooth, // 平滑过渡,比默认更柔和
event: () => { this.translateY = -5; } // 向上抖动
},
{
duration: 200,
curve: Curve.Smooth,
event: () => { this.translateY = 0; }
}
]);
}
变体 2 --- 多字段抖动:
typescript
startAnimation(name: boolean = true, total: boolean = true): void {
const uiContext = this.getUIContext?.();
if (!uiContext) return;
this.nameTranslateX = 0;
this.totalTranslateX = 0;
uiContext.keyframeAnimateTo({ iterations: 2 }, [
{
duration: 100,
event: () => {
if (name) this.nameTranslateX = 5;
if (total) this.totalTranslateX = 5;
}
},
{
duration: 100,
event: () => {
if (name) this.nameTranslateX = 0;
if (total) this.totalTranslateX = 0;
}
}
]);
}
调用方式(以 CatManagement.ets 为例):
typescript
// 校验:名称为空时触发抖动 + Toast 提示
if (StringUtil.isEmpty(this.name)) {
VibrateUtil.alarmVibrate(); // 触觉反馈:警告振动
this.startAnimation(); // 视觉反馈:抖动动画
this.promptAction.showToast({ // 文字反馈:Toast 提示
message: '请输入宠物名称'
});
return;
}

三、总结与最佳实践
「喵屿」App 在提醒与删除场景中成功落地了三种 HarmonyOS 动画模式,下表总结了各自的核心技术、适用场景与关键参数:
| 模式 | 核心技术 | 适用场景 | 参数要点 |
|---|---|---|---|
| 持续抖动 | setInterval + .rotate().animation() |
编辑模式提示(批量删除) | 幅度 1.5°、间隔 100ms、Curve.Linear |
| 飞入目标 | animateTo() |
删除确认动画(单个删除) | duration 500ms、Curve.EaseOut、scale 0.08 |
| 校验抖动 | keyframeAnimateTo() |
表单校验失败提示 | iterations 2、每帧 100ms、位移 5vp |
核心设计原则
1. 选型原则
| 动画接口 | 核心选择原则 | 典型使用场景 |
|---|---|---|
animation |
为组件的不同属性 设置不同的动画参数时使用。 | 按钮点击时,背景色渐变与缩放使用不同曲线。 |
animateTo |
为多个属性 (可能跨组件)设置相同的动画参数 ,或需要动画嵌套时使用。 | 页面布局切换时,多个组件同时以相同方式移动和淡入淡出。 |
keyframeAnimateTo |
为同一属性 创建连续、多段动画时使用。 | 实现类似"呼吸灯"效果(缩放-暂停-缩放),或复杂的路径动画。 |
通用指导原则:
- 优先使用
animation:当只需为单个组件的属性添加动画,且参数各异时,这是最简洁的方式。 - 需要同步控制多个动画时用
animateTo:当多个属性或组件需要以相同的动画参数协同变化时,使用animateTo可以避免代码重复,并确保动画同步。 - 需要复杂序列动画时用
keyframeAnimateTo:当单个属性需要经历一系列不同的状态变化(如移动、停顿、再移动)时,关键帧动画是最佳选择,它能确保动画流畅衔接。 - 性能考虑 :属性动画(以上三种)的性能通常优于帧动画(
ohos.animator),因此在满足需求的前提下应优先选择属性动画接口。帧动画仅在需要逐帧精确控制或可暂停能力时考虑。
2. 多模态反馈增强
动画不应孤立存在,与触觉反馈(VibrateUtil.buttonVibrate / alarmVibrate)结合使用,能显著提升用户对操作结果的感知强度。例如表单校验失败时,"抖动动画 + 警告振动 + Toast 提示"三管齐下,形成完整的错误反馈闭环。
3. 性能与可复用性
- 飞入垃圾桶动画采用 Overlay 层独立渲染,避免原网格组件重新布局,性能开销最小化。
- 将抖动启动/停止、关键帧构建等逻辑封装为
AnimationUtil工具类,可被任意页面调用,降低重复代码。 - 注意
keyframeAnimateTo每次调用前需重置状态变量,否则可能产生意外的跳帧。
结语
通过「喵屿」App 的实践可以看出,HarmonyOS 的 ArkUI 动画体系既提供了简洁的声明式接口(animation),也保留了强大的命令式控制能力(animateTo、keyframeAnimateTo)。开发者可根据业务场景自由组合,在保证性能的前提下,为用户带来更具情感和确定性的交互体验。本文提炼的三种动画模式及配套的最佳实践,可直接复制到任何 HarmonyOS项目中,帮助快速实现高品质的提醒与删除动效。