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 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 或  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次属性。