鸿蒙动画开发全指南:属性动画+显式动画+转场动画+路径动画,一文掌握所有动画类型
本文是鸿蒙NEXT开发实战系列第32篇,重点讲解ArkUI动画系统。文章详细介绍了四种动画类型:属性动画(animateTo)、显式动画(animation)、转场动画(pageTransition)和路径动画(motionPath),并提供了完整代码示例。同时分享了动画性能优化技巧,包括避免过度动画、使用硬件加速属性、合理设置动画参数等。最后总结了动画类型选择指南和最佳实践,帮助开发者在保证性能
📖 鸿蒙NEXT开发实战系列 | 第32篇 | 实战篇 🎯 适合人群:有ArkUI基础的开发者 ⏰ 阅读时间:约15分钟 | 💻 开发环境:DevEco Studio 5.0+
导航链接
📑 目录
一、前言:动画在App中的重要性
好的动画设计能让App体验提升一个档次。动画不仅仅是视觉上的装饰,它在用户体验中扮演着重要角色:
-
引导用户注意力:通过动画引导用户关注重要内容
-
提供操作反馈:让用户的操作得到即时响应
-
增强界面连贯性:让页面切换更加自然流畅
-
提升品牌感知:独特的动画风格可以增强品牌形象
鸿蒙ArkUI提供了强大的动画系统,支持多种动画类型,能够满足各种复杂的动画需求。
二、动画类型总览
鸿蒙ArkUI的动画系统主要包含以下四种类型:
|
动画类型 |
使用场景 |
核心API |
|---|---|---|
|
属性动画 |
组件属性变化时的过渡动画 |
|
|
显式动画 |
显式控制动画的开始和结束 |
|
|
转场动画 |
页面或组件的入场/退场动画 |
|
|
路径动画 |
沿指定路径运动的动画 |
|
三、属性动画 animateTo
属性动画是鸿蒙中最常用的动画类型,当组件的属性发生变化时,系统会自动创建平滑的过渡动画。
3.1 基本语法
animateTo({
duration: 300, // 动画时长(毫秒)
tempo: 1.0, // 动画速率
curve: Curve.EaseInOut, // 动画曲线
delay: 0, // 延迟时间
iterations: 1, // 迭代次数(-1为无限循环)
playMode: PlayMode.Normal, // 播放模式
onFinish: () => {} // 动画结束回调
}, () => {
// 属性变化的闭包
this.width = 200
this.opacity = 0.5
})
3.2 完整示例:按钮点击动画
@Entry
@Component
struct AnimateToDemo {
@State width: number = 100
@State height: number = 100
@State opacity: number = 1
@State rotateAngle: number = 0
@State scaleValue: number = 1
@State bgColor: string = '#007DFF'
build() {
Column({ space: 20 }) {
Text('属性动画 animateTo 演示')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 动画演示区域
Row() {
Column() {
Image($r('app.media.icon'))
.width(this.width)
.height(this.height)
.opacity(this.opacity)
.rotate({ angle: this.rotateAngle })
.scale({ x: this.scaleValue, y: this.scaleValue })
.backgroundColor(this.bgColor)
.borderRadius(12)
.animation({
duration: 500,
curve: Curve.EaseInOut
})
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center)
.backgroundColor('#F5F5F5')
.borderRadius(16)
}
.padding(16)
// 控制按钮
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
Button('改变大小')
.onClick(() => {
animateTo({
duration: 500,
curve: Curve.EaseInOut
}, () => {
this.width = this.width === 100 ? 200 : 100
this.height = this.height === 100 ? 200 : 100
})
})
.margin(8)
Button('改变透明度')
.onClick(() => {
animateTo({
duration: 300,
curve: Curve.Linear
}, () => {
this.opacity = this.opacity === 1 ? 0.3 : 1
})
})
.margin(8)
Button('旋转')
.onClick(() => {
animateTo({
duration: 800,
curve: Curve.EaseOut
}, () => {
this.rotateAngle += 90
})
})
.margin(8)
Button('缩放')
.onClick(() => {
animateTo({
duration: 400,
curve: Curve.FastOutSlowIn
}, () => {
this.scaleValue = this.scaleValue === 1 ? 1.5 : 1
})
})
.margin(8)
Button('改变颜色')
.onClick(() => {
animateTo({
duration: 600,
curve: Curve.Linear
}, () => {
this.bgColor = this.bgColor === '#007DFF' ? '#FF6B6B' : '#007DFF'
})
})
.margin(8)
}
.width('100%')
}
.padding(16)
.width('100%')
.height('100%')
}
}
3.3 常用动画曲线
|
曲线类型 |
说明 |
适用场景 |
|---|---|---|
|
|
匀速运动 |
进度条、旋转 |
|
|
缓入缓出 |
通用动画 |
|
|
缓入 |
元素消失 |
|
|
缓出 |
元素出现 |
|
|
缓入缓出 |
平滑过渡 |
|
|
快出慢入 |
Material Design风格 |
|
|
弹簧效果 |
回弹动画 |
四、显式动画
显式动画使用 animation() 属性修饰器,可以在属性变化时自动创建动画效果。与 animateTo 不同的是,显式动画可以为每个组件单独配置动画参数。
4.1 基本语法
Component()
.width(this.width)
.height(this.height)
.animation({
duration: 300,
curve: Curve.EaseInOut,
delay: 0,
iterations: 1,
playMode: PlayMode.Normal
})
4.2 完整示例:列表项动画
@Entry
@Component
struct AnimationDemo {
@State items: number[] = [1, 2, 3, 4, 5]
@State showDetail: boolean = false
@State rotateAngle: number = 0
build() {
Column({ space: 16 }) {
Text('显式动画演示')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 展开/收起动画
Column() {
Row() {
Text('点击展开详情')
.fontSize(16)
.layoutWeight(1)
Image($r('app.media.arrow'))
.width(24)
.height(24)
.rotate({ angle: this.rotateAngle })
.animation({
duration: 300,
curve: Curve.EaseInOut
})
}
.width('100%')
.padding(16)
.onClick(() => {
animateTo({
duration: 300,
curve: Curve.EaseInOut
}, () => {
this.showDetail = !this.showDetail
this.rotateAngle = this.showDetail ? 180 : 0
})
})
if (this.showDetail) {
Column({ space: 8 }) {
Text('这里是详细内容区域')
Text('可以放置更多信息...')
}
.width('100%')
.padding(16)
.opacity(this.showDetail ? 1 : 0)
.animation({
duration: 300,
curve: Curve.EaseIn
})
}
}
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({ radius: 4, color: '#1A000000' })
// 列表项动画
Column({ space: 12 }) {
Text('列表项入场动画')
.fontSize(18)
.fontWeight(FontWeight.Medium)
ForEach(this.items, (item: number, index: number) => {
Row() {
Text(`Item ${item}`)
.fontSize(16)
.layoutWeight(1)
}
.width('100%')
.height(50)
.padding({ left: 16, right: 16 })
.backgroundColor('#FFFFFF')
.borderRadius(8)
.translate({ x: 0, y: 0 })
.opacity(1)
.animation({
duration: 300,
delay: index * 100, // 延迟动画,形成波浪效果
curve: Curve.EaseOut
})
}, (item: number) => item.toString())
}
Button('刷新列表')
.onClick(() => {
animateTo({
duration: 500,
curve: Curve.EaseInOut
}, () => {
// 触发列表重新渲染
this.items = [...this.items]
})
})
}
.padding(16)
.width('100%')
.height('100%')
}
}
4.3 组合动画示例
@Entry
@Component
struct CombinedAnimationDemo {
@State positionX: number = 0
@State positionY: number = 0
@State scale: number = 1
@State rotation: number = 0
@State isAnimating: boolean = false
build() {
Column({ space: 20 }) {
// 动画演示区域
Stack() {
Circle()
.width(60)
.height(60)
.fill('#007DFF')
.translate({ x: this.positionX, y: this.positionY })
.scale({ x: this.scale, y: this.scale })
.rotate({ angle: this.rotation })
.animation({
duration: 1000,
curve: Curve.EaseInOut
})
}
.width('100%')
.height(200)
.backgroundColor('#F0F0F0')
.borderRadius(16)
// 控制按钮
Row({ space: 12 }) {
Button(this.isAnimating ? '停止' : '开始')
.onClick(() => {
this.isAnimating = !this.isAnimating
if (this.isAnimating) {
this.startAnimation()
}
})
Button('重置')
.onClick(() => {
this.isAnimating = false
animateTo({
duration: 500,
curve: Curve.EaseOut
}, () => {
this.positionX = 0
this.positionY = 0
this.scale = 1
this.rotation = 0
})
})
}
}
.padding(16)
}
private startAnimation() {
animateTo({
duration: 2000,
curve: Curve.EaseInOut,
iterations: -1, // 无限循环
playMode: PlayMode.Alternate,
onFinish: () => {
if (this.isAnimating) {
this.startAnimation()
}
}
}, () => {
this.positionX = 100
this.positionY = 50
this.scale = 1.5
this.rotation = 360
})
}
}
五、转场动画
转场动画用于页面切换或组件的显示/隐藏过程中的过渡效果。
5.1 页面转场动画
@Entry
@Component
struct PageTransitionDemo {
build() {
Column() {
Text('页面转场动画演示')
.fontSize(24)
Button('跳转到详情页')
.onClick(() => {
router.pushUrl({
url: 'pages/DetailPage'
})
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
// 页面入场转场动画
.pageTransitionEnter({
type: TransitionType.Push,
duration: 500,
curve: Curve.EaseInOut
})
// 页面退场转场动画
.pageTransitionExit({
type: TransitionType.Pop,
duration: 300,
curve: Curve.EaseOut
})
}
}
5.2 组件转场动画
@Entry
@Component
struct ComponentTransitionDemo {
@State show: boolean = false
build() {
Column({ space: 20 }) {
Text('组件转场动画演示')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Button(this.show ? '隐藏组件' : '显示组件')
.onClick(() => {
this.show = !this.show
})
if (this.show) {
Column() {
Text('这是一个带转场动画的组件')
.fontSize(16)
}
.width('80%')
.height(150)
.backgroundColor('#007DFF')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
// 组件入场转场动画
.transition({
type: TransitionType.Insert,
opacity: 0,
translate: { x: 0, y: -50 },
scale: { x: 0.8, y: 0.8 }
})
// 组件退场转场动画
.transition({
type: TransitionType.Delete,
opacity: 0,
translate: { x: 0, y: 50 },
scale: { x: 0.8, y: 0.8 }
})
}
}
.padding(16)
.width('100%')
.height('100%')
}
}
5.3 自定义转场动画
@Entry
@Component
struct CustomTransitionDemo {
@State currentIndex: number = 0
private items: string[] = ['页面1', '页面2', '页面3']
build() {
Column({ space: 20 }) {
// 导航按钮
Row({ space: 20 }) {
Button('上一页')
.onClick(() => {
if (this.currentIndex > 0) {
this.currentIndex--
}
})
Text(`${this.currentIndex + 1} / ${this.items.length}`)
.fontSize(18)
Button('下一页')
.onClick(() => {
if (this.currentIndex < this.items.length - 1) {
this.currentIndex++
}
})
}
// 内容区域 - 使用自定义转场
Stack() {
ForEach(this.items, (item: string, index: number) => {
if (index === this.currentIndex) {
Column() {
Text(item)
.fontSize(24)
.fontColor('#FFFFFF')
}
.width('100%')
.height(200)
.backgroundColor(this.getColor(index))
.borderRadius(16)
.justifyContent(FlexAlign.Center)
// 自定义入场动画
.transition({
type: TransitionType.Insert,
opacity: 0,
translate: { x: 300 },
curve: Curve.EaseOut
})
// 自定义退场动画
.transition({
type: TransitionType.Delete,
opacity: 0,
translate: { x: -300 },
curve: Curve.EaseIn
})
}
}, (item: string) => item)
}
.width('100%')
.clip(true)
}
.padding(16)
}
private getColor(index: number): ResourceColor {
const colors: ResourceColor[] = ['#007DFF', '#FF6B6B', '#4CAF50']
return colors[index % colors.length]
}
}
六、路径动画
路径动画可以让组件沿着指定的路径运动,常用于制作复杂的动画效果。
6.1 基本语法
Component()
.motionPath({
path: 'MstartX startY LendX endY', // SVG路径
from: 0, // 起始位置(0-1)
to: 1, // 结束位置(0-1)
rotatable: true // 是否跟随路径旋转
})
6.2 完整示例:圆形路径动画
@Entry
@Component
struct MotionPathDemo {
@State progress: number = 0
@State isRunning: boolean = false
private timer: number = 0
build() {
Column({ space: 20 }) {
Text('路径动画演示')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 圆形路径动画
Stack({ alignContent: Alignment.Center }) {
// 圆形轨道
Circle()
.width(200)
.height(200)
.stroke('#E0E0E0')
.strokeWidth(2)
.fill('transparent')
// 运动的小球
Circle()
.width(30)
.height(30)
.fill('#007DFF')
.motionPath({
path: 'M100,0 A100,100 0 1,1 100,200 A100,100 0 1,1 100,0',
from: 0,
to: 1,
rotatable: true
})
.animation({
duration: 3000,
iterations: -1,
curve: Curve.Linear
})
}
.width(250)
.height(250)
// 贝塞尔曲线路径动画
Stack() {
// 路径轨迹(可视化)
Path()
.width('100%')
.height(150)
.commands('M50,75 C150,0 250,150 350,75')
.stroke('#E0E0E0')
.strokeWidth(2)
.fill('transparent')
// 运动的元素
Circle()
.width(20)
.height(20)
.fill('#FF6B6B')
.motionPath({
path: 'M50,75 C150,0 250,150 350,75',
from: 0,
to: 1,
rotatable: false
})
.animation({
duration: 2000,
iterations: -1,
playMode: PlayMode.Alternate,
curve: Curve.EaseInOut
})
}
.width('100%')
.height(150)
// 自定义路径动画
Column() {
Text('自定义路径动画')
.fontSize(16)
.margin({ bottom: 10 })
Stack() {
// 路径轨迹
Path()
.width('100%')
.height(100)
.commands('M20,50 Q100,0 180,50 Q260,100 340,50')
.stroke('#CCCCCC')
.strokeWidth(1)
.strokeDashArray([5, 5])
.fill('transparent')
// 运动的飞机图标
Text('✈️')
.fontSize(24)
.motionPath({
path: 'M20,50 Q100,0 180,50 Q260,100 340,50',
from: 0,
to: 1,
rotatable: true
})
.animation({
duration: 3000,
iterations: -1,
curve: Curve.Linear
})
}
.width('100%')
.height(100)
}
}
.padding(16)
.width('100%')
.height('100%')
}
}
6.3 多路径动画组合
@Entry
@Component
struct MultiPathDemo {
@State isAnimating: boolean = false
build() {
Column({ space: 30 }) {
Text('多路径动画组合')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Stack() {
// 多个小球沿不同路径运动
ForEach([0, 1, 2], (index: number) => {
Circle()
.width(20)
.height(20)
.fill(this.getBallColor(index))
.motionPath({
path: this.getPath(index),
from: 0,
to: 1,
rotatable: false
})
.animation({
duration: 2000 + index * 500,
iterations: -1,
playMode: PlayMode.Alternate,
curve: Curve.EaseInOut
})
}, (index: number) => index.toString())
}
.width('100%')
.height(200)
.backgroundColor('#F5F5F5')
.borderRadius(16)
Button(this.isAnimating ? '停止动画' : '开始动画')
.onClick(() => {
this.isAnimating = !this.isAnimating
})
}
.padding(16)
.width('100%')
.height('100%')
}
private getPath(index: number): string {
const paths: string[] = [
'M30,100 C100,20 200,180 270,100',
'M30,100 C100,180 200,20 270,100',
'M30,100 Q150,30 270,100'
]
return paths[index]
}
private getBallColor(index: number): ResourceColor {
const colors: ResourceColor[] = ['#007DFF', '#FF6B6B', '#4CAF50']
return colors[index]
}
}
七、动画性能优化
在开发动画时,性能优化是非常重要的。以下是一些最佳实践:
7.1 避免过度动画
// ❌ 错误示例:同时执行过多动画
animateTo({ duration: 300 }, () => {
this.width = 200
this.height = 200
this.opacity = 0.5
this.translate = { x: 100, y: 100 }
this.rotate = 45
this.scale = { x: 1.5, y: 1.5 }
this.backgroundColor = '#FF0000'
// ... 更多属性变化
})
// ✅ 正确示例:精简动画属性
animateTo({ duration: 300 }, () => {
this.width = 200
this.height = 200
})
7.2 使用硬件加速属性
// ✅ 推荐:使用这些属性可以获得硬件加速
Component()
.translate({ x: 100, y: 100 }) // 位移
.scale({ x: 1.5, y: 1.5 }) // 缩放
.rotate({ angle: 45 }) // 旋转
.opacity(0.5) // 透明度
// ⚠️ 谨慎使用:这些属性动画性能开销较大
Component()
.width(200) // 尺寸变化可能触发重绘
.height(200)
.backgroundColor('#FF0000') // 背景色变化
7.3 合理设置动画参数
// ✅ 合理的动画时长
animateTo({
duration: 300, // 推荐300ms左右
curve: Curve.EaseInOut, // 使用合适的曲线
}, () => {
// 属性变化
})
// ❌ 不推荐:动画时间过长
animateTo({
duration: 3000, // 太长的动画会让用户感到厌烦
}, () => {
// 属性变化
})
7.4 避免动画冲突
// ❌ 错误示例:多个动画同时作用于同一组件
Button('动画')
.onClick(() => {
animateTo({ duration: 300 }, () => {
this.width = 200
})
animateTo({ duration: 500 }, () => {
this.width = 300 // 与上面的动画冲突
})
})
// ✅ 正确示例:使用单一动画控制
Button('动画')
.onClick(() => {
animateTo({
duration: 500,
curve: Curve.EaseInOut
}, () => {
this.width = this.width === 100 ? 300 : 100
})
})
7.5 性能优化清单
|
优化项 |
建议 |
说明 |
|---|---|---|
|
动画时长 |
200-500ms |
太短看不清,太长影响体验 |
|
帧率 |
60fps |
保证动画流畅 |
|
动画属性 |
使用位移/缩放/旋转/透明度 |
这些属性可以硬件加速 |
|
避免重绘 |
减少width/height动画 |
尺寸变化会触发重绘 |
|
减少层数 |
避免过深的组件嵌套 |
减少渲染负担 |
7.6 动画调试技巧
// 开启动画调试
animateTo({
duration: 1000,
curve: Curve.EaseInOut,
// 添加动画开始和结束回调,用于调试
onFinish: () => {
console.info('动画完成')
}
}, () => {
this.width = 200
})
八、总结与最佳实践
8.1 动画类型选择指南
|
场景 |
推荐动画类型 |
原因 |
|---|---|---|
|
属性值变化 |
animateTo |
简单易用,自动插值 |
|
组件显示/隐藏 |
transition |
专门用于转场场景 |
|
页面切换 |
pageTransition |
提供完整的页面转场 |
|
复杂路径运动 |
motionPath |
支持SVG路径描述 |
|
需要精确控制 |
animation |
可单独配置每个组件 |
8.2 最佳实践总结
-
保持简洁:动画应该是微妙的,不要过度使用
-
保持一致:整个App的动画风格应该统一
-
性能优先:优先使用硬件加速属性
-
响应迅速:动画时长控制在300ms左右
-
有意义:动画应该服务于功能,而非纯粹装饰
8.3 常见问题解答
Q: 动画卡顿怎么办? A: 检查是否使用了重绘属性,尝试使用位移/缩放/旋转/透明度等硬件加速属性。
Q: 如何实现循环动画? A: 设置 iterations: -1 即可实现无限循环。
Q: 如何实现反向动画? A: 使用 playMode: PlayMode.Alternate 可以实现正向-反向交替播放。
九、系列文章推荐
标签:鸿蒙动画, ArkUI动画, animateTo, 转场动画, 路径动画, 显式动画, 动画性能优化
版权声明:本文为原创技术文章,转载请注明出处。
系列信息:鸿蒙NEXT开发实战系列 - 专注于实战的鸿蒙开发教程
更多推荐




所有评论(0)