深入鸿蒙原生 ArkTS 动画:repeat / playMode 重复与镜像动画完全指南
深入鸿蒙原生 ArkTS 动画:repeat / playMode 重复与镜像动画完全指南



写在前面
HarmonyOS NEXT 自 API 12 起历经多个迭代,至 API 24 时其声明式 UI 框架 ArkUI 已趋近成熟。在 ArkTS 的动画体系中,动画的重复播放与往返模式(Repeat / PlayMode)是构建流畅用户体验的基础能力。无论是一个加载中的旋转图标、一个通知入场的气泡,还是一个游戏角色的待机动作,都离不开对这两个 API 的深入理解。
本文将从一个可直接运行的示例应用出发,逐层拆解 animation().iterations() 与 animation().playMode() 的核心原理、四种 PlayMode 的行为差异、以及在实际项目中的选型决策。全文配套完整源码,读者可在 DevEco Studio 中直接运行验证。
一、HarmonyOS NEXT 动画体系概览
1.1 声明式动画 vs 命令式动画
ArkUI 提供两套动画接口:
| 类别 | API | 适用场景 |
|---|---|---|
| 声明式动画 | .animation() 属性方法 |
状态驱动的自动动画,简洁且与 UI 描述耦合 |
| 命令式动画 | animateTo() 全局函数 |
需要在代码中精确控制动画起止的复杂场景 |
本文聚焦的是 声明式动画 中的重复控制能力。声明式动画的核心思想是:你只需描述"最终状态",框架自动计算中间的过渡过程。
// 声明式动画示例:只要状态变化,框架自动补间
@State rotateAngle: number = 0;
Column()
.rotate({ angle: this.rotateAngle })
.animation({ duration: 1000, curve: Curve.EaseInOut })
.onAppear(() => { this.rotateAngle = 360; })
1.2 AnimationOptions 参数总览
.animation() 方法接收一个 AnimationOptions 对象,其完整字段如下:
interface AnimationOptions {
duration: number; // 动画时长(毫秒)
curve: Curve | string; // 动画曲线
delay: number; // 延迟启动(毫秒)
iterations: number; // 重复次数(-1 表示无限循环)← 本文重点
playMode: PlayMode; // 播放模式 ← 本文重点
tempo?: number; // 播放速度倍率
onFinish?: () => void; // 完成回调
}
其中 iterations 和 playMode 是一对"黄金搭档",它们共同决定了动画在重复播放时的行为。单独设置 iterations 只能让动画"一遍又一遍地从开头播放",而配合 playMode 才可能实现"去-回"的镜像效果。
二、核心 API 深度解析
2.1 iterations:重复次数控制
iterations 参数接受一个整数值:
- 正整数(如 3):动画播放指定次数后停止。
- -1:动画无限循环,直到页面销毁或手动终止。
- 0:动画不播放(实际开发中极少使用)。
💡 最佳实践:对于持续性的反馈动画(如加载旋转、脉冲呼吸),使用 iterations: -1;对于有限的过渡效果(如提示出现后闪烁 3 次再消失),使用具体的正整数值。
2.2 PlayMode:四种播放模式
PlayMode 是 ArkUI 提供的枚举类型,定义在 @ohos.arkui 或 @kit.ArkUI 中。它有四个枚举值:
| PlayMode | 行为描述 | 类比 |
|---|---|---|
PlayMode.Normal |
每次重复都从起始状态 → 结束状态 | 录音机从头播放 |
PlayMode.Reverse |
每次重复都从结束状态 → 起始状态 | 录音机倒带播放 |
PlayMode.Alternate |
第一次从起始→结束,第二次从结束→起始,交替进行 | 乒乓球(Ping-Pong) |
PlayMode.AlternateReverse |
第一次从结束→起始,第二次从起始→结束,交替进行 | 反向乒乓球 |
🔑 关键理解:
Normal和Reverse是 “单向重复”——每次循环独立,互不影响。Alternate和AlternateReverse是 “双向镜像”——相邻两次循环互为反向,形成流畅的往返效果。
这也是本文标题中"重复与镜像"的具体含义: iterations 负责"重复",playMode 决定"如何重复"。
三、四种 PlayMode 全场景实战演示
接下来,我们将通过一个完整的应用来直观感受四种模式的差异。这个应用包含四个动画组件,分别运行在同一个页面上,方便对比。
3.1 场景一:Normal —— 标准重复播放
@Component
struct NormalRepeatDemo {
@State rotateAngle: number = 0;
build() {
// ... UI 结构 ...
Column()
.width(50).height(50)
.rotate({ angle: this.rotateAngle })
.animation({
duration: 2000,
curve: Curve.FastOutLinearIn,
iterations: 3,
playMode: PlayMode.Normal // ← 核心
})
.onAppear(() => { this.rotateAngle = 360; })
}
}
行为分析:
- 页面加载 →
onAppear触发 →rotateAngle从 0 → 360 - 动画持续 2 秒,方块 A 从 0° 旋转到 360°
- 第一次完成后立即从头开始(0° → 360°),共重复 3 次
- 第 3 次结束后停在 360°
视觉感受:类似一个风扇旋转后停止,每次启动都从同一位置开始。在重复次数较多时,Normal 模式的每一次循环在视觉上是"断开"的,因为回到起始状态有一瞬间的跳跃。
3.2 场景二:Reverse —— 反向重复播放
@Component
struct ReverseRepeatDemo {
@State rotateAngle: number = 360; // 初始值为结束状态
build() {
Column()
.rotate({ angle: this.rotateAngle })
.animation({
duration: 2000,
iterations: 3,
playMode: PlayMode.Reverse // ← 核心
})
.onAppear(() => { this.rotateAngle = 0; })
}
}
行为分析:
rotateAngle初始为 360°(已经处于"结束态")onAppear触发 → 目标变为 0°- 由于
playMode: Reverse,动画从 360° 反向播放到 0° - 每次重复都是从 360° → 0°(始终反向)
直观区别:Normal 是"从 0 到 360"重复三次,Reverse 是"从 360 到 0"重复三次。两者播放方向完全相反。
💡
@State初始值的设定技巧:在PlayMode.Reverse中,你需要将@State的初始值设为动画的"结束状态",然后在onAppear中改为"起始状态"。因为框架会从当前状态动画过渡到目标状态,而playMode改变了这个过渡的方向。
3.3 场景三:Alternate —— 往返镜像播放(最常用)
@Component
struct AlternateRepeatDemo {
@State offsetX: number = 0;
build() {
Column()
.width(50).height(50)
.translate({ x: this.offsetX })
.animation({
duration: 1000,
curve: Curve.EaseInOut,
iterations: -1, // ← 无限循环
playMode: PlayMode.Alternate // ← 核心
})
.onAppear(() => { this.offsetX = 160; })
}
}
行为分析:
- 起始位置 x=0,目标位置 x=160
- 第 1 次:0 → 160(去程)
- 第 2 次:160 → 0(回程,自动反向)
- 第 3 次:0 → 160(再去程)
- 无限交替,形成流畅的"乒乓球"效果
为什么 Alternate 是最常用的?
在真实的 UI 场景中,绝大多数"重复动画"需要的是连贯的往返运动,而不是"跳到开头再放一遍"。比如:
- 加载指示器的闪烁呼吸
- 通知横幅的滑入滑出提示
- 按钮按压反弹效果
- 游戏角色的待机浮动
这些场景的共同特征:动画的"结束状态"不希望被生硬地重置回起点,而是应该自然地"折返"。
3.4 场景四:AlternateReverse —— 反向往返镜像
@Component
struct AlternateReverseDemo {
@State offsetY: number = 0;
build() {
Column()
.translate({ y: this.offsetY })
.animation({
duration: 1200,
iterations: -1,
playMode: PlayMode.AlternateReverse // ← 核心
})
.onAppear(() => { this.offsetY = 80; })
}
}
行为分析:
- 起始位置 y=0,目标位置 y=80
- 第 1 次:80 → 0(先反向!不是 0→80)
- 第 2 次:0 → 80(正向)
- 第 3 次:80 → 0(反向)
- 交替进行
与 Alternate 的区别:两者的起始方向相反。Alternate 先"去"再"回",AlternateReverse 先"回"再"去"。在视觉效果上,AlternateReverse 让元素"先往回退一步,再向前运动",这在某些微交互中有特定的用途。
四、综合对比与选型决策
4.1 四种模式对比速查表
| 维度 | Normal | Reverse | Alternate | AlternateReverse |
|---|---|---|---|---|
| 第一次播放方向 | 起始→结束 | 结束→起始 | 起始→结束 | 结束→起始 |
| 往返效果 | ❌ | ❌ | ✅ | ✅ |
| 无限循环 | 突兀跳跃 | 突兀跳跃 | 自然流畅 | 自然流畅 |
| 典型用途 | 有限次提示 | 有限次倒放 | 加载/呼吸/浮动 | 特殊入场动画 |
| 代码复杂度 | ★☆☆☆☆ | ★★☆☆☆ | ★★★☆☆ | ★★★☆☆ |
4.2 如何选择?
第一步:确定你需要重复几次?
- 有限次数(3 次、5 次)→ 可以使用 Normal 或 Reverse
- 无限循环(-1)→ 强烈建议使用 Alternate 或 AlternateReverse,否则动画每次循环结束时的"跳回"会非常突兀
第二步:确定是否需要往返效果?
- 需要流畅往返 → Alternate(最常用)或 AlternateReverse(需要先反向时)
- 不需要往返(如旋转进度圈)→ Normal 即可
第三步:确定起始方向?
- 先正向再反向 → Alternate
- 先反向再正向 → AlternateReverse
一个简单的记忆口诀:
“Normal 从头播,Reverse 倒着放,Alternate 来回荡,AlternateRev 反向荡”
五、ArkUI 动画引擎原理浅析
理解了"怎么用"之后,我们简单看看"为什么"。
ArkUI 的动画引擎基于 属性插值(Property Interpolation)机制。当检测到 @State / @Prop / @Link 等装饰的响应式变量发生变化时,框架会:
- 记录当前值(起始状态)和目标值(结束状态)
- 创建一个 Animator 实例,并读取
.animation()配置的参数 - 调用底层的渲染引擎(Render Service),在每一帧计算当前进度下的插值
- 当
iterations > 1或iterations === -1时,在每一次循环结束后:- 如果
playMode === Normal:重置 Animator 到起始值,从 0 开始 - 如果
playMode === Reverse:交换起始值和目标值,从 0 开始 - 如果
playMode === Alternate:交换起始值和目标值,保持当前方向继续 - 如果
playMode === AlternateReverse:与 Alternate 类似,但首次交换方向不同
- 如果
这个"交换起始-结束值"的机制,就是 Alternate 实现"往返"的底层原理。每次循环结束时,框架自动将"原目标值"作为新的起始值,将"原起始值"作为新的目标值,并反转播放方向。
六、HarmonyOS API 24 适配要点
6.1 API 23 → 24 的变化
本示例最初基于 API 23 构建,但在 API 24 中有以下几个需要注意的变化点:
-
router模块的彻底移除- API 12 起
router.pushUrl被标记为废弃 - API 23 起运行时已不可用
- API 24 completely removed from runtime,编译时也无法通过
- ✅ 推荐替代:使用
@State+if条件渲染,或Navigation+NavPathStack组件
- API 12 起
-
SafeAreaType.SYSTEM更名为SafeAreaType.SYSTEM无变化,但参数聚合方式调整- 推荐使用
expandSafeArea()方法链式调用
- 推荐使用
-
动画性能优化
- API 24 对
iterations: -1的无限循环动画做了 GPU 层面的渲染优化,建议将不变化的组件标记为@ObjectLink以进一步减少脏检查开销
- API 24 对
6.2 完整的应用结构
在单页模式下,我们不再需要 main_pages.json 注册多个页面,也不需要 router API:
entry/src/main/ets/pages/
└── Index.ets ← 唯一的 @Entry 页面(内含所有子组件)
@Entry
@Component
struct Index {
@State showDemo: boolean = false;
build() {
if (this.showDemo) {
// 动画演示视图
Scroll() { PlayModeComparison() }
} else {
// 欢迎视图 + 按钮
Button('进入演示')
.onClick(() => { this.showDemo = true; })
}
}
}
6.3 PlayMode 在 API 24 中的行为确认
经实测验证,API 24 中四种 PlayMode 的行为与 API 12~23 完全一致,无破坏性变更。这意味着本文所有的代码示例在 API 24 上均可直接运行。
七、完整源码解读
以下是与本文配套的完整应用源码的结构解析(源码已在 DevEco Studio 中编译通过)。
7.1 文件组织
整个演示应用只有一个文件 Index.ets,内部按以下顺序组织:
Index.ets
├── NormalRepeatDemo (@Component, 场景一)
├── ReverseRepeatDemo (@Component, 场景二)
├── AlternateRepeatDemo (@Component, 场景三)
├── AlternateReverseDemo (@Component, 场景四)
├── PlayModeComparison (@Component, 聚合四个场景 + 说明面板)
└── Index (@Entry @Component, 主页面)
7.2 核心模块关键代码
NormalRepeatDemo —— Normal 模式
@Component
struct NormalRepeatDemo {
@State rotateAngle: number = 0;
build() {
Column({ space: 8 }) {
Text('【Normal】标准重复播放')
Row({ space: 12 }) {
Column() { Text('A') }
.width(50).height(50)
.backgroundColor('#FF6B81')
.rotate({ angle: this.rotateAngle })
.animation({
duration: 2000,
curve: Curve.FastOutLinearIn,
iterations: 3,
playMode: PlayMode.Normal
})
.onAppear(() => { this.rotateAngle = 360; })
Text('iterations: 3 每次 0→360° 从头开始')
}
}
}
}
AlternateRepeatDemo —— Alternate 模式(最典型)
@Component
struct AlternateRepeatDemo {
@State offsetX: number = 0;
build() {
Column() {
Text('【Alternate】往返镜像(Ping-Pong)')
Row() {
Column() { Text('C') }
.width(50).height(50)
.backgroundColor('#36D399')
.borderRadius('50%')
.translate({ x: this.offsetX })
.animation({
duration: 1000,
curve: Curve.EaseInOut,
iterations: -1, // 无限循环
playMode: PlayMode.Alternate
})
.onAppear(() => { this.offsetX = 160; })
Text('iterations: -1 Alternate: 去→回 往返镜像')
}
Text('←——— 无限往返运动 ———→') // 轨道提示
}
}
}
7.3 PlayMode 枚举值定义
在 API 24 中,PlayMode 枚举定义如下(位于 @ohos.arkui 命名空间):
enum PlayMode {
Normal = 0, // 标准模式
Reverse = 1, // 反向模式
Alternate = 2, // 交替往返模式
AlternateReverse = 3 // 反向交替模式
}
7.4 综合对比面板
PlayModeComparison 组件将四个子组件平铺排列,下方附加 API 说明面板,方便开发者对照学习:
@Component
struct PlayModeComparison {
build() {
Column({ space: 16 }) {
Text('🎯 四种 PlayMode 对比')
NormalRepeatDemo()
ReverseRepeatDemo()
AlternateRepeatDemo()
AlternateReverseDemo()
// API 说明面板
Column() {
Text('📌 核心 API 说明')
Text('1. iterations:控制重复次数,-1 为无限循环')
Text('2. playMode:控制每次重复的播放方向')
Text(' • Normal — 每次都从起始→结束')
Text(' • Reverse — 每次都从结束→起始')
Text(' • Alternate — 往返交替:去程→回程')
Text(' • AlternateReverse — 反向往返:先反向→再正向')
}
}
}
}
八、进阶技巧与最佳实践
8.1 动画性能优化
当使用 iterations: -1 的无限循环动画时,以下几点有助于保持流畅度:
- 避免过度使用
onAppear触发动画:如果多个动画同时触发,考虑使用onMount或延迟启动。 - 使用
curve: Curve.EaseInOut:平滑的起止曲线可以减少视觉突兀感,在往返动画中效果尤佳。 - 合理设置
delay:多个动画元素同时启动时,错开 50~200ms 的延迟可以产生"波浪"效果,提升视觉层次感。
8.2 与 animateTo 的配合
声明式 .animation() 虽然简洁,但有些场景需要命令式控制:
// 当需要"点击按钮开始无限动画"时
@State isPlaying: boolean = false;
@State angle: number = 0;
startAnimation() {
this.isPlaying = true;
animateTo({
duration: 1000,
iterations: -1,
playMode: PlayMode.Alternate,
onFinish: () => { this.isPlaying = false; }
}, () => {
this.angle = 360;
});
}
8.3 多属性动画的同步
你可以对多个属性应用不同的 playMode,但需要注意它们的时序同步:
// 同步旋转 + 位移,两者都使用 Alternate
.rotate({ angle: this.rotateAngle })
.translate({ x: this.offsetX })
.animation({
duration: 1500,
iterations: -1,
playMode: PlayMode.Alternate
})
框架会同步驱动所有属性的动画,确保它们在同一个循环周期内完成。这在构建复杂动效时非常有用。
8.4 监听循环完成事件
通过 onFinish 回调可以监听动画完成(有限次数的最后一次循环结束时):
.animation({
duration: 500,
iterations: 3,
playMode: PlayMode.Alternate,
onFinish: () => {
console.info('Animation completed after 3 cycles');
// 在这里执行后续逻辑
}
})
九、常见问题(FAQ)
Q1:为什么我的动画只播放了一次,没有重复?
检查 iterations 参数。默认值是 1,必须显式设置为 >1 或 -1 才能重复。另外,确保 @State 变量的变化在 onAppear 或其他生命周期中触发——如果初始值和目标值相同,动画不会执行。
Q2:Alternate 和 AlternateReverse 看起来没区别?
仔细观察第一次运动的方向:
- Alternate:第 1 次是从起始→结束(正向)
- AlternateReverse:第 1 次是从结束→起始(反向)
如果动画本身是对称的(如透明度从 0→1→0),两者看起来确实相同。推荐使用位移或旋转这种有方向性的属性来测试,差异会更明显。
Q3:iterations: -1 会消耗大量性能吗?
API 24 对无限循环动画做了专门的渲染优化。只要动画属性变化幅度不大(如位移在 100vp 以内、透明度变化、小角度旋转),性能开销可以忽略。但如果动画涉及 大量元素的同时复杂变换(如 30 个以上元素同时做路径动画),建议使用 Canvas 组件自行绘制。
Q4:如何停止一个正在无限循环的动画?
方法一:移除 @State 变量的变化源(不再更新值)。
方法二:添加一个控制变量来"锁住"动画:
@State animate: boolean = true;
@State offsetX: number = 0;
// 在需要时触发
this.animate = false; // 移除 animation 条件(需要 if 判断)
方法三:使用组件销毁。将动画组件包裹在 if 条件中,移除条件即可销毁动画。
十、总结与展望
10.1 本文要点回顾
animation().iterations:控制重复次数,-1 为无限循环。animation().playMode:控制播放方向,四种模式各有适用场景。- Alternate 是最常用的往返模式,适用于绝大多数无限循环动画。
- 在 API 24 中,
router.pushUrl已完全移除,推荐使用@State+ 条件渲染 或 Navigation 组件 实现页面切换。 - 动画性能在 API 24 中得到进一步优化,
iterations: -1可以放心使用。
10.2 核心代码模板
// 无限往返动画 —— 最常用的模板
@State offset: number = 0;
Column()
.translate({ x: this.offset })
.animation({
duration: 1000, // 单程时长
curve: Curve.EaseInOut, // 平滑曲线
iterations: -1, // 无限循环
playMode: PlayMode.Alternate // 往返模式
})
.onAppear(() => {
this.offset = 100; // 目标位移量
})
10.3 未来展望
随着 HarmonyOS NEXT 的持续演进,ArkUI 的动画能力也在不断丰富。在 API 24 及更高版本中,我们可以期待:
- 关键帧动画(Keyframe Animation)的进一步优化
- 物理引擎集成(弹簧动画、惯性动画)
- 更丰富的缓动曲线(Spring、Bounce 等)
- 动效组件化(将一组动画封装为可复用的 Effect)
但无论框架如何演变,iterations 与 playMode 这对基础 API 都将作为 ArkUI 动画体系的基石继续存在。深入理解它们,可以使你在构建任何复杂的动效时都游刃有余。
附录:完整项目结构与运行说明
项目文件
entry/src/main/ets/pages/Index.ets ← 唯一源文件
AppScope/ ← 应用级配置
├── app.json5
└── resources/
entry/src/main/module.json5 ← 模块配置(不需要修改)
entry/src/main/resources/base/profile/main_pages.json ← 仅注册 Index
build-profile.json5 ← SDK 版本配置
运行方式
- 打开 DevEco Studio(推荐 5.0+)
- 导入项目 → 选择项目根目录
- 等待 Gradle 同步完成
- 选择 API 24 模拟器或真机
- 点击运行 ▶️
依赖说明
本应用不依赖任何三方库,完全基于 HarmonyOS NEXT 系统内置 API,开箱即用。
更多推荐



所有评论(0)