HarmonyOS开发:SVG动画与SMIL动画
HarmonyOS开发:SVG动画与SMIL动画
核心要点
- SVG动画分为SMIL声明式动画和JavaScript驱动动画两种方式
- HarmonyOS通过ArkUI的Canvas组件渲染SVG,支持animate、animateTransform等SMIL元素
- 动画性能优化需关注硬件加速、帧率控制和内存管理
- 实际开发中常结合属性动画和路径动画实现复杂视觉效果
一、背景与动机
1.1 SVG动画的应用场景
在现代移动应用开发中,矢量图形动画因其体积小、缩放不失真、性能优异等特点,已成为UI设计的重要组成部分。SVG动画广泛应用于:
- 启动动画:Logo动画、品牌展示动画
- 交互反馈:按钮点击波纹、加载进度动画
- 数据可视化:图表动画、流程图动画
- 图标动画:菜单展开、状态切换动画
1.2 HarmonyOS SVG动画的技术演进
HarmonyOS从API Version 7开始支持SVG渲染,随着ArkUI框架的成熟,SVG动画能力不断增强:
graph TB
A[SVG动画技术栈] --> B[SMIL声明式动画]
A --> C[JavaScript驱动动画]
A --> D[ArkUI属性动画]
B --> B1[animate元素]
B --> B2[animateTransform]
B --> B3[animateMotion]
B --> B4[set元素]
C --> C1[requestAnimationFrame]
C --> C2[Canvas 2D API]
C --> C3[路径插值计算]
D --> D1[AnimatorOptions]
D --> D2[animateTo]
D --> D3[UIContext动画]
classDef primary fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,color:#fff
classDef secondary fill:#67C23A,stroke:#4A9E2A,stroke-width:2px,color:#fff
classDef tertiary fill:#E6A23C,stroke:#B8820C,stroke-width:2px,color:#fff
class A primary
class B,C,D secondary
class B1,B2,B3,B4,C1,C2,C3,D1,D2,D3 tertiary
1.3 为什么选择SVG动画
相比传统帧动画和Lottie动画,SVG动画具有独特优势:
| 特性 | SVG动画 | 帧动画 | Lottie动画 |
|---|---|---|---|
| 文件体积 | 极小(KB级) | 大(MB级) | 中等 |
| 缩放质量 | 完美 | 失真 | 完美 |
| 运行时修改 | 支持 | 不支持 | 有限支持 |
| 学习成本 | 中等 | 低 | 高 |
| 性能开销 | 低 | 中 | 中 |
二、核心原理
2.1 SMIL动画原理
SMIL(Synchronized Multimedia Integration Language)是W3C制定的同步多媒体集成语言,SVG内置了SMIL动画子集。
2.1.1 动画时间模型
SMIL动画基于时间线模型,包含以下核心概念:
graph LR
A[动画生命周期] --> B[开始时间<br/>begin]
A --> C[持续时间<br/>dur]
A --> D[结束时间<br/>end]
A --> E[重复次数<br/>repeatCount]
B --> F[激活区间]
C --> F
D --> F
F --> G[简单持续时间]
G --> H[插值计算]
H --> I[动画值应用]
classDef timeNode fill:#409EFF,stroke:#3A7BD5,stroke-width:2px,color:#fff
classDef processNode fill:#67C23A,stroke:#4A9E2A,stroke-width:2px,color:#fff
class A timeNode
class B,C,D,E timeNode
class F,G,H,I processNode
2.1.2 属性插值算法
动画值通过插值函数计算:
f(t) = from + (to - from) × calcMode(t/dur)
其中calcMode支持四种模式:
- linear:线性插值
- discrete:离散插值(无过渡)
- paced:匀速插值(适用于路径动画)
- spline:贝塞尔曲线插值
2.2 HarmonyOS SVG渲染架构
graph TB
subgraph 应用层
A[ArkTS组件]
B[SVG字符串/资源]
end
subgraph ArkUI框架层
C[Canvas组件]
D[SVG解析器]
E[动画控制器]
end
subgraph 渲染引擎层
F[Skia引擎]
G[GPU加速]
end
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
classDef appLayer fill:#E8F4FD,stroke:#4A90E2,stroke-width:2px
classDef frameworkLayer fill:#F0F9EB,stroke:#67C23A,stroke-width:2px
classDef engineLayer fill:#FDF6EC,stroke:#E6A23C,stroke-width:2px
class A,B appLayer
class C,D,E frameworkLayer
class F,G engineLayer
2.3 动画性能考量
SVG动画性能受以下因素影响:
- 重绘区域:动画元素越多,重绘开销越大
- 复杂度:路径节点数、滤镜效果数量
- 硬件加速:GPU加速可显著提升性能
- 帧率控制:60fps vs 30fps的CPU占用差异
三、代码实战
3.1 基础SMIL动画实现
以下示例展示如何在HarmonyOS中实现基础SVG动画:
// SvgAnimationDemo.ets
@Component
export struct SvgAnimationDemo {
@State animationState: string = 'running'
@State progressValue: number = 0
// 基础属性动画SVG
private basicAnimationSvg: string = `
<svg width="200" height="200" viewBox="0 0 200 200">
<!-- 圆形缩放动画 -->
<circle cx="100" cy="100" r="20" fill="#4A90E2">
<animate
attributeName="r"
from="20"
to="60"
dur="2s"
repeatCount="indefinite"
calcMode="spline"
keySplines="0.4 0 0.2 1; 0.4 0 0.2 1"
keyTimes="0; 0.5; 1"
values="20; 60; 20"
/>
</circle>
<!-- 颜色渐变动画 -->
<rect x="50" y="150" width="100" height="30" rx="5">
<animate
attributeName="fill"
values="#67C23A;#E6A23C;#F56C6C;#67C23A"
dur="3s"
repeatCount="indefinite"
/>
</rect>
</svg>
`
build() {
Column({ space: 20 }) {
Text('SVG基础动画')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// Canvas渲染SVG
Canvas(this.context)
.width(200)
.height(200)
.onReady(() => {
this.renderSvg(this.basicAnimationSvg)
})
// 动画控制按钮
Row({ space: 15 }) {
Button('播放')
.onClick(() => this.animationState = 'running')
Button('暂停')
.onClick(() => this.animationState = 'paused')
Button('重置')
.onClick(() => this.resetAnimation())
}
}
.padding(20)
}
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(new Settings())
private renderSvg(svgString: string): void {
// 使用ArkUI的SVG解析能力
const svg = new Svg(svgString)
this.context.drawImage(svg, 0, 0)
}
private resetAnimation(): void {
this.progressValue = 0
this.animationState = 'running'
}
}
3.2 路径动画与变形动画
路径动画(animateMotion)和变形动画(animateTransform)是SVG动画的核心能力:
// PathAnimationDemo.ets
@Component
export struct PathAnimationDemo {
@State isAnimating: boolean = true
// 路径动画SVG - 粒子沿路径运动
private motionPathSvg: string = `
<svg width="300" height="300" viewBox="0 0 300 300">
<!-- 定义运动路径 -->
<path
id="motionPath"
d="M 50,150 C 100,50 200,250 250,150"
fill="none"
stroke="#E6E6E6"
stroke-width="2"
stroke-dasharray="5,5"
/>
<!-- 运动的粒子 -->
<circle r="10" fill="#4A90E2">
<animateMotion
dur="4s"
repeatCount="indefinite"
rotate="auto"
>
<mpath href="#motionPath"/>
</animateMotion>
</circle>
<!-- 带轨迹的粒子 -->
<circle r="8" fill="#67C23A">
<animateMotion
dur="4s"
repeatCount="indefinite"
rotate="auto-reverse"
keyPoints="0;0.5;1"
keyTimes="0;0.5;1"
calcMode="spline"
keySplines="0.5 0 0.5 1; 0.5 0 0.5 1"
>
<mpath href="#motionPath"/>
</animateMotion>
</circle>
</svg>
`
// 变形动画SVG - 旋转与缩放组合
private transformAnimationSvg: string = `
<svg width="200" height="200" viewBox="0 0 200 200">
<g transform="translate(100, 100)">
<!-- 旋转动画 -->
<rect x="-40" y="-40" width="80" height="80" fill="#E6A23C" opacity="0.8">
<animateTransform
attributeName="transform"
type="rotate"
from="0"
to="360"
dur="6s"
repeatCount="indefinite"
/>
</rect>
<!-- 缩放动画 -->
<circle r="30" fill="#4A90E2" opacity="0.6">
<animateTransform
attributeName="transform"
type="scale"
values="1;1.5;1"
dur="3s"
repeatCount="indefinite"
additive="sum"
/>
</circle>
<!-- 倾斜动画 -->
<polygon points="0,-20 20,20 -20,20" fill="#67C23A" opacity="0.7">
<animateTransform
attributeName="transform"
type="skewX"
values="0;30;0;-30;0"
dur="4s"
repeatCount="indefinite"
/>
</polygon>
</g>
</svg>
`
build() {
Column({ space: 30 }) {
Text('路径动画与变形动画')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 路径动画展示
Column() {
Text('路径动画')
.fontSize(16)
.fontColor('#666666')
Canvas(this.context1)
.width(300)
.height(300)
.onReady(() => {
this.renderSvg(this.context1, this.motionPathSvg)
})
}
// 变形动画展示
Column() {
Text('变形动画')
.fontSize(16)
.fontColor('#666666')
Canvas(this.context2)
.width(200)
.height(200)
.onReady(() => {
this.renderSvg(this.context2, this.transformAnimationSvg)
})
}
// 控制开关
Toggle({ type: ToggleType.Switch, isOn: this.isAnimating })
.onChange((isOn: boolean) => {
this.isAnimating = isOn
})
}
.padding(20)
}
private context1: CanvasRenderingContext2D = new CanvasRenderingContext2D(new Settings())
private context2: CanvasRenderingContext2D = new CanvasRenderingContext2D(new Settings())
private renderSvg(context: CanvasRenderingContext2D, svgString: string): void {
const svg = new Svg(svgString)
context.drawImage(svg, 0, 0)
}
}
3.3 JavaScript驱动的高性能动画
对于复杂交互场景,使用JavaScript驱动动画可获得更精细的控制:
// JsDrivenAnimationDemo.ets
@Component
export struct JsDrivenAnimationDemo {
@State particles: ParticleData[] = []
@State animationFrameId: number = -1
private canvasWidth: number = 400
private canvasHeight: number = 400
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(new Settings())
aboutToAppear() {
this.initParticles()
this.startAnimation()
}
aboutToDisappear() {
this.stopAnimation()
}
// 初始化粒子系统
private initParticles(): void {
const particleCount = 50
for (let i = 0; i < particleCount; i++) {
this.particles.push({
x: Math.random() * this.canvasWidth,
y: Math.random() * this.canvasHeight,
radius: Math.random() * 8 + 2,
velocityX: (Math.random() - 0.5) * 2,
velocityY: (Math.random() - 0.5) * 2,
color: this.getRandomColor(),
alpha: Math.random() * 0.5 + 0.5
})
}
}
// 获取随机颜色
private getRandomColor(): string {
const colors = ['#4A90E2', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
return colors[Math.floor(Math.random() * colors.length)]
}
// 启动动画循环
private startAnimation(): void {
const animate = () => {
this.updateParticles()
this.renderParticles()
this.animationFrameId = requestAnimationFrame(animate)
}
animate()
}
// 停止动画
private stopAnimation(): void {
if (this.animationFrameId !== -1) {
cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = -1
}
}
// 更新粒子状态
private updateParticles(): void {
this.particles.forEach((particle: ParticleData) => {
// 更新位置
particle.x += particle.velocityX
particle.y += particle.velocityY
// 边界检测与反弹
if (particle.x < 0 || particle.x > this.canvasWidth) {
particle.velocityX *= -1
particle.x = Math.max(0, Math.min(this.canvasWidth, particle.x))
}
if (particle.y < 0 || particle.y > this.canvasHeight) {
particle.velocityY *= -1
particle.y = Math.max(0, Math.min(this.canvasHeight, particle.y))
}
// Alpha呼吸效果
particle.alpha = 0.5 + 0.5 * Math.sin(Date.now() / 1000 + particle.x)
})
}
// 渲染粒子
private renderParticles(): void {
// 清空画布
this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
// 绘制背景渐变
const gradient = this.context.createLinearGradient(0, 0, this.canvasWidth, this.canvasHeight)
gradient.addColorStop(0, '#1A1A2E')
gradient.addColorStop(1, '#16213E')
this.context.fillStyle = gradient
this.context.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
// 绘制粒子连线
this.drawConnections()
// 绘制粒子
this.particles.forEach((particle: ParticleData) => {
this.context.beginPath()
this.context.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2)
this.context.fillStyle = particle.color
this.context.globalAlpha = particle.alpha
this.context.fill()
this.context.globalAlpha = 1
})
}
// 绘制粒子间连线
private drawConnections(): void {
const maxDistance = 100
for (let i = 0; i < this.particles.length; i++) {
for (let j = i + 1; j < this.particles.length; j++) {
const dx = this.particles[i].x - this.particles[j].x
const dy = this.particles[i].y - this.particles[j].y
const distance = Math.sqrt(dx * dx + dy * dy)
if (distance < maxDistance) {
const alpha = 1 - distance / maxDistance
this.context.beginPath()
this.context.moveTo(this.particles[i].x, this.particles[i].y)
this.context.lineTo(this.particles[j].x, this.particles[j].y)
this.context.strokeStyle = '#4A90E2'
this.context.globalAlpha = alpha * 0.3
this.context.lineWidth = 1
this.context.stroke()
this.context.globalAlpha = 1
}
}
}
}
build() {
Column({ space: 20 }) {
Text('JavaScript驱动粒子动画')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Canvas(this.context)
.width(this.canvasWidth)
.height(this.canvasHeight)
.onReady(() => {
this.renderParticles()
})
Row({ space: 15 }) {
Button('重新生成')
.onClick(() => {
this.particles = []
this.initParticles()
})
Button('暂停/继续')
.onClick(() => {
if (this.animationFrameId === -1) {
this.startAnimation()
} else {
this.stopAnimation()
}
})
}
}
.padding(20)
}
}
// 粒子数据接口
interface ParticleData {
x: number
y: number
radius: number
velocityX: number
velocityY: number
color: string
alpha: number
}
3.4 组合动画与时间线控制
实现复杂动画序列需要精确的时间线控制:
// TimelineAnimationDemo.ets
@Component
export struct TimelineAnimationDemo {
@State currentPhase: number = 0
@State phaseNames: string[] = ['初始化', '加载中', '处理数据', '完成']
// 时间线动画配置
private timelineConfig: AnimationPhase[] = [
{ duration: 500, easing: 'ease-out', target: 'logo', property: 'scale', from: 0, to: 1 },
{ duration: 800, easing: 'linear', target: 'progress', property: 'width', from: 0, to: 100 },
{ duration: 600, easing: 'ease-in-out', target: 'circle', property: 'rotation', from: 0, to: 360 },
{ duration: 400, easing: 'ease-out', target: 'text', property: 'opacity', from: 0, to: 1 }
]
// 组合动画SVG
private compositeAnimationSvg: string = `
<svg width="300" height="300" viewBox="0 0 300 300">
<defs>
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4A90E2">
<animate attributeName="stop-color" values="#4A90E2;#67C23A;#E6A23C;#4A90E2" dur="4s" repeatCount="indefinite"/>
</stop>
<stop offset="100%" style="stop-color:#67C23A">
<animate attributeName="stop-color" values="#67C23A;#E6A23C;#4A90E2;#67C23A" dur="4s" repeatCount="indefinite"/>
</stop>
</linearGradient>
</defs>
<!-- Logo动画 -->
<g id="logo" transform="translate(150, 100)">
<polygon points="0,-40 40,20 -40,20" fill="url(#logoGradient)">
<animateTransform attributeName="transform" type="scale" values="0;1;1" dur="0.5s" fill="freeze" begin="0s"/>
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="3s" repeatCount="indefinite" additive="sum"/>
</polygon>
</g>
<!-- 进度条动画 -->
<g transform="translate(50, 180)">
<rect width="200" height="8" rx="4" fill="#E6E6E6"/>
<rect width="0" height="8" rx="4" fill="#4A90E2">
<animate attributeName="width" from="0" to="200" dur="2s" fill="freeze" begin="0.5s"/>
</rect>
</g>
<!-- 文字淡入 -->
<text x="150" y="220" text-anchor="middle" font-size="16" fill="#333333" opacity="0">
动画加载完成
<animate attributeName="opacity" from="0" to="1" dur="0.5s" fill="freeze" begin="2.5s"/>
</text>
</svg>
`
build() {
Column({ space: 20 }) {
Text('组合动画与时间线')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 当前阶段指示
Row({ space: 10 }) {
ForEach(this.phaseNames, (name: string, index: number) => {
Column() {
Circle()
.width(20)
.height(20)
.fill(this.currentPhase >= index ? '#4A90E2' : '#E6E6E6')
Text(name)
.fontSize(12)
.fontColor(this.currentPhase >= index ? '#333333' : '#999999')
}
})
}
// SVG动画展示
Canvas(this.context)
.width(300)
.height(300)
.onReady(() => {
this.renderSvg()
this.startTimeline()
})
// 手动控制
Row({ space: 15 }) {
Button('上一阶段')
.onClick(() => {
this.currentPhase = Math.max(0, this.currentPhase - 1)
})
Button('下一阶段')
.onClick(() => {
this.currentPhase = Math.min(this.phaseNames.length - 1, this.currentPhase + 1)
})
}
}
.padding(20)
}
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(new Settings())
private renderSvg(): void {
const svg = new Svg(this.compositeAnimationSvg)
this.context.drawImage(svg, 0, 0)
}
private startTimeline(): void {
// 模拟时间线进度
let phase = 0
const interval = setInterval(() => {
phase++
if (phase >= this.phaseNames.length) {
clearInterval(interval)
return
}
this.currentPhase = phase
}, 1000)
}
}
// 动画阶段配置接口
interface AnimationPhase {
duration: number
easing: string
target: string
property: string
from: number
to: number
}
四、踩坑与注意事项
4.1 常见问题与解决方案
graph TB
A[SVG动画常见问题] --> B[动画不生效]
A --> C[性能卡顿]
A --> D[内存泄漏]
A --> E[兼容性问题]
B --> B1[检查SVG语法]
B --> B2[确认属性名正确]
B --> B3[验证时间属性]
C --> C1[减少重绘区域]
C --> C2[启用GPU加速]
C --> C3[降低帧率要求]
D --> D1[及时释放资源]
D --> D2[取消动画帧]
D --> D3[清理事件监听]
E --> E1[降级处理方案]
E --> E2[特性检测]
E --> E3[多版本适配]
classDef problemNode fill:#F56C6C,stroke:#C45656,stroke-width:2px,color:#fff
classDef solutionNode fill:#67C23A,stroke:#4A9E2A,stroke-width:2px,color:#fff
classDef detailNode fill:#E6A23C,stroke:#B8820C,stroke-width:2px,color:#fff
class A problemNode
class B,C,D,E solutionNode
class B1,B2,B3,C1,C2,C3,D1,D2,D3,E1,E2,E3 detailNode
问题1:SMIL动画在Canvas中不生效
原因:ArkUI的Canvas组件对SMIL动画支持有限,部分动画属性无法自动执行。
解决方案:
// 方案一:使用属性动画替代SMIL
@Component
struct WorkaroundDemo1 {
@State scale: number = 1
build() {
Column() {
Circle()
.width(100)
.height(100)
.fill('#4A90E2')
.scale({ x: this.scale, y: this.scale })
.animation({
duration: 2000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
}
.onAppear(() => {
// 触发动画
this.scale = 1.5
})
}
}
// 方案二:使用AnimatorOptions精确控制
@Component
struct WorkaroundDemo2 {
private animatorOptions: AnimatorOptions = {
duration: 2000,
easing: 'ease-in-out',
fill: 'forwards',
iterations: -1,
begin: 0,
end: 100
}
private animator: AnimatorResult | null = null
build() {
Column() {
Circle()
.width(100)
.height(100)
.fill('#67C23A')
}
.onAppear(() => {
this.animator = Animator.create(this.animatorOptions)
this.animator.onFrame((progress: number) => {
// 根据progress更新UI
})
this.animator.play()
})
.onDisappear(() => {
this.animator?.cancel()
})
}
}
问题2:动画帧率不稳定
原因:JavaScript动画在主线程执行,UI更新会阻塞动画计算。
解决方案:
// 使用requestAnimationFrame + 时间差控制帧率
@Component
struct FrameRateControlDemo {
private lastFrameTime: number = 0
private targetFPS: number = 30
private frameInterval: number = 1000 / this.targetFPS
private controlledAnimation(): void {
const currentTime = performance.now()
const deltaTime = currentTime - this.lastFrameTime
if (deltaTime >= this.frameInterval) {
this.lastFrameTime = currentTime - (deltaTime % this.frameInterval)
// 执行动画更新
this.updateAnimation()
}
requestAnimationFrame(() => this.controlledAnimation())
}
private updateAnimation(): void {
// 动画逻辑
}
}
4.2 性能优化最佳实践
// PerformanceOptimizedAnimation.ets
@Component
export struct PerformanceOptimizedAnimation {
@State @Watch('onDataChange') dataPoints: number[] = []
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(new Settings())
private offscreenCanvas: OffscreenCanvas | null = null
private animationId: number = -1
// 性能优化配置
private performanceConfig = {
useOffscreen: true, // 使用离屏渲染
throttleFPS: 30, // 限制帧率
batchUpdates: true, // 批量更新
useWillChange: true // 提示浏览器优化
}
onDataChange(): void {
if (this.performanceConfig.batchUpdates) {
// 批量更新优化
this.batchRender()
} else {
this.render()
}
}
// 离屏渲染优化
private initOffscreenCanvas(): void {
if (this.performanceConfig.useOffscreen) {
this.offscreenCanvas = new OffscreenCanvas(400, 400)
// 在离屏Canvas上预渲染静态内容
this.preRenderStatic()
}
}
private preRenderStatic(): void {
if (!this.offscreenCanvas) return
const ctx = this.offscreenCanvas.getContext('2d')
// 绘制静态背景等
ctx.fillStyle = '#1A1A2E'
ctx.fillRect(0, 0, 400, 400)
}
// 批量渲染
private batchRender(): void {
// 收集所有更新,一次性渲染
const updates: RenderUpdate[] = []
this.dataPoints.forEach((point, index) => {
updates.push({
type: 'circle',
x: index * 10,
y: point,
radius: 5,
color: '#4A90E2'
})
})
this.applyBatchUpdates(updates)
}
private applyBatchUpdates(updates: RenderUpdate[]): void {
this.context.clearRect(0, 0, 400, 400)
// 先绘制离屏内容
if (this.offscreenCanvas) {
this.context.drawImage(this.offscreenCanvas, 0, 0)
}
// 批量绘制更新
updates.forEach((update: RenderUpdate) => {
this.context.beginPath()
this.context.arc(update.x, update.y, update.radius, 0, Math.PI * 2)
this.context.fillStyle = update.color
this.context.fill()
})
}
private render(): void {
// 常规渲染逻辑
}
build() {
Column() {
Canvas(this.context)
.width(400)
.height(400)
.onReady(() => {
this.initOffscreenCanvas()
})
}
}
}
interface RenderUpdate {
type: string
x: number
y: number
radius: number
color: string
}
4.3 内存管理注意事项
// MemoryManagedAnimation.ets
@Component
export struct MemoryManagedAnimation {
private animationController: AnimationController | null = null
private eventListeners: Map<string, Function> = new Map()
private resources: Resource[] = []
aboutToAppear() {
this.initializeAnimation()
}
aboutToDisappear() {
this.cleanup()
}
private initializeAnimation(): void {
// 创建动画控制器
this.animationController = new AnimationController()
// 注册事件监听(保存引用以便清理)
const resizeHandler = (width: number, height: number) => {
this.handleResize(width, height)
}
this.eventListeners.set('resize', resizeHandler)
window.addEventListener('resize', resizeHandler)
}
private cleanup(): void {
// 1. 停止并释放动画控制器
if (this.animationController) {
this.animationController.stop()
this.animationController = null
}
// 2. 移除所有事件监听
this.eventListeners.forEach((handler, event) => {
window.removeEventListener(event, handler as EventListener)
})
this.eventListeners.clear()
// 3. 释放资源
this.resources.forEach((resource) => {
resource.dispose()
})
this.resources = []
}
private handleResize(width: number, height: number): void {
// 处理尺寸变化
}
}
class AnimationController {
private isRunning: boolean = false
start(): void {
this.isRunning = true
}
stop(): void {
this.isRunning = false
}
}
interface Resource {
dispose(): void
}
五、总结
5.1 技术选型指南
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单属性动画 | SMIL animate | 声明式、代码简洁 |
| 复杂路径动画 | JavaScript + Canvas | 灵活可控、性能好 |
| UI交互动画 | ArkUI属性动画 | 与框架集成度高 |
| 数据可视化动画 | requestAnimationFrame | 精确控制、帧同步 |
| 大量粒子动画 | 离屏Canvas + WebWorker | 避免主线程阻塞 |
5.2 核心要点回顾
- SMIL动画:适合简单声明式动画,但HarmonyOS支持有限,需注意兼容性
- JavaScript驱动动画:灵活度高,适合复杂交互场景,需注意性能优化
- 性能优化:离屏渲染、帧率控制、批量更新是三大核心手段
- 内存管理:及时释放资源、取消动画帧、清理事件监听防止内存泄漏
- 时间线控制:复杂动画序列需要精确的时间管理和状态同步
5.3 最佳实践建议
// 最佳实践模板
@Component
struct BestPracticeTemplate {
// 1. 状态管理
@State animationState: AnimationState = AnimationState.IDLE
// 2. 资源引用(便于清理)
private resources: Disposable[] = []
// 3. 动画配置(集中管理)
private animationConfig: AnimationConfig = {
duration: 1000,
easing: 'ease-in-out',
fps: 60
}
build() {
Column() {
// UI结构
}
.onAppear(() => this.initialize())
.onDisappear(() => this.cleanup())
}
private initialize(): void {
// 初始化逻辑
}
private cleanup(): void {
// 清理逻辑
this.resources.forEach(r => r.dispose())
}
}
enum AnimationState {
IDLE,
PLAYING,
PAUSED,
STOPPED
}
interface AnimationConfig {
duration: number
easing: string
fps: number
}
interface Disposable {
dispose(): void
}
SVG动画是HarmonyOS应用开发中不可或缺的视觉增强手段,合理运用SMIL声明式动画和JavaScript驱动动画,结合性能优化最佳实践,能够打造出流畅、美观的用户界面。在实际开发中,应根据具体场景选择合适的技术方案,并始终关注性能和内存管理,确保应用在各种设备上都能提供优质的用户体验。
- 点赞
- 收藏
- 关注作者
评论(0)