跳到主要内容

第十章:动画

本章配套代码: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.EaseInOut 200-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/Chapter10Index.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 曲线,过渡平滑

运行效果截图:

Chapter10 动画总览

旋转动画(头像)

缩放动画(蓝色方块)

透明度动画(文本)

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 课后练习

  1. 实现一个进度条动画(宽度从 0 增长到 100%)
  2. 设计按钮按压反馈(按下缩小 0.95,抬起还原)
  3. 实现页面切换的淡入淡出/左右滑入转场
  4. 做一个无限旋转的加载图标
  5. 将练习整合进 Chapter10/Index.ets

10.12 参考资料

评论

加载中…
加载中...