ArkUI-动画
- 系统能力
- 属性动画
- 显式动画
- 关键帧动画
- 转场动画
- 路径动画
- 粒子动画
- 资源调用
- GIF动画
- 帧动画
- 三方库
- Lottie
- SVG
- 提升动画的流畅度
- 使用renderGroup
- 概述
- 使用约束
系统能力
属性动画
通过更改组件的属性值实现渐变过渡效果,例如缩放、旋转、平移等。支持的属性包括width、height、backgroundColor、opacity、scale、rotate、translate等。
@Entry
@Component
struct AttrAnimationExample {
@State widthSize: number = 250
@State heightSize: number = 100
@State rotateAngle: number = 0
@State flag: boolean = true
build() {
Column() {
Button('change size')
.onClick(() => {
if (this.flag) {
this.widthSize = 150
this.heightSize = 60
} else {
this.widthSize = 250
this.heightSize = 100
}
this.flag = !this.flag
})
.margin(30)
.width(this.widthSize)
.height(this.heightSize)
.animation({
duration: 2000,
curve: Curve.EaseOut,
iterations: 3,
playMode: PlayMode.Normal
})
Button('change rotate angle')
.onClick(() => {
this.rotateAngle = 90
})
.margin(50)
.rotate({ angle: this.rotateAngle })
.animation({
duration: 1200,
curve: Curve.Friction,
delay: 500,
iterations: -1, // 设置-1表示动画无限循环
playMode: PlayMode.Alternate,
expectedFrameRateRange: {
min: 20,
max: 120,
expected: 90,
}
})
}.width('100%').margin({ top: 20 })
}
}
显式动画
可以通过用户的直接操作或应用程序的特定逻辑来触发,例如按钮点击时的缩放动画、列表项展开时的渐变动画等。HarmonyOS提供了全局animateTo显式动画接口来指定由于闭包代码导致状态变化的插入过渡动效。
// xxx.ets
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250
@State heightSize: number = 100
@State rotateAngle: number = 0
private flag: boolean = true
build() {
Column() {
Button('change size')
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
if (this.flag) {
animateTo({
duration: 2000,
curve: Curve.EaseOut,
iterations: 3,
playMode: PlayMode.Normal,
onFinish: () => {
console.info('play end')
}
}, () => {
this.widthSize = 150
this.heightSize = 60
})
} else {
animateTo({}, () => {
this.widthSize = 250
this.heightSize = 100
})
}
this.flag = !this.flag
})
Button('change rotate angle')
.margin(50)
.rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle })
.onClick(() => {
animateTo({
duration: 1200,
curve: Curve.Friction,
delay: 500,
iterations: -1, // 设置-1表示动画无限循环
playMode: PlayMode.Alternate,
onFinish: () => {
console.info('play end')
},
expectedFrameRateRange: {
min: 10,
max: 120,
expected: 60,
}
}, () => {
this.rotateAngle = 90
})
})
}.width('100%').margin({ top: 5 })
}
}
关键帧动画
在UIContext中提供keyframeAnimateTo接口来指定若干个关键帧状态,实现分段的动画。
// xxx.ets
import { UIContext } from '@kit.ArkUI';
@Entry
@Component
struct KeyframeDemo {
@State myScale: number = 1.0;
uiContext: UIContext | undefined = undefined;
aboutToAppear() {
this.uiContext = this.getUIContext?.();
}
build() {
Column() {
Circle()
.width(100)
.height(100)
.fill("#46B1E3")
.margin(100)
.scale({ x: this.myScale, y: this.myScale })
.onClick(() => {
if (!this.uiContext) {
console.info("no uiContext, keyframe failed");
return;
}
this.myScale = 1;
// 设置关键帧动画整体播放3次
this.uiContext.keyframeAnimateTo({ iterations: 3 }, [
{
// 第一段关键帧动画时长为800ms,scale属性做从1到1.5的动画
duration: 800,
event: () => {
this.myScale = 1.5;
}
},
{
// 第二段关键帧动画时长为500ms,scale属性做从1.5到1的动画
duration: 500,
event: () => {
this.myScale = 1;
}
}
]);
})
}.width('100%').margin({ top: 5 })
}
}
转场动画
路径动画
指对象沿着指定路径进行移动的动画效果。通过设置路径可以实现视图沿着预定义的路径进行移动,例如曲线运动、圆周运动等,为用户呈现更加生动的交互效果。
// xxx.ets
@Entry
@Component
struct MotionPathExample {
@State toggle: boolean = true
build() {
Column() {
Button('click me').margin(50)
// 执行动画:从起点移动到(300,200),再到(300,500),再到终点
.motionPath({ path: 'Mstart.x start.y L300 200 L300 500 Lend.x end.y', from: 0.0, to: 1.0, rotatable: true })
.onClick(() => {
animateTo({ duration: 4000, curve: Curve.Linear }, () => {
this.toggle = !this.toggle // 通过this.toggle变化组件的位置
})
})
}.width('100%').height('100%').alignItems(this.toggle ? HorizontalAlign.Start : HorizontalAlign.Center)
}
}
粒子动画
通过大量小颗粒的运动来形成整体动画效果。通过对粒子在颜色、透明度、大小、速度、加速度、自旋角度等维度变化做动画,来营造一种氛围感。
@Entry
@Component
struct ParticleExample {
@State
myCount : number = 100
flag : boolean = false;
build() {
Column(){
Stack() {
Particle({particles:[
{
emitter:{
particle:{
type:ParticleType.IMAGE,//粒子类型
config:{
src:$r("app.media.book"),
size:[10,10]
},
count: this.myCount,//粒子总数
lifetime:10000,//粒子生命周期,单位ms
lifetimeRange:100//粒子生命周期取值范围,单位ms
},
emitRate:3,//每秒发射粒子数
shape:ParticleEmitterShape.CIRCLE//发射器形状
},
color:{
range:[Color.White,Color.White]//初始颜色范围
},
opacity:{
range:[1.0,1.0],
updater:{
type:ParticleUpdater.CURVE,//变化方式为曲线变化
config:[
{
from:0,//变化起始值
to:1.0,//变化终点值
startMillis:0,//开始时间
endMillis:6000//结束时间
},
{
from:1.0,
to:.0,
startMillis:6000,
endMillis:10000
}
]
}
},
scale:{
range:[0.1,1.0],
updater:{
type:ParticleUpdater.CURVE,
config:[
{
from: 0,
to: 1.5,
startMillis: 0,
endMillis: 8000,
curve: Curve.EaseIn
}
]
}
},
acceleration:{
speed:{
range:[3,9],
updater:{
type: ParticleUpdater.CURVE,
config:[
{
from:10,
to:20,
startMillis:0,
endMillis:3000,
curve:Curve.EaseIn
},
{
from:10,
to:2,
startMillis:3000,
endMillis:8000,
curve:Curve.EaseIn
}
]
}
},
angle:{
range:[0,180],
updater:{
type:ParticleUpdater.CURVE,
config:[{
from:1,
to:2,
startMillis:0,
endMillis:1000,
curve:Curve.EaseIn
},
{
from:50,
to:-50,
startMillis:1000,
endMillis:3000,
curve:Curve.EaseIn
},
{
from:3,
to:5,
startMillis:3000,
endMillis:8000,
curve:Curve.EaseIn
}
]
}
}
},
spin:{
range:[0.1,1.0],
updater:{
type:ParticleUpdater.CURVE,
config:[
{
from: 0,
to: 360,
startMillis: 0,
endMillis: 8000,
curve: Curve.EaseIn
}
]
}
},
}
,{
emitter:{
particle:{
type:ParticleType.IMAGE,
config:{
src:$r('app.media.heart'),
size:[10,10]
},
count: this.myCount,
lifetime:10000,
lifetimeRange:100
},
emitRate:3,
shape:ParticleEmitterShape.CIRCLE
},
color:{
range:[Color.White,Color.White]
},
opacity:{
range:[1.0,1.0],//粒子透明度
updater:{
type:ParticleUpdater.CURVE,//透明度的变化方式是随机变化
config:[
{
from:0,
to:1.0,
startMillis:0,
endMillis:6000
},
{
from:1.0,
to:.0,
startMillis:6000,
endMillis:10000
}
]
}
},
scale:{
range:[0.1,1.0],
updater:{
type:ParticleUpdater.CURVE,
config:[
{
from: 0,
to: 2.0,
startMillis: 0,
endMillis: 10000,
curve: Curve.EaseIn
}
]
}
},
acceleration:{//加速度的配置,从大小和方向两个维度变化,speed表示加速度大小,angle表示加速度方向
speed:{
range:[3,9],
updater:{
type: ParticleUpdater.CURVE,
config:[
{
from:10,
to:20,
startMillis:0,
endMillis:3000,
curve:Curve.EaseIn
},
{
from:10,
to:2,
startMillis:3000,
endMillis:8000,
curve:Curve.EaseIn
}
]
}
},
angle:{
range:[0,180],
updater:{
type:ParticleUpdater.CURVE,
config:[{
from:1,
to:2,
startMillis:0,
endMillis:1000,
curve:Curve.EaseIn
},
{
from:50,
to:-50,
startMillis:0,
endMillis:3000,
curve:Curve.EaseIn
},
{
from:3,
to:5,
startMillis:3000,
endMillis:10000,
curve:Curve.EaseIn
}
]
}
}
},
spin:{
range:[0.1,1.0],
updater:{
type:ParticleUpdater.CURVE,
config:[
{
from: 0,
to: 360,
startMillis: 0,
endMillis: 10000,
curve: Curve.EaseIn
}
]
}
},
},{
emitter:{
particle:{
type:ParticleType.IMAGE,
config:{
src:$r('app.media.sun'),
size:[10,10]
},
count: this.myCount,
lifetime:10000,
lifetimeRange:100
},
emitRate:3,
shape:ParticleEmitterShape.CIRCLE
},
color:{
range:[Color.White,Color.White]
},
opacity:{
range:[1.0,1.0],
updater:{
type:ParticleUpdater.CURVE,
config:[
{
from:0,
to:1.0,
startMillis:0,
endMillis:6000
},
{
from:1.0,
to:.0,
startMillis:6000,
endMillis:10000
}
]
}
},
scale:{
range:[0.1,1.0],
updater:{
type:ParticleUpdater.CURVE,
config:[
{
from: 0,
to: 2.0,
startMillis: 0,
endMillis: 10000,
curve: Curve.EaseIn
}
]
}
},
acceleration:{
speed:{
range:[3,9],
updater:{
type: ParticleUpdater.CURVE,
config:[
{
from:10,
to:20,
startMillis:0,
endMillis:3000,
curve:Curve.EaseIn
},
{
from:10,
to:2,
startMillis:3000,
endMillis:8000,
curve:Curve.EaseIn
}
]
}
},
angle:{
range:[0,180],
updater:{
type:ParticleUpdater.CURVE,
config:[{
from:1,
to:2,
startMillis:0,
endMillis:1000,
curve:Curve.EaseIn
},
{
from:50,
to:-50,
startMillis:1000,
endMillis:3000,
curve:Curve.EaseIn
},
{
from:3,
to:5,
startMillis:3000,
endMillis:8000,
curve:Curve.EaseIn
}
]
}
}
},
spin:{
range:[0.1,1.0],
updater:{
type:ParticleUpdater.CURVE,
config:[
{
from: 0,
to: 360,
startMillis: 0,
endMillis: 10000,
curve: Curve.EaseIn
}
]
}
},
}
]
}).width(300).height(300)
}.width(500).height(500).align(Alignment.Center)
}.width("100%").height("100%")
}
}
资源调用
GIF动画
GIF动画可以在特定位置循环播放,为应用界面增添生动的视觉效果。在开发中,可以使用Image组件来实现GIF动画的播放。
帧动画
通过逐帧播放一系列图片来实现动画效果,在开发中可以使用ImageAnimator组件来实现帧动画的播放。
// xxx.ets
@Entry
@Component
struct ImageAnimatorExample {
@State state: AnimationStatus = AnimationStatus.Initial
@State reverse: boolean = false
@State iterations: number = 1
build() {
Column({ space: 10 }) {
ImageAnimator()
.images([
{
src: $r('app.media.img1')
},
{
src: $r('app.media.img2')
},
{
src: $r('app.media.img3')
},
{
src: $r('app.media.img4')
}
])
.duration(2000)
.state(this.state).reverse(this.reverse)
.fillMode(FillMode.None).iterations(this.iterations).width(340).height(240)
.margin({ top: 100 })
.onStart(() => {
console.info('Start')
})
.onPause(() => {
console.info('Pause')
})
.onRepeat(() => {
console.info('Repeat')
})
.onCancel(() => {
console.info('Cancel')
})
.onFinish(() => {
console.info('Finish')
this.state = AnimationStatus.Stopped
})
Row() {
Button('start').width(100).padding(5).onClick(() => {
this.state = AnimationStatus.Running
}).margin(5)
Button('pause').width(100).padding(5).onClick(() => {
this.state = AnimationStatus.Paused // 显示当前帧图片
}).margin(5)
Button('stop').width(100).padding(5).onClick(() => {
this.state = AnimationStatus.Stopped // 显示动画的起始帧图片
}).margin(5)
}
Row() {
Button('reverse').width(100).padding(5).onClick(() => {
this.reverse = !this.reverse
}).margin(5)
Button('once').width(100).padding(5).onClick(() => {
this.iterations = 1
}).margin(5)
Button('infinite').width(100).padding(5).onClick(() => {
this.iterations = -1 // 无限循环播放
}).margin(5)
}
}.width('100%').height('100%')
}
}
三方库
Lottie
//构建渲染上下文
private mainRenderingSettings: RenderingContextSettings = new RenderingContextSettings(true)
private mainCanvasRenderingContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.mainRenderingSettings)
build() {
Column() {
// 显示徽章
List({ space: Constants.MIDDLE_SPACE }) {
ForEach(ACHIEVE_IMAGE_LIST, (item: AchieveImage) => {
ListItem() {
Image(this.getShowImg(item))
// 图片的属性值
…
// 点击事件
.onClick(() => {
if (this.learnedIds.includes(item.pathId)) {
lottie.loadAnimation({
container: this.mainCanvasRenderingContext,
renderer: 'canvas',
loop: false,
autoplay: false,
name: item.pathId,
path: item.lottiePath
})
lottie.play()
this.clickedItem = item;
this.isShow = true;
}
})
}, (item: AchieveImage) => JSON.stringify(item))
}
// 模态转场
.bindContentCover(
this.isShow,
this.playLottieBuilder(),
{ modalTransition: ModalTransition.ALPHA, backgroundColor: $r('app.color.achieve_background_color'), onDisappear: () => {lottie.destroy()}}
)
// 列表属性
…
}
// 列容器属性
…
}
//模态转场后页面
@Builder playLottieBuilder() {
Column() {
Column() {
// 建立画布
Canvas(this.mainCanvasRenderingContext)
.height('50%')
.width('80%')
.backgroundColor($r('app.color.achieve_background_color'))
.onReady(() => {
if (this.clickedItem != null) {
lottie.loadAnimation({
container: this.mainCanvasRenderingContext,
renderer: 'canvas',
loop: false,
autoplay: true,
name: this.clickedItem.pathId,
path: this.clickedItem.lottiePath
})
}
})
.onClick(() => {
this.isShow = false;
})
}
Column() {
Button('知道啦')
.onClick(() => {
this.isShow = false;
})
}
}
}
}
SVG
提升动画的流畅度
- 使用系统提供的动画接口:系统接口经过精心设计和优化,能够在不同设备上提供流畅的动画效果,最大程度地减少丢帧率和卡顿现象。
- 使用图形变换属性变化组件布局:通过对组件的图形变换属性进行调整,而不是直接修改组件的布局属性,可以减少不必要的布局计算和重绘操作,从而降低丢帧率,提升动画的流畅度和响应速度。
- 参数相同时使用同一个animateTo:当多个动画的参数相同时,将相同动画参数的动画合并在一个动画闭包中并使用同一个animateTo方法进行处理能够有效减少不必要的计算和渲染开销。
- 多次animateTo时统一更新状态变量:在进行多次动画操作时,统一更新状态变量可以避免不必要的状态更新和重复渲染,从而减少性能开销。
使用renderGroup
概述
renderGroup是组件通用方法,它代表了渲染绘制的一个组合。其核心功能就是标记组件,在绘制阶段将组件和其子组件的绘制结果进行合并并缓存,以达到复用的效果,从而降低绘制负载。首次绘制组件时,若组件被标记为启用renderGroup状态,将对组件和其子组件进行离屏绘制,将绘制结果进行缓存。此后当需要重新绘制组件时,就会优先使用缓存而不必重新绘制,从而降低绘制负载,优化渲染性能。组件渲染流程图如下所示:
在进行缓存更新时,需要满足以下三个条件:
- 组件在当前组件树上。
- 组件renderGroup被标记为true。
- 组件内容被标脏。
在进行缓存清理时,需要满足以下任意条件:
- 组件不存在于组件树上。
- 组件renderGroup被标记为false。
具体缓存管理流程图如下所示:
使用约束
为了能使renderGroup功能生效,组件存在以下约束。
- 组件内容固定不变:父组件和其子组件各属性保持固定,不发生变化。如果父组件内容不是固定的,也就是说其子组件中上存在某些属性变化或者样式变化的组件,此时如果使用renderGroup,那么缓存的利用率将大大下降,并且有可能需要不断执行缓存更新逻辑,在这种情况下,不仅不能优化卡顿效果,甚至还可能使卡顿恶化。例如:文本内容使用双向绑定的动态数据;图片资源使用gif格式;使用video组件播放视频。
- 子组件无动效:由父组件统一应用动效,其子组件均无动效。如果子组件上也应用动效,那么子组件相对父组件就不再是静止的,每一帧都有可能需要更新缓存,更新逻辑同样需要消耗系统资源。