【鸿蒙】ArkUI 动画体系:显式动画与转场动画全面对比
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 阻塞感
做法:transition 的 animation.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 在异步回调中调用不生效
现象:在onAppear、 setTimeout 回调、异步函数中调用 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 路由时,页面组件不再是 @Entry, pageTransition() 不会被路由系统调用。 复现:项目使用 Navigation(this.navStack) 路由,在子页面写 pageTransition()。 解决:改用 customNavContentTransition 在 Navigation 层面统一配置转场。
---
九、总结
1. animateTo 驱动已有组件的属性渐变,适用于交互反馈和状态切换
2. transition 处理组件挂载/卸载时的出现/消失动画,必须由 if/ForEach 触发
3. pageTransition 仅适用于 @Entry 路由页面的整页转场,Navigation 体系用 customNavContentTransition
4. 三者不能直接嵌套混用,各自管理各自作用域
5. 退出动画失效首查父容器是否同时消失;animateTo 不生效首查是否在异步上下文中调用
核心结论:选对 API 比调参数更重要——显隐用 transition,属性变化用 animateTo,路由跳转用 pageTransition/customNavContentTransition。---
参考资料
- OpenHarmony 源码路径:foundation/arkui/ace_engine/frameworks/core/animation/
更多推荐


所有评论(0)