一、概述
动画在应用开发中扮演着重要的角色,能够提升用户体验,传达信息,引导用户操作,提升应用品质和增加视觉吸引力。而动画的性能表现也至关重要,优化可以从属性更新和布局等几个方面考虑,尽可能减少冗余刷新。本文将介绍如下4种优化手段,通过这些优化手段的单个使用或组合使用,可以对动画帧率、应用卡顿等方面带来优化,提升性能和用户体验:
- 使用系统提供的动画接口:系统接口经过精心设计和优化,能够在不同设备上提供流畅的动画效果,最大程度地减少丢帧率和卡顿现象。
- 使用图形变换属性变化组件布局:通过对组件的图形变换属性进行调整,而不是直接修改组件的布局属性,可以减少不必要的布局计算和重绘操作,从而降低丢帧率,提升动画的流畅度和响应速度。
- 参数相同时使用同一个animateTo:当多个动画的参数相同时,合并它们并使用同一个animateTo方法进行处理能够有效减少不必要的计算和渲染开销。
- 多次animateTo时统一更新状态变量:在进行多次动画操作时,统一更新状态变量可以避免不必要的状态更新和重复渲染,从而减少性能开销。
测试的关键指标:
- 丢帧率(Janky Frames):表示一个时间周期内的丢帧比率,指一个时间周期内有问题的帧比例。HarmonyOS系统要求每一帧都要在11.1ms(90Hz刷新率)内绘制完成,如果页面没有在11.1ms内完成这一帧的绘制,就会出现丢帧。部分丢帧一般用户肉眼是感知不到的,只有出现连续丢帧用户才有明显感知。
- 最大连续丢帧数(maximum successive frame dropping count):表示从页面开始有响应变化到页面结束刷新的过程中,由于显示器画面刷新频率低于预设的画面帧率而未能正常呈现的最大连续帧数。一般而言,当连续值超过3时,用户可以明显感知到卡顿掉帧,数值越大卡顿时间越长。
- 平均1s大卡顿次数:表示程序运行过程中平均1s出现连续丢3帧以上的卡顿的次数,数值越大用户体验感越差。
二、使用系统提供的动画接口
实现效果:同一界面多个按钮同时缩放的场景
1、自定义动画
自定义动画是指通过编写自定义的动画逻辑和计算过程来实现特定的动画效果。开发人员可以根据应用的需求和设计要求,使用自定义的动画算法和逻辑来创建独特的动画效果。自定义动画的优势在于可以实现非常个性化的动画效果,并且能够完全控制动画的每一个细节,但需要开发人员具备一定的动画算法和计算能力。
// 自定义动画代码
@Entry
@Component
struct CustomAnimationExample {
@State widthSize: number = 80
@State heightSize: number = 40
@State flag: boolean = true
// 自定义动画函数
computeSize() {
let duration = 2000
// 设定循环周期
let period = 1
let widthSizeEnd = 0
let heightSizeEnd = 0
if (this.flag) {
widthSizeEnd = 50
heightSizeEnd = 25
} else {
widthSizeEnd = 80
heightSizeEnd = 40
}
// 计算循环次数
let doTimes = duration / period
// 计算每次循环变化量
let deltaHeight = (heightSizeEnd - this.heightSize) / doTimes
let deltaWeight = (widthSizeEnd - this.widthSize) / doTimes
// 循环,组件每个周期增加一部分宽高
for (let i = 1; i <= doTimes; i++) {
let t = period * (i);
setTimeout(() => {
this.heightSize = this.heightSize + deltaHeight
this.widthSize = this.widthSize + deltaWeight
}, t)
}
this.flag = !this.flag
}
build() {
Column() {
Button()
.width(this.widthSize)
.height(this.heightSize)
// 其他相似布局
…
Button('click me')
.onClick(() => {
let delay = 500
// 调用自定义函数
setTimeout(() => {
this.computeSize()
}, delay)
})
.width('50%').height('15%').backgroundColor(0x317aff)
}.width('100%').margin({ top: 5 })
}
}
2、系统动画接口
系统动画接口是指通过使用系统提供的动画框架和接口来实现动画效果。在移动应用开发中,通常会使用属性动画来实现各种动画效果。通过可动画属性改变引起UI上产生的连续视觉效果,即为属性动画。属性动画是最基础易懂的动画,ArkUI提供两种属性动画接口animateTo和animation驱动组件属性按照动画曲线等动画参数进行连续的变化,产生属性动画。使用系统提供的动画接口可以简化动画的实现过程,并且能够充分利用系统优化的动画计算和渲染能力,从而提高动画的性能和流畅度。
// 系统动画
@Entry
@Component
struct PropertyAnimateToExample {
@State widthSize: number = 80;
@State heightSize: number = 40;
@State flag: boolean = true;
build() {
Column() {
Button()
.width(this.widthSize)
.height(this.heightSize)
// 对Button组件的宽高属性进行动画配置
.animation({
duration: 2000, // 动画时长
curve: Curve.Linear, // 动画曲线
delay: 500, // 动画延迟
iterations: 1, // 播放次数
playMode: PlayMode.Normal // 动画模式
})
// 其他相似布局
…
Button('click me')
.onClick((event?: ClickEvent | undefined) => {
if (this.flag) {
this.widthSize = 50
this.heightSize = 25
} else {
this.widthSize = 80
this.heightSize = 40
}
this.flag = !this.flag
})
.width('50%').height('15%').backgroundColor(0x317aff)
}.width('100%').margin({ top: 5 })
}
}
// 或
@Entry
@Component
struct ExplicitAnimateToExample {
@State widthSize: number = 80;
@State heightSize: number = 40;
@State flag: boolean = true;
build() {
Column() {
Button()
.width(this.widthSize)
.height(this.heightSize)
// 其他相似布局
…
Button('click me')
.onClick((event?: ClickEvent | undefined) => {
// 对Button组件的宽高属性进行动画配置
if (this.flag) {
animateTo({
duration: 2000, // 动画时长
curve: Curve.Linear, // 动画曲线
delay: 500, // 动画延迟
iterations: 1, // 播放次数
playMode: PlayMode.Normal // 动画模式
}, () => {
this.widthSize = 50;
this.heightSize = 25;
})
} else {
animateTo({
duration: 2000, // 动画时长
curve: Curve.Linear, // 动画曲线
delay: 500, // 动画延迟
iterations: 1, // 播放次数
playMode: PlayMode.Normal // 动画模式
}, () => {
this.widthSize = 80;
this.heightSize = 40;
})
}
this.flag = !this.flag;
})
.width('50%').height('15%').backgroundColor(0x317aff)
}.width('100%').margin({ top: 5 })
}
}
3、结论
系统动画接口内部实现对开发者是透明的,已经进行了尽可能的性能优化,避免开发者使用自定义动画时产生不必要的性能劣化。自定义动画适合实现个性化的、复杂的动画效果,而系统提供的动画接口则适合实现常见的动画效果并且能够获得更好的性能表现。因此,在动画能够使用系统接口实现的情况下,开发者应尽量使用系统接口实现,保持动画的流畅性和稳定性,提升应用的性能表现。
三、使用图形变换属性变化组件
实现效果:针对同一界面多个图片同时缩放并位移的场景(如下图所示),分别通过改变布局属性、改变图形变换属性实现
1、改动布局属性
·常见的布局属性包括位置、大小、内边距、外边距、对齐方式、权重等。当这些布局属性发生改变时,界面将重新布局以适应新的属性值。
@Entry
@Component
struct Index {
@State imageWidth: number = 60;
@State imageHeight: number = 60;
@State xPosition: number = 0;
@State yPosition: number = 0;
build() {
Column(){
Image($r('app.media.sample'))
.width(this.imageWidth)
.height(this.imageHeight)
.position({x: this.xPosition, y: this.yPosition})
// 其他相似布局
…
Button("布局属性")
// 按键属性设置
…
.onClick(() => {
let doTimes = 10;
// 按播放次数循环播放动画
for (let i = 0; i < doTimes; i++) {
// 间隔播放位置、宽高变化
if (i % 2 == 0){
setTimeout(() => {
animateTo({ duration:1000 }, () => {
this.imageWidth = 120;
this.imageHeight = 120;
this.xPosition = 15;
this.yPosition = 15;
})
}, 1000 * i)
} else {
setTimeout(() => {
animateTo({ duration: 1000 }, () => {
this.imageWidth = 60;
this.imageHeight = 60;
this.xPosition = 0;
this.yPosition = 0;
})
}, 1000 * i)
}
}
})
}.width('100%').margin({ top: 5 })
}
}
2、改动图形变换属性
·图形变换属性是指对组件布局结果的变换操作,如平移、旋转、缩放等操作。通过改变这些图形变换属性,可以实现对组件布局完成后,在界面上的位置和形态进行动态变换。
@Entry
@Component
struct Index {
@State imageScaleX: number = 1;
@State imageScaleY: number = 1;
@State imageTranslateX: number = 0;
@State imageTranslateY: number = 0;
build() {
Column() {
Image($r('app.media.like'))
.scale({ x: this.imageScaleX, y: this.imageScaleY, centerX: 0, centerY: 0 })
.translate({ x: this.imageTranslateX, y: this.imageTranslateY })
// 其他相似布局
…
Button("图形变换属性")
// 按键属性设置
…
.onClick(() => {
let doTimes = 10;
// 按播放次数循环播放动画
for (let i = 0; i < doTimes; i++) {
if (i % 2 == 0) {
setTimeout(() => {
animateTo({ duration: 1000 }, () => {
this.imageScaleX = 2;
this.imageScaleY = 2;
this.imageTranslateX = 15;
this.imageTranslateY = 15;
})
}, 1000 * i)
} else {
setTimeout(() => {
animateTo({ duration: 1000 }, () => {
this.imageScaleX = 1;
this.imageScaleY = 1;
this.imageTranslateX = 0;
this.imageTranslateY = 0;
})
}, 1000 * i)
}
}
})
}.width('100%').margin({ top: 5 })
}
}
图形变换属性 | 布局属性 |
---|---|
rotate | / |
translate | position、offset |
scale | width、height、Size |
transform | / |
3、结论
使用图形变化属性改变图片大小和位置时,能够显著降低丢帧率和大卡顿的发生频率。界面布局是非常耗时的操作,因此频繁地改动布局属性会导致界面性能下降,出现卡顿现象,影响用户体验。因此,在动画能够使用图形变化属性实现的情况下,开发者应尽量使用图形变化属性实现,保持动画的流畅性和稳定性,提升应用的性能表现。
四、参数相同时使用同一个animateTo
每次调用animateTo方法,都会触发一次属性变化,这意味着在每次动画执行时都需要进行动画前后的对比,以确定属性的变化情况。当多次连续调用animateTo时,会增加额外的布局计算和绘制开销,从而降低性能表现。特别是当这些animateTo操作针对同一个组件的属性时,会导致该组件更新的次数增加,进一步影响性能。
在实际开发中,如果多个属性需要以相同的动画参数进行变化,推荐将它们放到同一个动画闭包中执行。通过将多个属性的动画操作合并到同一个动画闭包中,可以减少对组件的多次更新,避免重复的布局计算和绘制操作,提升动画效果的性能。
除了性能方面的优势,将多个属性的动画操作合并到同一个动画闭包中还有助于提高代码的可读性和维护性。通过集中管理相关联的属性变化,可以使代码结构更加清晰,便于后续的维护和修改。
实现效果:针对多个相同组件同时修改多个属性的场景
1、多个animateTo闭包
@Entry
@Component
struct MyComponent {
@State w:number = 150
@State h:number = 2
@State brightNum:number = 1.5
@State color:Color = Color.Red
// 动画闭包1,设置宽度变化
func1() {
animateTo({curve: Curve.Sharp, duration: 1000}, () => {
this.w = (this.w === 80 ? 150 : 80);
});
}
// 动画闭包2,设置颜色变化
func2() {
animateTo({curve: Curve.Sharp, duration: 1000}, () => {
this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
});
}
// 动画闭包3,设置高度变化
func3() {
animateTo({curve: Curve.Sharp, duration: 1000}, () => {
this.h = (this.h === 2 ? 5 : 2);
});
}
// 动画闭包4,设置高光变化
func4() {
animateTo({curve: Curve.Sharp, duration: 1000}, () => {
this.brightNum= (this.brightNum=== 1.5 ? 1 : 1.5);
});
}
build() {
Column() {
Row()
.width(this.w)
.backgroundColor(this.color)
.height(this.h)
.brightness(this.brightNum)
// 其他相似布局
…
Button("click nFunc")
// 按键属性设置
…
.onClick(() => {
let doTimes = 10;
// 按播放次数循环播放动画
for (let i = 0; i < doTimes; i++) {
setTimeout(() => {
this.func1();
this.func2();
this.func3();
this.func4();
}, 1000 * i)
}
})
}
}
}
2、一个animateTo闭包
@Entry
@Component
struct MyComponent {
@State w:number = 150
@State h:number = 2
@State brightNum:number = 1.5
@State color:Color = Color.Red
// 统一动画闭包,同时设置四个属性变化
func() {
animateTo({curve: Curve.Sharp, duration: 1000}, () => {
this.w = (this.w === 80 ? 150 : 80);
this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
this.h = (this.h === 2 ? 5 : 2);
this.brightNum= (this.brightNum=== 1.5 ? 1 : 1.5);
});
}
build() {
Column() {
Row()
.width(this.w)
.backgroundColor(this.color)
.height(this.h)
.brightness(this.brightNum)
// 其他相似布局
…
Button("click oneFunc")
// 按键属性设置
…
.onClick(() => {
let doTimes = 10;
// 按播放次数循环播放动画
for (let i = 0; i < doTimes; i++) {
setTimeout(() => {
this.func();
}, 1000 * i)
}
})
}
}
}
3、结论
将多个属性变化动画合并到同一个animateTo动画闭包中能够显著降低丢帧率和大卡顿的发生频率。合并动画操作可以减少不必要的布局计算和绘制开销,从而提升动画的流畅性和性能表现,有助于优化动画效果的性能。
五、多次animateTo时统一更新状态变量
使用animateTo方法执行动画时,会对执行动画闭包前后的状态进行对比,然后只对差异部分进行动画处理。
在动画执行过程中,脏节点是指在界面上需要进行重新绘制的区域。如果状态发生了变化,ArkUI会跟踪这些变化,并在动画闭包执行前进行状态对比,相关的脏节点会被标记为需要刷新,以便在动画闭包执行前对这些脏节点进行重新绘制。这样,只有发生变化的部分才会被纳入动画处理,而不需要重新绘制整个界面。
这种差异对比的方式能够显著减少不必要的绘制操作,提高动画的性能和流畅度。
实现效果:针对多个相同组件同时修改多个属性的场景
1、在多个animateTo之间更新状态变量
@Entry
@Component
struct MyComponent {
@State w: number = 100
@State h: number = 2
@State color: Color = Color.Red
build() {
Column() {
Row()
.width(this.w)
.backgroundColor(this.color)
.height(this.h)
// 其他相似布局
…
Button("click1")
// 按键属性设置
…
.onClick(() => {
let doTimes = 5;
for (let i = 0; i < doTimes; i++) {
setTimeout(() => {
this.w = 80
// h是非动画属性
this.h = 4
animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
this.w = (this.w === 80 ? 150 : 80);
});
// 在两个animateTo之间更新状态变量
this.color = Color.Yellow
animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
});
}, 2000 * i)
}
})
}
}
}
2、在animateTo之前显式指定属性初值
@Entry
@Component
struct MyComponent {
@State w: number = 100
@State h: number = 2
@State color: Color = Color.Red
build() {
Column() {
Row()
.width(this.w)
.backgroundColor(this.color)
.height(this.h)
// 其他相似布局
…
Button("click2")
// 按键属性设置
…
.onClick(() => {
let doTimes = 5;
for (let i = 0; i < doTimes; i++) {
setTimeout(() => {
// 在动画之前显式的指定所有需要动画的属性初值
this.w = 80
this.color = Color.Yellow
// 动画1,修改宽度
animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
this.w = (this.w === 80 ? 150 : 80);
});
// 动画2,修改颜色
animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
});
// 动画完成后刷新非动画属性
this.h = 5
}, 2000 * i)
}
})
}
}
}
3、在animateTo之前使用原始状态
@Entry
@Component
struct MyComponent {
//原始状态
@State w: number = 80
@State h: number = 5
@State color: Color = Color.Yellow
build() {
Column() {
Row()
.width(this.w)
.backgroundColor(this.color)
.height(this.h)
// 其他相似布局
…
Button("click3")
// 按键属性设置
…
.onClick(() => {
let doTimes = 5;
for (let i = 0; i < doTimes; i++) {
// 直接使用原始状态实现动画
setTimeout(() => {
animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
this.w = (this.w === 80 ? 150 : 80);
});
animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
});
}, 2000 * i)
}
})
}
}
}
4、结论
在进行属性变化动画时,显式指定属性初值或者使用原始状态作为动画的起始状态能够显著降低丢帧率和大卡顿的发生频率。因此,在进行动画操作时,合理管理状态变量的更新和初始值设定对于优化动画效果的性能至关重要,开发者应根据需要,尽可能地避免在多个animateTo之间更新状态变量,从而提升动画的流畅性和性能表现。
六、使用renderGroup
renderGroup是组件通用方法,它代表了渲染绘制的一个组合。其核心功能就是标记组件,在绘制阶段将组件和其子组件的绘制结果进行合并并缓存,以达到复用的效果,从而降低绘制负载。
组件渲染流程图如下所示:
在进行缓存更新时,需要满足以下三个条件:
- 组件在当前组件树上。
- 组件renderGroup被标记为true。
- 组件内容被标脏。
在进行缓存清理时,需要满足以下任意条件:
- 组件不存在于组件树上。
- 组件renderGroup被标记为false。
具体缓存管理流程图如下所示:
实现效果: 在同一个页面下使用了固定的图片和文本内容,并且每个组件统一使用旋转和缩放的动效
// IconItem.ets
@Component
export struct IconItem {
@StorageLink('renderGroupFlag') renderGroupFlag: boolean = false;
image: string | Resource = '';
text: string | Resource = '';
build() {
Flex({
direction: FlexDirection.Column,
justifyContent: FlexAlign.Center,
alignContent: FlexAlign.Center
}) {
Image(this.image)
.height(20)
.width(20)
.objectFit(ImageFit.Contain)
.margin({ left: 15 })
Text(this.text)
.fontSize(10)
.fontColor("#182431")
.margin({ top: 5 })
.width(50)
.opacity(0.8)
.textAlign(TextAlign.Center)
}
.backgroundColor('#e3e3e3')
.width(50)
.height(50)
.borderRadius(25)
// 在IconItem内调用renderGroup,false为关闭,true为开启
.renderGroup(this.renderGroupFlag)
}
}
// Index.ets
import { IconItem } from './IconItem'
// IconItem相关数据
class IconItemSource {
image: string | Resource = ''
text: string | Resource = ''
constructor(image: string | Resource = '', text: string | Resource = '') {
this.image = image;
this.text = text;
}
}
@Entry
@Component
struct Index {
// renderGroup接口是否开启
@State renderGroupFlag: boolean = false;
private iconItemSourceList: IconItemSource[] = [];
aboutToAppear() {
// 遍历添加60个IconItem的数据
for (let index = 0; index < 20; index++) {
const numStart: number = index * 3;
// 此处循环使用三张图片资源
this.iconItemSourceList.push(
new IconItemSource($r('app.media.album'), `item${numStart + 1}`),
new IconItemSource($r('app.media.applet'), `item${numStart + 2}`),
new IconItemSource($r('app.media.cards'), `item${numStart + 3}`),
);
}
}
build() {
Column() {
Row() {
Row() {
Text('场景示例')
.fontSize(24)
.lineHeight(24)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bold)
.margin({ left: 30 })
}
// 动态切换renderGroup功能
Stack({ alignContent: Alignment.End }) {
Button(this.renderGroupFlag ? 'renderGroup已开启' : 'renderGroup已关闭', {
type: ButtonType.Normal,
stateEffect: true
})
.fontSize(12)
.borderRadius(8)
.backgroundColor(0x317aff)
.width(150)
.height(30)
.margin({ right: 30 })
.onClick(() => {
this.renderGroupFlag = !this.renderGroupFlag;
AppStorage.setOrCreate('renderGroupFlag', this.renderGroupFlag)
})
}
}
.height(56)
.width('100%')
.backgroundColor(Color.White)
.justifyContent(FlexAlign.SpaceBetween)
// IconItem放置在grid内
GridRow({
columns: 6,
gutter: { x: 0, y: 0 },
breakpoints: { value: ["400vp", "600vp", "800vp"],
reference: BreakpointsReference.WindowSize },
direction: GridRowDirection.Row
}) {
ForEach(this.iconItemSourceList, (item: IconItemSource) => {
GridCol() {
IconItem({ image: item.image, text: item.text })
.transition(
TransitionEffect.scale({ x: 0.5, y: 0.5 })
.animation({duration: 3000, curve: Curve.FastOutSlowIn, iterations: -1 })
.combine(TransitionEffect.rotate({ z: 1, angle: 360 })
.animation({ duration: 3000, curve: Curve.Linear, iterations: -1 }))
)
}
.height(70)
.width('25%')
})
}
.width("100%")
.height("100%")
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
}
}
1、结论
在在单一页面上存在大量应用动效的组件,且组件和其子组件各属性保持固定、组件统一应用动效时,开发者可以使用renderGroup来提升应用的性能,保证应用及动画的流畅性。
七、总结
- 用户体验:流畅的动画能够提升用户体验,使用户感到界面更加生动和直观。相反,卡顿或者延迟的动画会带来不良的体验。
- 设备资源消耗:动画的性能表现会直接影响设备的资源消耗,尤其是CPU和GPU的占用。低性能的动画可能导致设备发热、耗电量增加,甚至影响其他应用的运行。
- 应用稳定性:性能较差的动画可能会导致应用崩溃或者卡顿,影响应用的稳定性和可靠性。
- 节省流量:高性能的动画可以减少数据传输量,对于移动应用来说,这意味着可以节省用户的流量消耗。
- 适应不同设备:不同设备的性能差异很大,提升动画性能可以使应用在各种设备上都能够流畅运行。