ArkTS 动画系统深度实战:属性动画、显式动画与页面转场全解析
ArkTS动画系统实战指南 本文深入解析鸿蒙ArkTS动画系统的三大核心体系: 属性动画:通过.animation()修饰符实现,适用于简单状态切换,自动响应属性变化,支持自定义缓动曲线和弹簧效果。 显式动画:使用animateTo()函数精确控制动画范围,支持完成回调,适合复杂联动动画场景。 页面转场:通过pageTransition()实现页面级过渡效果,提升应用整体流畅度。 文章通过实际代码
ArkTS 动画系统深度实战:属性动画、显式动画与页面转场全解析
适用版本:HarmonyOS NEXT / API 12+
难度:中级
阅读时间:约 15 分钟
动画是应用体验的核心竞争力之一。很多开发者在鸿蒙开发中卡在「动画为什么不生效」「转场效果闪烁」等问题上。本文从底层原理出发,系统梳理 ArkTS 动画三大核心体系,附带真实业务场景代码,让你彻底搞清楚"该用哪种动画 API"。
一、ArkTS 动画体系全景
ArkTS 的动画系统分为以下三层:
┌─────────────────────────────────────────────┐
│ 页面级动画(转场) │
│ pageTransition() / NavigationTransition │
├─────────────────────────────────────────────┤
│ 组件级动画 │
│ animateTo()(显式动画) │
├─────────────────────────────────────────────┤
│ 属性级动画 │
│ .animation()(属性动画) │
└─────────────────────────────────────────────┘
核心区别:
| 类型 | 触发方式 | 适用场景 | 精确度 |
|---|---|---|---|
属性动画 .animation() |
状态变化自动触发 | 简单状态切换 | 低 |
显式动画 animateTo() |
手动调用闭包 | 复杂联动动画 | 高 |
页面转场 pageTransition() |
路由跳转自动触发 | 页面切换效果 | 中 |
二、属性动画:最简单的动画方式
属性动画通过 .animation() 修饰符绑定到组件上,当组件的可动画属性(宽高、透明度、位移等)发生变化时自动播放。
2.1 基础用法
@Entry
@Component
struct AttributeAnimDemo {
@State private cardWidth: number = 200
@State private cardOpacity: number = 1.0
@State private cardRotate: number = 0
build() {
Column({ space: 30 }) {
// 核心:.animation() 修饰符绑定动画参数
Column() {
Text('点击我')
.fontSize(18)
.fontColor(Color.White)
}
.width(this.cardWidth)
.height(100)
.backgroundColor('#4096FF')
.borderRadius(12)
.opacity(this.cardOpacity)
.rotate({ angle: this.cardRotate })
.animation({
duration: 400, // 持续时间(ms)
curve: Curve.EaseInOut, // 缓动曲线
delay: 0, // 延迟
iterations: 1, // 播放次数(-1 = 无限循环)
playMode: PlayMode.Normal
})
.onClick(() => {
this.cardWidth = this.cardWidth === 200 ? 300 : 200
this.cardOpacity = this.cardOpacity === 1.0 ? 0.6 : 1.0
this.cardRotate = this.cardRotate === 0 ? 15 : 0
})
Button('重置').onClick(() => {
this.cardWidth = 200
this.cardOpacity = 1.0
this.cardRotate = 0
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
2.2 常见缓动曲线对照
// 系统内置曲线
Curve.Linear // 匀速
Curve.Ease // 慢→快→慢(默认)
Curve.EaseIn // 慢入快出
Curve.EaseOut // 快入慢出
Curve.EaseInOut // 慢入慢出
Curve.FastOutSlowIn // Material Design 标准曲线
Curve.Spring // 弹性效果
// 自定义贝塞尔曲线
curves.cubicBezierCurve(0.25, 0.1, 0.25, 1.0)
// 弹簧曲线(物理感强)
curves.springCurve(0, 1, 200, 20) // velocity, mass, stiffness, damping
2.3 属性动画的坑:.animation() 位置很重要
// ❌ 错误:animation 加在不需要动画的属性后面
Column()
.animation({ duration: 300 }) // 放在最前面不对
.width(this.width)
.backgroundColor(this.color)
// ✅ 正确:animation 放在需要动画属性的最后
Column()
.width(this.width)
.backgroundColor(this.color)
.animation({ duration: 300 }) // 作用于它前面的所有可动画属性
三、显式动画 animateTo:精确控制的利器
animateTo() 是更强大的动画 API,你可以精确控制哪些状态变化触发动画,并支持完成回调。
3.1 基础用法
@Entry
@Component
struct ExplicitAnimDemo {
@State private isExpanded: boolean = false
@State private listHeight: number = 0
@State private arrowAngle: number = 0
// 数据列表
private items: string[] = ['选项A', '选项B', '选项C', '选项D']
build() {
Column() {
// 折叠面板头部
Row() {
Text('展开菜单')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
Image($r('app.media.arrow_down'))
.width(20)
.height(20)
.rotate({ angle: this.arrowAngle })
// 箭头用属性动画即可
.animation({ duration: 300, curve: Curve.EaseInOut })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor('#F5F5F5')
.borderRadius(8)
.onClick(() => {
// 使用 animateTo 精确控制动画范围
animateTo(
{
duration: 350,
curve: Curve.FastOutSlowIn,
onFinish: () => {
console.log('动画完成,isExpanded =', this.isExpanded)
}
},
() => {
// 闭包内的所有状态变化都会触发动画
this.isExpanded = !this.isExpanded
this.listHeight = this.isExpanded ? this.items.length * 48 : 0
this.arrowAngle = this.isExpanded ? 180 : 0
}
)
})
// 折叠内容区
Column() {
ForEach(this.items, (item: string) => {
Text(item)
.fontSize(14)
.width('100%')
.height(48)
.padding({ left: 20 })
.fontColor('#666666')
})
}
.width('100%')
.height(this.listHeight)
.clip(true) // 必须加 clip,否则超出部分会显示
.backgroundColor('#FAFAFA')
}
.width('100%')
.padding(16)
}
}
3.2 实战:点赞动效(scale + opacity 组合)
@Component
struct LikeButton {
@State private liked: boolean = false
@State private likeCount: number = 128
@State private scale: number = 1.0
@State private heartOpacity: number = 0
build() {
Row({ space: 6 }) {
// 爱心图标
Stack() {
// 底层:灰色轮廓
Image($r('app.media.heart_outline'))
.width(24)
.height(24)
.fillColor(this.liked ? '#FF4D6D' : '#999999')
// 叠层:点赞粒子效果
Image($r('app.media.heart_filled'))
.width(36)
.height(36)
.fillColor('#FF4D6D')
.opacity(this.heartOpacity)
.scale({ x: this.scale, y: this.scale })
}
.width(36)
.height(36)
Text(`${this.likeCount}`)
.fontSize(14)
.fontColor(this.liked ? '#FF4D6D' : '#666666')
.animation({ duration: 200 })
}
.onClick(() => {
// 第一段动画:放大
animateTo({ duration: 150, curve: Curve.EaseOut }, () => {
this.liked = !this.liked
this.likeCount += this.liked ? 1 : -1
this.scale = 1.4
this.heartOpacity = this.liked ? 0.8 : 0
})
// 第二段动画:回弹(延迟 150ms)
animateTo({ duration: 200, curve: Curve.Spring, delay: 150 }, () => {
this.scale = 1.0
this.heartOpacity = 0
})
})
}
}
3.3 animateTo vs .animation() 选择原则
需要动画?
│
├─ 状态改变时"顺便"就触发动画 → .animation()
│ 例:按钮展开收缩、颜色切换
│
└─ 需要精确控制"哪次变化"触发动画 → animateTo()
例:点击特定按钮才触发、多段顺序动画、有完成回调
四、页面转场动画:让路由跳转更丝滑
4.1 使用 pageTransition() 自定义转场
// PageA.ets - 从右侧滑入
@Entry
@Component
struct PageA {
build() {
Column() {
Button('跳转到 PageB').onClick(() => {
router.pushUrl({ url: 'pages/PageB' })
})
}
.width('100%')
.height('100%')
}
// 进入动画:从右侧滑入
pageTransition() {
PageTransitionEnter({ duration: 350, curve: Curve.FastOutSlowIn })
.slide(SlideEffect.Right)
// 退出动画:向左淡出
PageTransitionExit({ duration: 300, curve: Curve.EaseIn })
.opacity(0)
.translate({ x: -80 })
}
}
// PageB.ets - 与 PageA 配合
@Entry
@Component
struct PageB {
build() {
Column() {
Button('返回').onClick(() => {
router.back()
})
}
.width('100%')
.height('100%')
}
pageTransition() {
// 进入时:从右侧推入
PageTransitionEnter({ duration: 350, curve: Curve.FastOutSlowIn })
.slide(SlideEffect.Right)
// 退出时:向右滑出(back 时)
PageTransitionExit({ duration: 300, curve: Curve.EaseIn })
.slide(SlideEffect.Right)
}
}
4.2 Navigation 路由下的转场(推荐写法)
HarmonyOS API 12 之后推荐使用 Navigation + NavDestination 替代 router,转场控制更细腻:
// AppNavigation.ets
@Entry
@Component
struct AppNavigation {
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack()
@Builder
pageMap(name: string) {
if (name === 'DetailPage') {
DetailPage()
} else if (name === 'ProfilePage') {
ProfilePage()
}
}
build() {
Navigation(this.pageInfos) {
// 主页内容
HomePage()
}
.navDestination(this.pageMap)
// 全局禁用默认转场,改用自定义
.customNavContentTransition((from, to, operation) => {
return {
timeout: 1000,
transition: (proxy) => {
// from page 淡出
from?.finishTransition()
// to page 从底部滑入
to?.startSharedTransition({
id: 'shared_hero',
duration: 400,
curve: Curve.FastOutSlowIn
})
}
}
})
}
}
4.3 共享元素转场(Hero Animation)
共享元素转场是电商、图库类应用的标配效果,ArkTS 原生支持:
// 列表页:商品卡片
@Component
struct ProductCard {
@Prop productId: string
@Prop imageUrl: ResourceStr
@Prop productName: string
build() {
Column() {
Image(this.imageUrl)
.width('100%')
.height(160)
.objectFit(ImageFit.Cover)
.borderRadius(8)
// 关键:geometryTransition 绑定共享 ID
.geometryTransition(`product_image_${this.productId}`)
.transition(TransitionEffect.OPACITY)
Text(this.productName)
.fontSize(14)
.margin({ top: 8 })
}
.onClick(() => {
// 跳转详情页,图片会平滑过渡
this.pageInfos?.pushPath({
name: 'ProductDetail',
param: { productId: this.productId }
})
})
}
}
// 详情页:大图展示
@Component
struct ProductDetail {
@State productId: string = ''
@State imageUrl: ResourceStr = ''
build() {
Column() {
Image(this.imageUrl)
.width('100%')
.height(300)
.objectFit(ImageFit.Cover)
// 相同的 geometryTransition ID = 触发共享元素动画
.geometryTransition(`product_image_${this.productId}`)
.transition(TransitionEffect.OPACITY)
// 详情内容...
}
}
}
五、高级技巧:自定义转场效果
5.1 TransitionEffect 组合
// 组合效果:淡入 + 向上位移 + 缩放
.transition(
TransitionEffect.OPACITY
.combine(TransitionEffect.translate({ y: 40 }))
.combine(TransitionEffect.scale({ x: 0.95, y: 0.95 }))
.animation({ duration: 350, curve: Curve.FastOutSlowIn })
)
// 不对称转场:进入和退出效果不同
.transition(
TransitionEffect.asymmetric(
// 进入:从下方滑入
TransitionEffect.translate({ y: 60 }).combine(TransitionEffect.OPACITY),
// 退出:向右侧淡出
TransitionEffect.translate({ x: 100 }).combine(TransitionEffect.OPACITY)
)
)
5.2 keyframeAnimateTo:关键帧动画
复杂的多段动画可以用关键帧,避免多个 animateTo 叠加的混乱:
// 模拟"弹跳加载"效果
animateToImmediately(
{ iterations: -1 }, // 无限循环
[
// 关键帧1:上升
{
duration: 400,
event: () => { this.ballY = -60 }
},
// 关键帧2:下落(带挤压效果)
{
duration: 300,
curve: Curve.Bounce,
event: () => {
this.ballY = 0
this.ballScaleX = 1.3
this.ballScaleY = 0.7
}
},
// 关键帧3:恢复形变
{
duration: 150,
event: () => {
this.ballScaleX = 1.0
this.ballScaleY = 1.0
}
}
]
)
六、踩坑总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
.animation() 不生效 |
修改了不可动画属性(如 visibility) | 改用 opacity 代替 visibility |
| animateTo 闭包外变量不动画 | 只有闭包内的状态变化才触发动画 | 确保状态变量在闭包内赋值 |
| 转场闪烁白屏 | pageTransition 配置了进入但没配退出 | 进入/退出动画需配对 |
| geometryTransition 错位 | 两端组件尺寸不一致时未指定 mode | 添加 .geometryTransition(id, { follow: true }) |
| 列表 ForEach 动画卡顿 | 每次 rebuild 都触发动画 | 用 @ObjectLink 减少不必要的组件重建 |
| Spring 曲线参数不懂 | stiffness(刚度)和 damping(阻尼)含义模糊 | stiffness↑弹得快,damping↑越不弹跳 |
七、最佳实践总结
选择 API 的简单决策树:
简单状态切换(宽高/颜色)?
└─ 是 → .animation()
需要精确控制 / 有回调?
└─ 是 → animateTo()
页面级跳转?
└─ router → pageTransition()
└─ Navigation → customNavContentTransition()
两个页面有相同元素?
└─ geometryTransition(共享元素转场)
多段顺序动画?
└─ animateToImmediately(关键帧)
动画的本质是"状态驱动"——ArkTS 的所有动画 API 都是围绕状态变化展开的。掌握这一思路后,你会发现动画实现远比你想象的简单。
参考资料
更多推荐





所有评论(0)