第十章:动画
本章配套代码:
code/Chapter10(隐式动画 Demo) 前置要求:已完成基础组件与状态管理章节
10.1 学习目标
- 掌握隐式动画(
.animation()) - 掌握显式动画(
animateTo) - 掌握转场动画(
.transition()) - 掌握动画参数(时长、曲线、循环)
10.2 动画概述
动画通过属性变化产生平滑过渡,提升体验。
┌──────────────────────────────────────┐
│ 三类动画 │
│ │
│ 隐式动画:属性变化自动补间 │
│ `.animation({ duration, curve })` │
│ │
│ 显式动画:手动控制动画过程 │
│ `animateTo(params, () => 改属性)` │
│ │
│ 转场动画:组件出现/消失时 │
│ `.transition({ type, opacity })` │
└─────────────────────────────── ───────┘
| 类型 | 触发方式 | 适用 |
|---|---|---|
| 隐式动画 | 状态变化自动 | 简单属性过渡 |
| 显式动画 | 手动调用 | 精确控制 |
| 转场动画 | 组件挂载/卸载 | 页面/组件切换 |
10.3 隐式动画
10.3.1 基本用法
给组件添加 .animation(),属性变化时自动动画过渡:
@Entry
@ComponentV2
struct ImplicitAnimation {
@Local isExpanded: boolean = false
build() {
Column() {
Text('点击我')
.fontSize(20)
.fontColor(Color.White)
.width(this.isExpanded ? 300 : 100) // 属性随状态变化
.height(this.isExpanded ? 100 : 50)
.backgroundColor(this.isExpanded ? '#007DFF' : '#999999')
.borderRadius(this.isExpanded ? 20 : 5)
.animation({ // 隐式动画
duration: 500,
curve: Curve.EaseInOut
})
.onClick(() => {
this.isExpanded = !this.isExpanded
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
原理:
isExpanded 变化
→ width/height/color/borderRadius 同时变化
→ .animation() 自动对变化的属性做补间动画
10.3.2 动画属性
.animation({
duration: 1000, // 时长(毫秒)
curve: Curve.EaseInOut, // 动画曲线
delay: 100, // 延迟启动
iterations: -1, // 循环次数(-1 无限)
playMode: PlayMode.Normal // 播放模式
})
10.3.3 动画曲线
| 曲线 | 说明 |
|---|---|
Curve.Linear | 匀速线性 |
Curve.EaseIn | 加速(慢开始) |
Curve.EaseOut | 减速(慢结束) |
Curve.EaseInOut | 先加速后减速(推荐) |
Curve.Friction | 摩擦效果 |
Curve.Spring | 弹性效果 |
💡 交互式 UI 动画推荐
Curve.EaseInOut200-500ms,兼顾自然与流畅。
10.4 显式动画(animateTo)
用 animateTo 包裹状态修改,精确控制动画过程:
@Entry
@ComponentV2
struct ExplicitAnimation {
@Local width: number = 100
@Local height: number = 100
@Local rotateAngle: number = 0
build() {
Column({ space: 20 }) {
Rectangle()
.width(this.width)
.height(this.height)
.fill(Color.Blue)
.rotate({ angle: this.rotateAngle })
Button('执行动画')
.onClick(() => {
this.getUIContext().animateTo({
duration: 1000,
curve: Curve.EaseInOut
}, () => {
this.width = this.width === 100 ? 200 : 100 // 在回调内修改状态
this.height = this.height === 100 ? 200 : 100
this.rotateAngle += 90
})
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
语法:
this.getUIContext().animateTo({
duration: 1000,
curve: Curve.EaseInOut,
delay: 0,
iterations: 1,
playMode: PlayMode.Normal
}, () => {
// 在闭包内修改状态,属性变化产生动画
this.width = 200
})
🔑 核心区别:隐式动画在属性上声明
.animation();显式动画在修改处用animateTo包裹。显式动画更灵活(可控制多组件同时动画)。
10.5 转场动画
组件出现/消失时播放过渡动画:
@Entry
@ComponentV2
struct TransitionExample {
@Local showBox: boolean = false
build() {
Column({ space: 20 }) {
Button('切换显示')
.onClick(() => {
this.showBox = !this.showBox
})
if (this.showBox) {
Stack() {
Text('动画盒子')
.fontSize(18)
.fontColor(Color.White)
}
.width(200)
.height(200)
.backgroundColor(Color.Blue)
.borderRadius(12)
.transition({ // 插入动画
type: TransitionType.Insert,
opacity: 0,
scale: { x: 0.5, y: 0.5 }
})
.transition({ // 删除动画
type: TransitionType.Delete,
opacity: 0,
scale: { x: 0.5, y: 0.5 }
})
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
| TransitionType | 时机 |
|---|---|
Insert | 组件创建/出现 |
Delete | 组件销毁/消失 |
All | 两者都有 |
💡 转场动画常用于
if条件渲染的显示/隐藏。
10.6 综合示例:隐式动画三连
对应 code/Chapter10 的 Index.ets,演示缩放/旋转/透明度三种隐式动画:
@Entry
@ComponentV2
struct Index {
@Local scaleValue: number = 1
@Local rotateValue: number = 0
@Local opacityValue: number = 1
build() {
Column({ space: 20 }) {
Text('Chapter 10: Animation')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// ① 缩放动画
Column({ space: 8 }) {
Text('Scale Animation')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('')
.width(80).height(80)
.backgroundColor('#007DFF')
.borderRadius(8)
.scale({ x: this.scaleValue, y: this.scaleValue })
.animation({ duration: 300, curve: Curve.EaseInOut })
.onClick(() => {
this.scaleValue = this.scaleValue === 1 ? 1.5 : 1
})
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
// ② 旋转动画
Column({ space: 8 }) {
Text('Rotate Animation')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Image($r('app.media.avatar'))
.width(60).height(60)
.borderRadius(30)
.clip(true)
.rotate({ angle: this.rotateValue })
.animation({ duration: 500, curve: Curve.EaseInOut })
.onClick(() => {
this.rotateValue += 90
})
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
// ③ 透明度动画
Column({ space: 8 }) {
Text('Opacity Animation')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('人生若只如初见')
.fontSize(18)
.opacity(this.opacityValue)
.animation({ duration: 600 })
.onClick(() => {
this.opacityValue = this.opacityValue === 1 ? 0.3 : 1
})
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#f5f5f5')
}
}
运行效果:
- 蓝色方块点击:1.0 → 1.5 → 1.0 缩放
- 头像点击:每次旋转 90°
- 文本点击:透明度 1.0 ↔ 0.3
- 均为 300-600ms EaseInOut 曲线,过渡平滑
运行效果截图:




10.7 加载动画(进阶)
无限旋转的加载动画(显式动画 + 无限循环):
@Entry
@ComponentV2
struct LoadingAnimation {
@Local rotation: number = 0
aboutToAppear(): void {
this.getUIContext().animateTo({
duration: 1000,
curve: Curve.Linear,
iterations: -1 // 无限循环
}, () => {
this.rotation = 360
})
}
build() {
Column({ space: 20 }) {
Circle()
.width(60).height(60)
.stroke(Color.Blue)
.strokeWidth(4)
.strokeDashArray([15, 8])
.rotate({ angle: this.rotation })
Text('加载中...')
.fontSize(16)
.fontColor('#666666')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
10.8 性能建议
| 建议 | 原因 |
|---|---|
用 transform(scale/rotate/opacity) | GPU 合成,避免布局重排 |
| 避免动画布局属性(width/height/position) | 触发重排,卡顿 |
| 时长控制在 200-500ms | 过快生硬、过慢拖沓 |
| 大量动画用显式动画统一管理 | 可控、可打断 |
10.9 常见问题
Q:动画不流畅怎么办?
优先动画 transform 类属性;避免在动画中改 width/height/layoutWeight;减少同屏动画数量。
Q:如何实现弹性动画?
用 Curve.Spring 或自定义贝塞尔曲线(Curve.cubicBezier(x1,y1,x2,y2))。
Q:.animation() 和 animateTo 怎么选?
单个组件简单过渡用 .animation();多组件联动/精细控制用 animateTo。
Q:动画点击无反应?
检查 .animation() 是否作用在实际变化的属性对应的组件上;确认状态已改变。
10.10 本章小结
| 知识点 | 说明 |
|---|---|
| 隐式动画 | .animation() 属性自动补间 |
| 显式动画 | animateTo 手动控制 |
| 转场动画 | .transition() 出现/消失 |
| 动画曲线 | EaseInOut 最常用 |
| 性能 | transform 优先 |
10.11 课后练习
- 实现一个进度条动画(宽度从 0 增长到 100%)
- 设计按钮按压反馈(按下缩小 0.95,抬起还原)
- 实现页面切换的淡入淡出/左右滑入转场
- 做一个无限旋转的加载图标
- 将练习整合进
Chapter10/Index.ets