深入鸿蒙原生 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;     // 完成回调
}

其中 iterationsplayMode 是一对"黄金搭档",它们共同决定了动画在重复播放时的行为。单独设置 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 第一次从结束→起始,第二次从起始→结束,交替进行 反向乒乓球

🔑 关键理解

  • NormalReverse“单向重复”——每次循环独立,互不影响。
  • AlternateAlternateReverse“双向镜像”——相邻两次循环互为反向,形成流畅的往返效果。

这也是本文标题中"重复与镜像"的具体含义: 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; })
  }
}

行为分析

  1. 页面加载 → onAppear 触发 → rotateAngle 从 0 → 360
  2. 动画持续 2 秒,方块 A 从 0° 旋转到 360°
  3. 第一次完成后立即从头开始(0° → 360°),共重复 3 次
  4. 第 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; })
  }
}

行为分析

  1. rotateAngle 初始为 360°(已经处于"结束态")
  2. onAppear 触发 → 目标变为 0°
  3. 由于 playMode: Reverse,动画从 360° 反向播放到 0°
  4. 每次重复都是从 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; })
  }
}

行为分析

  1. 起始位置 x=0,目标位置 x=160
  2. 第 1 次:0 → 160(去程)
  3. 第 2 次:160 → 0(回程,自动反向)
  4. 第 3 次:0 → 160(再去程)
  5. 无限交替,形成流畅的"乒乓球"效果

为什么 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; })
  }
}

行为分析

  1. 起始位置 y=0,目标位置 y=80
  2. 第 1 次:80 → 0(先反向!不是 0→80)
  3. 第 2 次:0 → 80(正向)
  4. 第 3 次:80 → 0(反向)
  5. 交替进行

与 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 等装饰的响应式变量发生变化时,框架会:

  1. 记录当前值(起始状态)和目标值(结束状态)
  2. 创建一个 Animator 实例,并读取 .animation() 配置的参数
  3. 调用底层的渲染引擎(Render Service),在每一帧计算当前进度下的插值
  4. iterations > 1iterations === -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 中有以下几个需要注意的变化点:

  1. router 模块的彻底移除

    • API 12 起 router.pushUrl 被标记为废弃
    • API 23 起运行时已不可用
    • API 24 completely removed from runtime,编译时也无法通过
    • 推荐替代:使用 @State + if 条件渲染,或 Navigation + NavPathStack 组件
  2. SafeAreaType.SYSTEM 更名为 SafeAreaType.SYSTEM 无变化,但参数聚合方式调整

    • 推荐使用 expandSafeArea() 方法链式调用
  3. 动画性能优化

    • API 24 对 iterations: -1 的无限循环动画做了 GPU 层面的渲染优化,建议将不变化的组件标记为 @ObjectLink 以进一步减少脏检查开销

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 的无限循环动画时,以下几点有助于保持流畅度:

  1. 避免过度使用 onAppear 触发动画:如果多个动画同时触发,考虑使用 onMount 或延迟启动。
  2. 使用 curve: Curve.EaseInOut:平滑的起止曲线可以减少视觉突兀感,在往返动画中效果尤佳。
  3. 合理设置 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)

但无论框架如何演变,iterationsplayMode 这对基础 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 版本配置

运行方式

  1. 打开 DevEco Studio(推荐 5.0+)
  2. 导入项目 → 选择项目根目录
  3. 等待 Gradle 同步完成
  4. 选择 API 24 模拟器或真机
  5. 点击运行 ▶️

依赖说明

本应用不依赖任何三方库,完全基于 HarmonyOS NEXT 系统内置 API,开箱即用。

Logo

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

更多推荐