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 = truebuild() {Column() {Button('change size').onClick(() => {if (this.flag) {this.widthSize = 150this.heightSize = 60} else {this.widthSize = 250this.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 = 0private flag: boolean = truebuild() {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 = 150this.heightSize = 60})} else {animateTo({}, () => {this.widthSize = 250this.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 = truebuild() {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 {@StatemyCount : number = 100flag : 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,//粒子生命周期,单位mslifetimeRange: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 = 1build() {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组件播放视频。
- 子组件无动效:由父组件统一应用动效,其子组件均无动效。如果子组件上也应用动效,那么子组件相对父组件就不再是静止的,每一帧都有可能需要更新缓存,更新逻辑同样需要消耗系统资源。