前言

动画是提升应用质感的关键,但不同的动画需求,需要不同的工具。HarmonyOS NEXT 提供了两套截然不同的动画机制:

  • animateTo(属性隐式动画):修改组件属性时自动补间,用起来极简,但它是"一次性"的——触发即执行,你无法在动画进行到一半时暂停、反转,也拿不到每一帧的插值。
  • @ohos.animator(Animator 逐帧动画):一个命令式的动画对象,可以 play(播放)、pause(暂停)、reverse(反转)、cancel(取消),并通过 onFrame 回调在每一帧拿到当前插值。它把"时间驱动的数值变化"这件事完全交给你掌控。

当你需要精确控制动画的生命周期,或者要用动画驱动那些不是组件属性的东西(比如 Canvas 绘制、复杂的自定义状态、多个元素的联动)时,Animator 才是正确答案。

本文用一个可交互的"逐帧动画实验室",把 @ohos.animator 讲透:一个由 onFrame 逐帧驱动的小球和进度条,配合播放/暂停/反转/重置的完整控制,以及缓动曲线、时长、循环次数的实时调节。全文含完整可运行代码,适合想深入掌握 HarmonyOS 动画体系的中级开发者。


一、Animator 与 animateTo 的本质区别

先把两者的定位讲清楚,这决定了你该在什么场景用哪个。

维度 animateTo Animator
形态 函数,包裹属性修改 对象,有生命周期方法
驱动方式 修改组件属性触发 onFrame 回调逐帧给值
生命周期控制 无(一次性执行) play / pause / reverse / cancel
拿到中间值 不能 每帧都能拿到
典型用途 简单属性过渡(位移、缩放、透明度) 自定义绘制、非属性动画、可交互动画

一句话总结:animateTo 让"属性变化"变得平滑;Animator 让你拿到"随时间变化的数值",怎么用这个数值完全由你决定。 前者面向属性,后者面向数据。

举个例子:做一个进度环从 0 走到 100%,如果用 Canvas 手绘弧线,那"当前百分比"不是任何组件的属性,animateTo 无能为力,而 AnimatoronFrame(value) 正好把 0→100 的插值一帧帧喂给你去绘制。


二、Animator 核心 API

2.1 创建

推荐通过 UIContext 创建 Animator:

import { AnimatorResult, AnimatorOptions } from '@kit.ArkUI';

const options: AnimatorOptions = {
  duration: 2000,
  easing: 'ease-in-out',
  delay: 0,
  fill: 'forwards',
  direction: 'normal',
  iterations: 1,
  begin: 0,
  end: 100
};

const anim: AnimatorResult = this.getUIContext().createAnimator(options);

2.2 AnimatorOptions 参数

字段 说明
duration 动画时长(ms)
easing 缓动曲线:linear / ease / ease-in / ease-out / ease-in-out / 贝塞尔
delay 延迟开始(ms)
fill 结束后的停留状态:none / forwards / backwards / both
direction 方向:normal / reverse / alternate / alternate-reverse
iterations 循环次数(-1 为无限)
begin 插值起始值
end 插值结束值

beginend 定义了 onFrame 回调值的范围。本文设为 0~100,正好当百分比用。

2.3 回调与控制方法

anim.onFrame = (value: number) => { /* 每帧的插值 */ };
anim.onFinish = () => { /* 播放完成 */ };
anim.onCancel = () => { /* 被取消 */ };
anim.onRepeat = () => { /* 每次循环重复 */ };

anim.play();      // 播放
anim.pause();     // 暂停
anim.reverse();   // 反向播放
anim.cancel();    // 取消(回到初始)
anim.update(opts);// 更新参数
anim.finish();    // 直接结束到终点

onFrame 是核心——动画运行期间,系统按屏幕刷新率高频调用它,把当前插值传给你。你在这里更新 @State,界面就动起来了。


三、实战:逐帧动画实验室

3.1 状态与 Animator 持有

import { router, AnimatorResult, AnimatorOptions } from '@kit.ArkUI';
import { FontSize, Spacing } from '../common/Constants';

@Entry
@Component
struct FrameAnimatorLabPage {
  @State frameValue: number = 0;    // onFrame 给的当前值
  @State statusText: string = '就绪';
  @State easingIndex: number = 4;
  @State duration: number = 2000;
  @State iterations: number = 1;

  private easings: string[] = ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'];
  private anim: AnimatorResult | undefined = undefined;
}

注意 anim 是一个普通成员(不是 @State),因为 Animator 对象本身不参与 UI 渲染;真正驱动界面的是它回调里更新的 frameValue

3.2 创建与参数更新

把"创建"和"更新"合并到一个方法里:首次调用时创建并绑定回调,之后只更新参数。这样切换缓动曲线、时长时不必重复创建对象:

private buildOptions(): AnimatorOptions {
  const options: AnimatorOptions = {
    duration: this.duration,
    easing: this.easings[this.easingIndex],
    delay: 0,
    fill: 'forwards',
    direction: 'normal',
    iterations: this.iterations,
    begin: 0,
    end: 100
  };
  return options;
}

// 首次创建 Animator 并绑定帧回调,之后仅更新参数
private ensureAnimator(): void {
  if (this.anim === undefined) {
    const created: AnimatorResult = this.getUIContext().createAnimator(this.buildOptions());
    created.onFrame = (value: number) => {
      this.frameValue = Math.round(value);
    };
    created.onFinish = () => { this.statusText = '播放完成'; };
    created.onCancel = () => { this.statusText = '已取消'; };
    created.onRepeat = () => { this.statusText = '循环中'; };
    this.anim = created;
  } else {
    this.anim.update(this.buildOptions());
  }
}

onFrame 里把插值四舍五入后写进 frameValue,触发界面刷新。这就是逐帧动画的核心闭环:系统给值 → 更新状态 → 界面重绘

3.3 生命周期控制

四个控制方法直接映射到 Animator 的能力:

private play(): void {
  this.ensureAnimator();
  if (this.anim !== undefined) {
    this.anim.play();
    this.statusText = '播放中';
  }
}

private pause(): void {
  if (this.anim !== undefined) {
    this.anim.pause();
    this.statusText = '已暂停';
  }
}

private reverse(): void {
  this.ensureAnimator();
  if (this.anim !== undefined) {
    this.anim.reverse();
    this.statusText = '反向播放';
  }
}

private reset(): void {
  if (this.anim !== undefined) {
    this.anim.cancel();
  }
  this.frameValue = 0;
  this.statusText = '就绪';
}

这正是 Animator 相对 animateTo 的杀手锏——你能在动画运行到 47% 时按下暂停,它就停在 47;按反向,它从 47 往回走。animateTo 做不到这种精细控制。

注意用 if (this.anim !== undefined) 做判空,而不是 this.anim! 非空断言,因为 ArkTS 对非空断言的支持有限,显式判空更稳妥。

3.4 页面离开时清理

动画对象持有系统资源,页面销毁时要取消,避免泄漏:

aboutToDisappear(): void {
  if (this.anim !== undefined) {
    this.anim.cancel();
  }
}

3.5 用帧值驱动可视元素

拿到 frameValue(0~100)后,可以驱动任意界面元素。实验室里用了三种呈现:

大数字直接显示当前帧值:

Text(this.frameValue.toString())
  .fontSize(64)
  .fontColor('#FFFFFF')
  .fontWeight(FontWeight.Bold)

进度填充条用百分比宽度(width 支持百分比字符串):

Row()
  .width(this.frameValue + '%')
  .height(10)
  .borderRadius(5)
  .linearGradient({ angle: 90, colors: [['#34D399', 0], ['#059669', 1]] })

水平移动的小球layoutWeight 巧妙定位——左右两个空 Row 按 frameValue100 - frameValue 分配权重,把小球"挤"到对应位置:

Row() {
  Row().layoutWeight(this.frameValue > 0 ? this.frameValue : 0.001)
  Column()
    .width(28).height(28).borderRadius(14)
    .backgroundColor('#059669')
  Row().layoutWeight(this.frameValue < 100 ? 100 - this.frameValue : 0.001)
}

这个技巧不需要知道容器的具体宽度,纯靠权重比例定位,frameValue 从 0 到 100,小球就从最左平滑滑到最右。layoutWeight 为 0 时用 0.001 兜底,避免某些边界情况下的布局异常。

3.6 参数实时调节

缓动曲线用一排按钮切换,切换后调 ensureAnimator() 更新(下次播放生效):

ForEach(this.easings, (e: string, index: number) => {
  Text(e)
    .backgroundColor(this.easingIndex === index ? '#059669' : '#D1FAE5')
    .onClick(() => {
      this.easingIndex = index;
      this.ensureAnimator();
    })
})

时长和循环次数用滑块,同样在改变时更新参数。切到 ease-in-out 播放,你会看到小球两头慢、中间快;切 linear 则匀速;ease-out 是先快后慢。这直观展示了缓动曲线对动画节奏的影响。


在这里插入图片描述
在这里插入图片描述

四、几个实战要点与坑

4.1 onFrame 里只做轻量更新

onFrame 每秒可能被调用 60~120 次,里面只应做轻量的状态赋值,别做复杂计算、日志、网络等操作,否则会掉帧。需要复杂计算时,先在动画外算好,onFrame 里只做插值映射。

4.2 记得在页面销毁时 cancel

Animator 持有系统级动画资源,aboutToDisappear 里务必 cancel()。忘记清理,页面反复进出会累积泄漏,甚至出现"看不见的动画还在跑"。

4.3 update 后需重新 play

调用 update(options) 只更新参数,不会自动重新播放。改完参数要再 play() 才生效。本文在改参数时先 ensureAnimator() 更新,用户下次点播放即用新参数。

4.4 判空优于非空断言

ArkTS 严格模式下,this.anim!.play() 这种非空断言可能不被支持或触发告警。统一用 if (this.anim !== undefined) 判空,更稳更清晰。

4.5 begin/end 决定回调值域

onFrame 的值域完全由 begin/end 决定。想要角度就设 0~360,想要像素位移就设 0~屏宽,想要百分比就设 0~100。灵活利用这一点,能省去自己做数值映射的麻烦。


五、典型应用场景

  • Canvas 自定义动画:用 onFrame 的插值驱动画布重绘,实现进度环、波浪、粒子等属性动画覆盖不到的效果;
  • 可暂停/可拖拽的动画:需要用户能中途暂停、回退的引导动画、教程演示;
  • 数值滚动:数字从 0 滚动到目标值(计数器、金额、评分);
  • 多元素联动:一个 Animator 的插值同时驱动多个元素按不同规律运动;
  • 物理感交互:配合缓动曲线模拟加速、减速、回弹。

六、小结

本文以"逐帧动画实验室"为载体,系统讲解了 HarmonyOS NEXT 的 @ohos.animator

  • 核心定位:命令式动画对象,通过 onFrame 逐帧回调交出插值,可 play / pause / reverse / cancel 精细控制生命周期;
  • AnimatorOptionsduration / easing / iterations / begin / end 等参数定义动画行为与值域;
  • 与 animateTo 的区别:animateTo 面向属性、一次性;Animator 面向数据、可控制、可拿中间值;
  • 工程要点:onFrame 保持轻量、页面销毁要 cancel、update 后需重新 play、判空优于非空断言;
  • 实用技巧:用 layoutWeight 比例定位小球、用百分比宽度做进度条、用 begin/end 直接定义值域。

Animator 是 HarmonyOS 动画体系里"更底层、更灵活"的那一半。当 animateTo 的简单属性过渡不够用时,Animator 让你拿回对时间和数值的完全掌控,去实现任何你能想象的动画。

Logo

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

更多推荐