1# 应用程序动效能力实践
2
3## 概述
4本文介绍如何在开发应用程序时合理地使用动效,来获得更好的性能。主要通过减少布局和属性的变更频次,避免冗余刷新,从而降低性能开销。
5基于上述考虑,提供四种较为推荐的动效实现方式:
6
7- 组件转场动画使用transition
8- 组件布局改动时使用图形变换属性动画
9- 动画参数相同时使用同一个animateTo
10- 多次animateTo时统一更新状态变量
11
12## 合理使用动效
13
14### 组件转场动画使用transition
15在实现组件出现和消失的动画效果时,通常有两种方式:
16
17- 使用组件动画(animateTo),并在在动画结束回调中增加逻辑处理。
18- 直接使用转场动画(transition)。
19
20animateTo需要在动画前后做两次属性更新,而transition只需做一次条件改变更新,性能更好。此外,使用transition,可以避免在结束回调中做复杂逻辑处理,开发实现更容易。因此,推荐优先使用transition。
21
22反例:通过改变透明度属性,从1到0进行隐藏,并在动画结束回调中控制组件的消失。
23
24```typescript
25@Entry
26@Component
27struct MyComponent {
28  @State mOpacity: number = 1;
29  @State show: boolean = true;
30  count: number = 0;
31
32  build() {
33    Column() {
34      Row() {
35        if (this.show) {
36          Text('value')
37            .opacity(this.mOpacity)
38        }
39      }
40      .width('100%')
41      .height(100)
42      .justifyContent(FlexAlign.Center)
43      Text('toggle state')
44        .onClick(() => {
45          this.count++;
46          const thisCount: number = this.count;
47          this.show = true;
48          // 通过改变透明度属性,对Text控件做隐藏或出现的动画
49          animateTo({ duration: 1000, onFinish: () => {
50            // 在最后一个动画中,先让Text控件隐藏,再改变条件让Text控件消失
51            if (thisCount === this.count && this.mOpacity === 0) {
52              this.show = false;
53            }
54          } }, () => {
55            this.mOpacity = this.mOpacity === 1 ? 0 : 1;
56          })
57        })
58    }
59  }
60}
61```
62
63正例:直接使用转场动画,实现Text控件透明度的出现与消失。
64
65```typescript
66@Entry
67@Component
68struct MyComponent {
69  @State show: boolean = true;
70
71  build() {
72    Column() {
73      Row() {
74        if (this.show) {
75          Text('value')
76            // 设置id,使转场可打断
77            .id('myText')
78            .transition(TransitionEffect.OPACITY.animation({ duration: 1000 }))
79        }
80      }.width('100%')
81      .height(100)
82      .justifyContent(FlexAlign.Center)
83      Text('toggle state')
84        .onClick(() => {
85          // 通过transition,做透明度的出现或消失动画
86          this.show = !this.show;
87        })
88    }
89  }
90}
91```
92
93### 组件布局改动时使用图形变换属性动画
94改动组件的布局显示有两种方式:
95
96- 通过改变布局属性,实现[属性动画](../ui/arkts-attribute-animation-overview.md):当布局属性发生改变时,界面将重新布局。常见的布局属性有width、height、layoutWeight等。
97- 通过改变[图形变换属性](../reference/apis-arkui/arkui-ts/ts-universal-attributes-transformation.md),实现[属性动画](../ui/arkts-attribute-animation-overview.md):图形变换是对组件布局结果的变换操作,如平移、旋转、缩放等操作。
98
99界面布局是非常耗时的操作,而当图形变换属性发生变化时,并不会重新触发布局。因此,优先推荐使用图形变换属性来实现组件布局的改动。接下来,采用上述两种方式分别对组件实现放大10倍的效果。
100
101反例:通过设置布局属性width和height,改变组件大小。
102
103```typescript
104@Entry
105@Component
106struct MyComponent {
107  @State textWidth: number = 10;
108  @State textHeight: number = 10;
109
110  build() {
111    Column() {
112      Text()
113        .backgroundColor(Color.Blue)
114        .fontColor(Color.White)
115        .fontSize(20)
116        .width(this.textWidth)
117        .height(this.textHeight)
118
119      Button('布局属性')
120        .backgroundColor(Color.Blue)
121        .fontColor(Color.White)
122        .fontSize(20)
123        .margin({ top: 30 })
124        .borderRadius(30)
125        .padding(10)
126        .onClick(() => {
127          animateTo({ duration: 1000 }, () => {
128            this.textWidth = 100;
129            this.textHeight = 100;
130          })
131        })
132    }
133}
134}
135```
136
137在对组件位置或大小变化做动画时,由于布局属性的改变会触发重新测量布局,导致性能开销大。scale属性的改变不会重新触发测量布局,性能开销小。因此,在组件位置大小持续发生变化的场景,如手指缩放的动画场景,推荐使用scale。
138
139正例:通过设置图形变换属性scale,改变组件大小。
140
141```typescript
142@Entry
143@Component
144struct MyComponent {
145  @State textScaleX: number = 1;
146  @State textScaleY: number = 1;
147
148  build() {
149    Column() {
150      Text()
151        .backgroundColor(Color.Blue)
152        .fontColor(Color.White)
153        .fontSize(20)
154        .width(10)
155        .height(10)
156        .scale({ x: this.textScaleX, y: this.textScaleY })
157        .margin({ top: 100 })
158
159      Button('图形变换属性')
160        .backgroundColor(Color.Blue)
161        .fontColor(Color.White)
162        .fontSize(20)
163        .margin({ top: 60 })
164        .borderRadius(30)
165        .padding(10)
166        .onClick(() => {
167          animateTo({ duration: 1000 }, () => {
168            this.textScaleX = 10;
169            this.textScaleY = 10;
170          })
171        })
172    }
173}
174}
175```
176
177### 动画参数相同时使用同一个animateTo
178每次animateTo都需要进行动画前后的对比,因此,减少animateTo的使用次数(例如使用同一个animateTo设置组件属性),可以减少该组件更新的次数,从而获得更好的性能。
179如果各个属性要做动画的参数相同,推荐将它们放到同一个动画闭包中执行。
180
181反例:相同动画参数的状态变量更新放在不同的动画闭包中。
182
183```typescript
184@Entry
185@Component
186struct MyComponent {
187  @State textWidth: number = 200;
188  @State color: Color = Color.Red;
189
190  func1() {
191    animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
192      this.textWidth = (this.textWidth === 100 ? 200 : 100);
193    });
194  }
195
196  func2() {
197    animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
198      this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
199    });
200  }
201
202  build() {
203    Column() {
204      Row()
205        .width(this.textWidth)
206        .height(10)
207        .backgroundColor(this.color)
208      Text('click')
209        .onClick(() => {
210          this.func1();
211          this.func2();
212        })
213    }
214    .width('100%')
215    .height('100%')
216  }
217}
218```
219
220正例:将相同动画参数的动画合并在一个动画闭包中。
221
222```typescript
223@Entry
224@Component
225struct MyComponent {
226  @State textWidth: number = 200;
227  @State color: Color = Color.Red;
228
229  func() {
230    animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
231      this.textWidth = (this.textWidth === 100 ? 200 : 100);
232      this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
233    });
234  }
235
236  build() {
237    Column() {
238      Row()
239        .width(this.textWidth)
240        .height(10)
241        .backgroundColor(this.color)
242      Text('click')
243        .onClick(() => {
244          this.func();
245        })
246    }
247    .width('100%')
248    .height('100%')
249  }
250}
251```
252
253### 多次animateTo时统一更新状态变量
254animateTo会将执行动画闭包前后的状态进行对比,对差异部分进行动画。为了对比,会在执行animateTo的动画闭包之前,将所有变更的状态变量和脏节点都刷新。
255如果多个animateTo之间存在状态更新,会导致执行下一个animateTo之前又存在需要更新的脏节点,可能造成冗余更新。
256
257反例:多个animateTo之间更新状态变量。
258
259![多个animateTo之间更新状态变量](figures/multi_animateto.png)
260
261以下代码在两个animateTo之间更新组件的其他状态。
262
263```typescript
264@Entry
265@Component
266struct MyComponent {
267  @State textWidth: number = 200;
268  @State textHeight: number = 50;
269  @State color: Color = Color.Red;
270
271  build() {
272    Column() {
273      Row()
274        .width(this.textWidth)
275        .height(10)
276        .backgroundColor(this.color)
277      Text('click')
278        .height(this.textHeight)
279        .onClick(() => {
280          this.textWidth = 100;
281          // textHeight是非动画属性
282          this.textHeight = 100;
283          animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
284            this.textWidth = 200;
285          });
286          this.color = Color.Yellow;
287          animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
288            this.color = Color.Red;
289          });
290        })
291    }
292    .width('100%')
293    .height('100%')
294  }
295}
296```
297
298在第一个animateTo前,重新设置了textWidth属性,所以Row组件需要更新一次。在第一个animateTo的动画闭包中,改变了textWidth属性,所以Row组件又需要更新一次并对比产生宽高动画。第二个animateTo前,重新设置了color属性,所以Row组件又需要更新一次。在第二个animateTo的动画闭包中,改变了color属性,所以Row组件再更新一次并产生了背景色动画。Row组件总共更新了4次属性。
299此外还更改了与动画无关的状态textHeight,如果不需要改变无关状态,则不应改变造成冗余更新。
300
301正例:统一更新状态变量。
302
303![统一更新状态变量1](figures/unify_animateto.png) 或 ![统一更新状态变量2](figures/unify_animateto_three_step.png)
304
305正例1:在animateTo之前使用原始状态,让动画从原始状态过渡到指定状态,这样也能避免动画在开始时发生跳变。
306
307```typescript
308@Entry
309@Component
310struct MyComponent {
311  @State textWidth: number = 100;
312  @State textHeight: number = 50;
313  @State color: Color = Color.Yellow;
314
315  build() {
316    Column() {
317      Row()
318        .width(this.textWidth)
319        .height(10)
320        .backgroundColor(this.color)
321      Text('click')
322        .height(this.textHeight)
323        .onClick(() => {
324          animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
325            this.textWidth = (this.textWidth === 100 ? 200 : 100);
326          });
327          animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
328            this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
329          });
330        })
331    }
332    .width('100%')
333    .height('100%')
334  }
335}
336```
337
338在第一个animateTo之前,不存在需要更新的脏状态变量和脏节点,无需更新。在第一个animateTo的动画闭包中,改变了textWidth属性,所以Row组件需要更新一次并对比产生宽高动画。在第二个animateTo之前,由于也没有执行额外的语句,不存在需要更新的脏状态变量和脏节点,无需更新。在第二个animateTo的动画闭包中,改变了color属性,所以Row组件再更新一次并产生了背景色动画。Row组件总共更新了2次属性。
339
340正例2:在animateTo之前显式的指定所有需要动画的属性初值,统一更新到节点中,然后再做动画。
341
342```typescript
343@Entry
344@Component
345struct MyComponent {
346  @State textWidth: number = 200;
347  @State textHeight: number = 50;
348  @State color: Color = Color.Red;
349
350  build() {
351    Column() {
352      Row()
353        .width(this.textWidth)
354        .height(10)
355        .backgroundColor(this.color)
356      Text('click')
357        .height(this.textHeight)
358        .onClick(() => {
359          this.textWidth = 100;
360          this.color = Color.Yellow;
361          animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
362            this.textWidth = 200;
363          });
364          animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
365            this.color = Color.Red;
366          });
367          this.textHeight = 100;
368        })
369    }
370    .width('100%')
371    .height('100%')
372  }
373}
374```
375
376在第一个animateTo之前,重新设置了textWidth和color属性,所以Row需要更新一次。在第一个animateTo的动画闭包中,改变了textWidth属性,所以Row组件需要更新一次并对比产生宽高动画。在第二个animateTo之前,由于没有执行额外的语句,不存在需要更新的脏状态变量和脏节点,无需更新。在第二个animateTo的动画闭包中,改变了color属性,所以Row组件再更新一次并产生了背景色动画。Row组件总共更新了3次属性。