ArkUI 动画体系:显式动画与转场动画全面对比

封面信息图 一文搞懂 animateTo、transition、pageTransition 的本质区别与适用场景,避免动画卡顿和属性失效。

适用版本:HarmonyOS NEXT / API 12+

阅读时长:约 18 分钟

---

一、从一个真实场景切入

你打开一个电商 App,点击"加入购物车",商品图片飞向右上角购物车图标——这是显式动画。你从商品列表页跳转到商品详情页,页面从右侧滑入——这是页面转场动画。你展开折叠的评论区,评论列表渐显——这是组件转场动画

三种动画在 ArkUI 中对应三套 API,但许多开发者会把它们混用,导致:

- animateTo 里写了 transition,啥效果都没有

- 页面转场不生效,因为少写了 pageTransition

- 转场动画方向搞反,因为误解了 TransitionEffect 的"进入/退出"语义

本文系统梳理这三套 API 的底层机制、对比关键差异、给出可直接运行的代码,并整理高频踩坑点。

---

二、ArkUI 动画体系全貌

ArkUI 动画体系

├── 属性动画(隐式)

│ └── animation() 修饰符:属性变化时自动触发

├── 显式动画

│ └── animateTo(param, closure):在闭包中修改状态,批量驱动动画

└── 转场动画

├── transition(effect):组件出现/消失时触发(if/ForEach 控制)

├── sharedTransition(id):跨页面共享元素转场

└── pageTransition():整页进入/退出动画(仅 @Entry 页面)

核心差异animateTo 驱动"已存在组件"的属性变化; transition 处理"组件挂载/卸载"时的动画; pageTransition 处理"整页路由跳转"。

---

三、显式动画:animateTo

3.1 工作原理

animateTo 内部实现基于 AttributeModifier 属性动画引擎,执行流程:
animateTo(param) {

修改 @State 变量

}

ArkUI 标记属性 dirty

下一帧 VSYNC 信号到来

Layout/Measure 阶段对比新旧属性值

为发生变化的属性插值,逐帧渲染

duration 到达后属性稳定在终止值

3.2 完整参数解析

animateTo(

{

duration: 400, // 动画时长(ms)

curve: Curve.EaseInOut, // 缓动曲线

delay: 0, // 延迟(ms)

iterations: 1, // 重复次数(-1=无限)

playMode: PlayMode.Normal,

onFinish: () => { /* 动画结束回调 */ }

},

() => {

// 在此修改驱动动画的状态变量

this.offsetX = 200

this.opacity = 0.5

}

)

3.3 可动画属性 vs 不可动画属性

| 可动画 | 不可动画 |

|--------|---------|

| width/height | 文本内容 |

| opacity | 组件类型 |

| rotate/scale/translate | 布局方向 |

| backgroundColor | fontSize(需特殊处理) |

| borderRadius | 条件渲染(if/ForEach) |

3.4 正确写法 vs 错误写法

错误写法(条件渲染放进 animateTo 闭包):
// ❌ 问题:条件渲染不属于"属性变化",animateTo 无法对其插值

animateTo({ duration: 300 }, () => {

this.showCard = true // 这行只会让组件瞬间出现,无动画

})

问题所在showCard 控制组件的挂载/卸载,不是属性值变化, animateTo 无法对"有没有组件"进行插值。 正确写法(条件渲染改为透明度控制,或配合 transition):
// ✅ 方案A:用 opacity 替代 if 控制显隐

animateTo({ duration: 300 }, () => {

this.cardOpacity = 1.0 // opacity: 0→1 是属性动画

})

// ✅ 方案B:if 控制挂载,配合 transition 处理出现/消失动画

if (this.showCard) {

CardComponent()

.transition(TransitionEffect.OPACITY.animation({ duration: 300 }))

}

// 直接赋值触发挂载,transition 接管动画

this.showCard = true

---

四、组件转场动画:transition

4.1 核心语义

transition 的执行时机绑定在 组件树的挂载(mount)和卸载(unmount)

- 组件被 if/ForEach/Visibility.None→Visible 加入渲染树时 → 进入动画TransitionType.Insert

- 组件从渲染树移除时 → 退出动画TransitionType.Delete

4.2 TransitionEffect 链式语法(API 10+)

// 新式链式 API(推荐)

Column()

.transition(

TransitionEffect.OPACITY // 淡入淡出

.combine(TransitionEffect.translate({ x: 0, y: 30 })) // 同时上移30

.animation({ duration: 350, curve: Curve.EaseOut }) // 独立配置曲线

)

// 进入和退出使用不同效果

Column()

.transition(

TransitionEffect.asymmetric(

TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: -20 })), // 进入:从上落下

TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: 20 })) // 退出:向下消失

)

)

4.3 触发机制:必须配合状态变量

@Component

struct ExpandableSection {

@State expanded: boolean = false

build() {

Column() {

Button('展开/折叠')

.onClick(() => {

// ✅ 直接修改状态,transition 自动触发

this.expanded = !this.expanded

})

if (this.expanded) {

// expanded=true 时组件挂载 → 触发进入动画

// expanded=false 时组件卸载 → 触发退出动画

Text('折叠内容区')

.transition(

TransitionEffect.OPACITY

.combine(TransitionEffect.scale({ x: 0.95, y: 0.95 }))

.animation({ duration: 250 })

)

}

}

}

}

错误写法(在 animateTo 闭包中触发 transition):
// ❌ 问题:animateTo 不会增强 transition,两者作用域不同

// transition 会触发,但 duration 以 transition.animation 为准,animateTo 的参数被忽略

animateTo({ duration: 300 }, () => {

this.expanded = true

})

正确写法:直接赋值触发, transition 自带 animation 配置。

---

五、页面转场动画:pageTransition

5.1 适用范围

pageTransition() 只能在 @Entry 装饰的组件(即路由页面组件)中定义,用于整页的进入和退出动画。
Router.pushUrl()  →  新页面 PageTransitionEnter 动画

Router.back() → 当前页面 PageTransitionExit 动画,前一页面 PageTransitionEnter 动画

5.2 完整写法

@Entry

@Component

struct ProductDetailPage {

build() {

// 页面内容

Column() { /* ... */ }

}

// 在 build() 同级定义 pageTransition()

pageTransition() {

// 进入动画:从右侧滑入

PageTransitionEnter({ duration: 350, curve: Curve.EaseOut })

.slide(SlideEffect.Right)

// 退出动画:向左侧滑出

PageTransitionExit({ duration: 350, curve: Curve.EaseIn })

.slide(SlideEffect.Left)

}

}

5.3 与 Navigation 路由的差异

若项目使用 Navigation + NavDestination 体系(推荐方式),页面转场由 NavDestination 接管:

// Navigation 体系下的转场配置

Navigation(this.pageStack) {

// ...

}

.customNavContentTransition((from, to, operation) => {

// 基于 operation (PUSH/POP) 决定动画方向

return {

timeout: 1000,

transition: (proxy) => {

// from/to 为 NavContentInfo,可访问各页面的 NavDestinationContext

}

}

})

---

六、三者横向对比

| 维度 | animateTo | transition | pageTransition |

|------|-----------|------------|----------------|

| 触发时机 | 手动调用 | 组件挂载/卸载 | 路由跳转 |

| 作用对象 | 已存在组件的属性 | 组件出现/消失 | 整页 |

| 适用场景 | 交互反馈、状态切换 | 列表增删、折叠展开 | 页面导航 |

| 动画配置位置 | 调用时传参 | .transition() 修饰符 | pageTransition() 函数 |

| 能否配合使用 | 不能直接嵌套 | 可组合 TransitionEffect | 独立作用域 |

| API 层级 | 通用 | 组件级 | 路由级 |

---

七、最佳实践

实践 1:对「显隐切换」统一用 transition,而非 animateTo

做法:凡是用 if 控制组件出现/消失的场景,在组件上加 .transition() 而非用 animateTo 包裹赋值。 原因animateTo 无法对"有没有组件"进行插值,闭包内改变 if 的状态变量只会让组件瞬间出现/消失,无动画。 不这样做会怎样:动画效果完全缺失,组件直接跳变,用户体验差。

---

实践 2:transition 动画时长不要过长,避免 UI 阻塞感

做法transitionanimation.duration 建议设置在 150~350ms,进入用 EaseOut,退出用 EaseIn原因:超过 400ms 的转场动画会让用户感觉 UI"慢";退出动画结束后页面才会被完全卸载,过长会延迟内存释放。 不这样做会怎样:用户会反复多次点击(误操作),并认为 App 性能差。

---

实践 3:在 Navigation 体系中不要混用 pageTransition

做法:项目一旦使用 Navigation 路由,统一通过 customNavContentTransition 配置转场,不要在 NavDestination 内部写 pageTransition原因pageTransition 属于 @Entry 页面路由体系,在 NavDestination 内不生效,会造成转场效果缺失且难以排查。 不这样做会怎样:转场动画静默失效,花时间排查却找不到原因。

---

实践 4:多个属性同步动画优先用 animateTo 批量驱动

做法:需要多个属性同时动画时,将所有状态变量修改放在同一个 animateTo 闭包内。 原因:同一 animateTo 闭包中的所有属性共享同一套动画参数(duration/curve),保证动画同步结束,避免视觉错位。 不这样做会怎样:分别用多个 animation() 修饰符,曲线参数不统一时属性动画不同步结束,产生"抖动"感。

---

八、常见坑点

坑 1:transition 退出动画不执行

现象:组件出现时有淡入动画,消失时直接消失无动画。 原因:退出动画在组件 从渲染树移除前执行,若父组件同时被销毁(如 if 控制的父容器也消失了),子组件的退出动画来不及播放就被强制卸载。 复现
if (this.show) {

Column() { // ← 父容器也受 if 控制

Text('子组件').transition(TransitionEffect.OPACITY.animation({ duration: 300 }))

}

}

解决:让父容器不消失,只控制子组件的挂载/卸载,或给父容器也加 transition

---

坑 2:animateTo 在异步回调中调用不生效

现象:在 onAppearsetTimeout 回调、异步函数中调用 animateTo,动画不触发。 原因animateTo 必须在 主线程同步上下文中执行,且需要绑定到当前组件的执行上下文( UIContext)。在异步回调中调用时上下文可能丢失。 复现
.onAppear(() => {

setTimeout(() => {

animateTo({ duration: 300 }, () => { this.x = 100 }) // ❌ 可能不生效

}, 100)

})

解决:使用 getUIContext().animateTo() 绑定明确的 UI 上下文:
// ✅ 使用 UIContext 版本

.onAppear(() => {

setTimeout(() => {

this.getUIContext().animateTo({ duration: 300 }, () => {

this.x = 100

})

}, 100)

})

---

坑 3:pageTransition 在 Navigation 路由下不生效

现象:在页面组件中写了 pageTransition(),但路由跳转时没有转场动画。 原因:使用 Navigation 路由时,页面组件不再是 @EntrypageTransition() 不会被路由系统调用。 复现:项目使用 Navigation(this.navStack) 路由,在子页面写 pageTransition()解决:改用 customNavContentTransitionNavigation 层面统一配置转场。

---

九、总结

1. animateTo 驱动已有组件的属性渐变,适用于交互反馈和状态切换

2. transition 处理组件挂载/卸载时的出现/消失动画,必须由 if/ForEach 触发

3. pageTransition 仅适用于 @Entry 路由页面的整页转场,Navigation 体系用 customNavContentTransition

4. 三者不能直接嵌套混用,各自管理各自作用域

5. 退出动画失效首查父容器是否同时消失;animateTo 不生效首查是否在异步上下文中调用

核心结论:选对 API 比调参数更重要——显隐用 transition,属性变化用 animateTo,路由跳转用 pageTransition/customNavContentTransition。

---

参考资料

- ArkUI 动画概述 - 官方文档

- 显式动画 animateTo - API 参考

- 组件内转场动画 - 官方文档

- 页面间转场动画 - 官方文档

- Navigation 自定义转场 - 官方文档

- OpenHarmony 源码路径:foundation/arkui/ace_engine/frameworks/core/animation/

Logo

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

更多推荐