动画是 UI 的灵魂——没有动画的页面像幻灯片翻页,有了动画才是"活的"交互。本文用五个独立 Demo 覆盖 ArkUI 中 animateTo 的四种核心变换和组合用法,每个 Demo 自包含且可直接运行。


一、我们要做什么

一个动画演示页面,包含五组独立动画:

  1. 缩放动画(Scale) — 元素放大回弹,模拟"点赞"效果
  2. 旋转动画(Rotate) — 图标 360° 旋转
  3. 平移动画(Translate) — 卡片左右滑动
  4. 透明度动画(Opacity) — 色块淡入淡出
  5. 组合动画 — scale + rotate + opacity 同时变化

每个 Demo 由"触发按钮 → 状态变化 → animateTo 过渡"驱动。


二、animateTo 基础

2.1 核心原理

animateTo({ duration: 300, curve: Curve.EaseInOut }, () => {
  this.someState = newValue;  // 修改 @State 变量
});

三要素:

  1. duration(时长) — 毫秒,300ms 是移动端动画的黄金时长
  2. curve(曲线) — 决定速度变化方式,非匀速
  3. 闭包中的状态修改 — ArkUI 拦截闭包中的 @State 变化,用动画过渡到新值

animateTo 不是"播放一段动画",而是"把状态变化的瞬间拉伸成平滑过渡"。这和 Flutter 的 AnimationController 思路一致,但 API 更简洁。

2.2 动画曲线对比

曲线 感觉 适用场景
Curve.EaseIn 先慢后快 元素离开屏幕
Curve.EaseOut 先快后慢 元素进入屏幕
Curve.EaseInOut 两端慢中间快 状态切换(最常用)
Curve.FastOutSlowIn 快出慢进 平移滑动
Curve.Spring 弹簧回弹 点赞、心动效果

选对曲线比选对时长更重要。错误的曲线会让动画感觉"不对"——即使只差 100ms。

2.3 三件重要的事

第一:animateTo 只对写在闭包中的 @State 变化生效。

// 正确 — myValue 在闭包中被修改
animateTo({ duration: 300 }, () => {
  this.myValue = 100;
});

// 错误 — myValue 在闭包外被修改,animateTo 无效
this.myValue = 100;
animateTo({ duration: 300 }, () => {
  // 空的?什么都不会发生
});

第二:父组件 animateTo 不会影响子组件内的状态。

每个 animateTo 只作用于它闭包中直接修改的 @State。子组件的 @State 是独立的。

第三:animateTo 是异步的。

调用 animateTo 后立即执行下一行代码,动画在后台播放。如果需要动画完成后做某事,用 setTimeout

animateTo({ duration: 300 }, () => { this.value = 100; });
setTimeout(() => {
  // 动画已结束
}, 300);

在这里插入图片描述

三、Demo 1:缩放动画 — 回弹效果

@State scaleValue: number = 1;

private animateScale(): void {
  // 第一步:放大
  animateTo({ duration: 150, curve: Curve.EaseOut }, () => {
    this.scaleValue = 1.5;
  });
  // 第二步:150ms 后缩回
  setTimeout(() => {
    animateTo({ duration: 200, curve: Curve.EaseIn }, () => {
      this.scaleValue = 1;
    });
  }, 150);
}

// 元素上应用
Row()
  .width(80).height(80)
  .backgroundColor(AppColors.PRIMARY)
  .borderRadius(40)
  .scale({ x: this.scaleValue, y: this.scaleValue })

两步动画构成"弹跳":先放大到 1.5 倍(150ms,EaseOut 先快后慢),再缩回原大小(200ms,EaseIn 先慢后快)。

为什么用两个 animateTo 而不是大 duration? 单个 animateTo 只能做单方向过渡——从 1 到 1.5 再回到 1?不存在的,animateTo 只从当前值过渡到目标值。要实现"去-回"效果,必须分两步。

为什么第二步用 setTimeout 而不是直接写在后面? animateTo 不等动画播完就返回了。直接写在后面的代码立即执行,会导致两个 animateTo 几乎同时触发——结果是从 1 直接跳到 1.5(第一步还没播完就被第二步覆盖)。setTimeout 的延迟匹配第一步的 duration(150ms),确保第一步播完再播第二步。

这个缩放模式可以直接用于:

  • 点赞按钮(心形图标放大回弹)
  • 收藏按钮
  • 添加到购物车
  • 任何"确认操作"的即时反馈

在这里插入图片描述

四、Demo 2:旋转动画

@State rotateAngle: number = 0;

private animateRotate(): void {
  const target = this.rotateAngle === 0 ? 360 : 0;
  animateTo({ duration: 500, curve: Curve.EaseInOut }, () => {
    this.rotateAngle = target;
  });
}

// 元素上应用
Image($r('sys.symbol.arrow_clockwise'))
  .width(48).height(48)
  .fillColor(AppColors.PRIMARY)
  .rotate({ angle: this.rotateAngle })

每次点击在 0° 和 360° 之间切换。Curve.EaseInOut 让旋转在两端慢、中间快——模拟物理世界的惯性。

rotate 的角度单位是度(degrees),不是弧度(radians)。 360 = 一圈,和 CSS 的 transform: rotate(360deg) 一致。

可以扩展为:

  • 刷新按钮的加载动画(用 setInterval 连续增加角度)
  • 展开/收起箭头的旋转
  • 设置齿轮的转动

在这里插入图片描述

五、Demo 3:平移动画

@State translateX: number = 0;

private animateTranslate(): void {
  const target = this.translateX === 0 ? 60 : 0;
  animateTo({ duration: 400, curve: Curve.FastOutSlowIn }, () => {
    this.translateX = target;
  });
}

// 元素上应用
Row()
  .width(80).height(80)
  .backgroundColor('#FAAD14')
  .borderRadius(BorderRadius.MD)
  .translate({ x: this.translateX, y: 0 })

元素在 X 轴方向来回滑动 60vp。Curve.FastOutSlowIn —— Material Design 标准曲线,快出慢进,完美适配平移。

可以扩展为:

  • 抽屉菜单的滑入滑出
  • 轮播卡片切换
  • 滑动删除确认(元素向右滑出屏幕)

六、Demo 4:透明度动画

@State opacityValue: number = 1;

private animateOpacity(): void {
  const target = this.opacityValue === 1 ? 0.15 : 1;
  animateTo({ duration: 500, curve: Curve.EaseInOut }, () => {
    this.opacityValue = target;
  });
}

// 元素上应用
Row()
  .width('80%').height(60)
  .backgroundColor('#722ED1')
  .borderRadius(BorderRadius.MD)
  .opacity(this.opacityValue)

透明度在 1(完全不透明)和 0.15(几乎透明)之间切换。注意不是到 0——完全透明会让元素消失,用户不知道为什么点不到。0.15 保留一丝可见性,暗示"它还在这里"。

可以扩展为:

  • 模态遮罩层淡入淡出
  • 骨架屏切换为真实内容
  • Toast 提示的出现消失

关键细节:.opacity(0.15) 不是 .opacity(0) 0.15 的可见度让用户知道元素没有消失,只是半隐藏状态。如果到 0,用户看不到回不来了——对"复原"操作造成认知负担。


七、Demo 5:组合动画

@State comboScale: number = 1;
@State comboAngle: number = 0;
@State comboOpacity: number = 1;

private animateCombo(): void {
  const shrink = this.comboScale === 1;
  animateTo({ duration: 500, curve: Curve.EaseInOut }, () => {
    this.comboScale = shrink ? 1.3 : 1;
    this.comboAngle = shrink ? 180 : 0;
    this.comboOpacity = shrink ? 0.4 : 1;
  });
}

// 元素上应用
Text('★')
  .fontSize(48)
  .fontColor('#FAAD14')
  .scale({ x: this.comboScale, y: this.comboScale })
  .rotate({ angle: this.comboAngle })
  .opacity(this.comboOpacity)

三个属性在同一个 animateTo 闭包中同时修改——ArkUI 自动同步三者的过渡,不会出现"缩放完了旋转还没开始"的错位。

一个 animateTo 闭包中修改多个 @State → 所有过渡同步进行。 这是实现复杂动画的正确方式——把相关的变化放在同一个闭包中,框架保证同步。


八、动画性能注意事项

8.1 只有 transform 动画是 GPU 加速的

scalerotatetranslateopacity 在 GPU 层执行,不触发重新布局。这意味着:

  • 60fps 流畅(即使在低端设备上)
  • 不影响列表滚动性能
  • 不会触发子组件的 build() 重执行

8.2 不要对布局属性用 animateTo

// 坏 — 修改 width 触发重新布局,性能差
animateTo({ duration: 300 }, () => {
  this.width = 200;  // 不适合动画!
});

// 好 — 用 scale 模拟"变大"
animateTo({ duration: 300 }, () => {
  this.scaleValue = 2;  // GPU 加速
});

widthheightmargin 等布局属性变化会触发整个布局树的重新计算。scale 是视觉变换,不影响布局,性能高一个数量级。

8.3 避免在动画中修改列表数据

如果 100 个 ListItem 同时播放动画,即使每个只改 opacity,也是 100 个动画同时运行。对于列表场景,优先使用 ListItem 的内置动画(如 swipeAction),自己写的 animateTo 要做好虚拟化测试。


九、常见面试题 / 踩坑点

9.1 animateTo 对哪些属性有效?

@State@Link 装饰的变量有效,包括数字、字符串、布尔值。对 private 普通属性无效——因为框架不追踪它们的引用变化。

有效属性类型:numberstringbooleanColor(通过数字 RGBA)

9.2 动画期间用户能交互吗?

能。animateTo 不阻塞主线程,动画播放期间用户可以点击其他按钮、滚动列表。如果你不想让用户在动画期间操作(例如防止重复点击),需要手动加锁:

private animating: boolean = false;

private playAnimation(): void {
  if (this.animating) return;  // 防重入
  this.animating = true;
  animateTo({ duration: 300 }, () => { this.value = target; });
  setTimeout(() => { this.animating = false; }, 300);
}

9.3 .scale({ x: 1.5, y: 1.5 }) 影响子元素吗?

影响。如果缩放一个 Row,里面的所有 TextImage 都会被等比缩放。包括字体大小、边框宽度、阴影半径——是真正的视觉缩放。

9.4 为什么不用 CSS 动画?

ArkUI 的动画体系师从 Flutter,不是 Web 的 CSS 动画。核心区别:

  • CSS 动画:声明式,分离在样式表中
  • ArkUI 动画:命令式,内嵌在业务逻辑中,animateTo 和状态修改紧密关联

这种设计的好处是"动画 = 状态过渡"——不需要额外的动画变量和 keyframe,改状态就自动有动画。


十、运行方式

代码位于 dev/entry/src/main/ets/pages/AnimationPage.ets

用 DevEco Studio 打开 dev/ 项目,首页点击"动画演示 — 缩放旋转淡入平移"即可体验:

  1. 页面展示五张 Demo 卡片,每张有独立动画
  2. 点击"触发缩放"→ 蓝色圆圈放大 1.5 倍后弹回
  3. 点击"触发旋转"→ 箭头图标旋转 360°,再点一次反向旋转
  4. 点击"触发平移"→ 黄色方块向右滑 60vp,再点滑回
  5. 点击"触发淡入淡出"→ 紫色色块变淡到 0.15,再点恢复
  6. 点击"全部触发"→ 金色星星同时缩放、旋转、变淡

十一、扩展方向

  • 连续动画序列 — 用 setTimeout 链实现 A→B→C→D 多阶段动画
  • 手势驱动的动画 — 在 PanGesture 回调中用 animateTo 实现跟手动画
  • 页面转场动画 — 在 router.pushUrl 时配置自定义转场效果
  • 列表动画 — 结合 ForEach 的增删,用 animateTo 让新元素滑入、删除的元素淡出
  • 骨架屏切换 — 数据加载完成后,骨架灰块用 opacity 淡出,真实内容淡入
  • 物理弹簧 — 使用 Curve.SpringinterpolatingSpring 实现更自然的物理效果
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐