1# 优化布局性能 2 3## 背景介绍 4 5应用开发中的用户界面(UI)布局是用户与应用程序交互的关键部分。使用不同类型的布局可以将页面排布的更加美观,但也容易带来不合理的布局。不合理的布局虽然能在界面显示上达到相同效果,但是过度的布局计算,界面嵌套带来了渲染和计算的大量开销,造成性能的衰退,本文重点介绍了几种常见的布局功能和适用场景,同时提供了几种优化布局结构的方法。 6 7## 常用布局 8 9布局是UI的必要元素,它定义了组件在界面中的位置。ArkUI框架提供了多种布局方式,除了基础的[线性布局](../ui/arkts-layout-development-linear.md)([Row](../reference/apis-arkui/arkui-ts/ts-container-row.md)/[Column](../reference/apis-arkui/arkui-ts/ts-container-column.md))、[层叠布局](../ui/arkts-layout-development-stack-layout.md)([Stack](../reference/apis-arkui/arkui-ts/ts-container-stack.md))、[弹性布局](../ui/arkts-layout-development-flex-layout.md)([Flex](../reference/apis-arkui/arkui-ts/ts-container-flex.md))、[相对布局](../ui/arkts-layout-development-relative-layout.md)([RelativeContainer](../reference/apis-arkui/arkui-ts/ts-container-relativecontainer.md))、[栅格布局](../ui/arkts-layout-development-grid-layout.md)([GridCol](../reference/apis-arkui/arkui-ts/ts-container-gridcol.md))外,也提供了相对复杂的[列表](../ui/arkts-layout-development-create-list.md)([List](../reference/apis-arkui/arkui-ts/ts-container-list.md))、[网格](../ui/arkts-layout-development-create-grid.md)([Grid](../reference/apis-arkui/arkui-ts/ts-container-grid.md)/[GridItem](../reference/apis-arkui/arkui-ts/ts-container-griditem.md))、[轮播](../ui/arkts-layout-development-create-looping.md)([Swiper](../reference/apis-arkui/arkui-ts/ts-container-swiper.md))。 10 11## 优化布局结构 12 13### 减少嵌套层级 14 15布局的嵌套层次过深会导致在创建节点及进行布局时耗费更多时间。因此开发者在开发时,应避免冗余的嵌套或者使用扁平化布局来优化嵌套层次。 16 17**避免冗余的嵌套** 18 19冗余的嵌套会带来不必要的组件节点,加深组件树的层级。例如,内部容器和外部容器是相同的布局方向,内部容器形成的布局效果可以用外部容器代替,对于这类冗余的容器,应该尽量优化,减少嵌套深度。 20 21反例: 22 23使用了Grid来实现一个网格,但在外层套了3层包含不同属性参数的Stack容器: 24 25```ts 26@Entry 27@Component 28struct AspectRatioExample12 { 29 @State children: Number[] = Array.from(Array<number>(900), (v, k) => k); 30 31 build() { 32 Scroll() { 33 Grid() { 34 ForEach(this.children, (item: Number[]) => { 35 GridItem() { 36 Stack() { 37 Stack() { 38 Stack() { 39 Text(item.toString()) 40 }.size({ width: "100%"}) 41 }.backgroundColor(Color.Yellow) 42 }.backgroundColor(Color.Pink) 43 } 44 }, (item: string) => item) 45 } 46 .columnsTemplate('1fr 1fr 1fr 1fr') 47 .columnsGap(0) 48 .rowsGap(0) 49 .size({ width: "100%", height: "100%" }) 50 } 51 } 52} 53 54``` 55 56通过查看组件树结构,发现三层Stack容器设置了不同的属性参数,可以使用GridItem的属性参数实现同样的UI效果。因此,三层Stack容器是冗余的容器,可以去掉,只留下GridItem作为组件节点。 57 58 59``` 60└─┬Scroll 61 └─┬Grid 62 ├─┬GridItem 63 │ └─┬Stack 64 │ └─┬Stack 65 │ └─┬Stack 66 │ └──Text 67 ├──GridItem 68 ├──GridItem 69``` 70 71正例: 72 73通过减少冗余的Stack容器嵌套,每个GridItem的组件数比上面少了3个: 74 75```ts 76@Entry 77@Component 78struct AspectRatioExample11 { 79 @State children: Number[] = Array.from(Array<number>(900), (v, k) => k); 80 81 build() { 82 Scroll() { 83 Grid() { 84 ForEach(this.children, (item: Number[]) => { 85 GridItem() { 86 Text(item.toString()) 87 }.backgroundColor(Color.Yellow) 88 }, (item: string) => item) 89 } 90 .columnsTemplate('1fr 1fr 1fr 1fr') 91 .columnsGap(0) 92 .rowsGap(0) 93 .size({ width: "100%", height: "100%" }) 94 } 95 } 96} 97``` 98 99通过查看该组件树层级结构如下: 100 101``` 102└─┬Scroll 103 └─┬Grid 104 ├─┬GridItem 105 │ └──Text 106 ├──GridItem 107 ├──GridItem 108``` 109 110**使用扁平化布局优化嵌套层级** 111 112开发者在实现自适应布局的时候,常使用Flex来达到弹性效果,这可能会造成多级嵌套。建议采用相对布局RelativeContainer进行扁平化布局,有效减少容器的嵌套层级,减少组件的创建时间。 113 114例如,以下是一个自适应的效果: 115 116 117 118反例: 119 120下述代码使用线性布局实现以上UI: 121 122```ts 123@Entry 124@Component 125struct MyComponent { 126 build() { 127 Row() { 128 Column() { 129 Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { 130 Text('张') 131 // 属性参数见正例 132 } 133 .width("40vp") 134 .height("40vp") 135 }.height("100%").justifyContent(FlexAlign.Center) 136 //body 137 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start }) { 138 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center }) { 139 Flex({ direction: FlexDirection.Row, 140 justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { 141 //Phone number or first name 142 Text('张三') 143 // 属性参数见正例 144 145 //Date Time 146 Text('2分钟前') 147 // 属性参数见正例 148 } 149 .width("100%").height(22) 150 151 Row() { 152 Text() { 153 //Content Abbreviations for Latest News 154 Span('Hello World'.replace(new RegExp("/[\r\n]/g"), " ")) 155 .fontSize("14fp") 156 .fontColor('# 66182431') 157 } 158 .maxLines(1) 159 .textOverflow({ overflow: TextOverflow.Ellipsis }) 160 } 161 .alignSelf(ItemAlign.Start) 162 .alignItems(VerticalAlign.Top) 163 .width("100%") 164 .height(19) 165 .margin({ top: "2vp" }) 166 }.width("100%") 167 .height("100%") 168 } 169 .layoutWeight(1) 170 .height("100%") 171 .padding({ left: "12vp" }) 172 } 173 .alignItems(VerticalAlign.Top) 174 .width("100%") 175 .height("100%") 176 } 177} 178``` 179 180通过查看该组件树层级结构如下: 181 182``` 183└─┬Row 184 ├──┬Column 185 │ └─┬Flex 186 │ └──Text 187 └─┬Flex 188 └─┬Flex 189 │ └─┬Flex 190 │ ├──Text 191 │ └──Text 192 └─┬Row 193 └──Text 194``` 195 196为了将4个元素放到合适的位置,开发者使用了11个组件,树深度为5,实际上是不合理的。 197 198分析元素之间的布局关系可以得到如下: 199 200 201 202正例: 203 204从上图得到一个明确的相对布局位置关系,该场景可以使用相对布局的形式来优化,具体代码实现如下: 205 206```ts 207@Entry 208@Component 209struct MyComponent { 210 build() { 211 Row() { 212 RelativeContainer() { 213 Text('张') 214 .fontSize('20.0vp') 215 .fontWeight(FontWeight.Bold) 216 .fontColor(Color.White) 217 .height('40vp') 218 .width('40vp') 219 .textAlign(TextAlign.Center) 220 .clip(new Circle({ width: '40vp', height: '40vp' })) 221 .backgroundColor(Color.Green) 222 .alignRules({ 223 center: { anchor: "__container__", align: VerticalAlign.Center }, 224 left: { anchor: "__container__", align: HorizontalAlign.Start } 225 }) 226 .id('head') 227 Text('张三') 228 .fontSize('16.0fp') 229 .textOverflow({ overflow: TextOverflow.Ellipsis }) 230 .fontColor('# ff182431') 231 .maxLines(1) 232 .fontWeight(FontWeight.Medium) 233 .padding({ left: '12vp' }) 234 .height(22) 235 .alignRules({ 236 top: { anchor: 'head', align: VerticalAlign.Top }, 237 left: { anchor: 'head', align: HorizontalAlign.End } 238 }) 239 .id('name') 240 Text('2分钟前') 241 .fontColor('# 66182431') 242 .fontSize('12fp') 243 .maxLines(1) 244 .height(22) 245 .alignRules({ 246 top: { anchor: 'head', align: VerticalAlign.Top }, 247 right: { anchor: '__container__', align: HorizontalAlign.End } 248 }) 249 .id("time") 250 Text() { 251 //Content Abbreviations for Latest News 252 Span('Hello World'.replace(new RegExp("/[\r\n]/g"), " ")) 253 .fontSize('14fp') 254 .fontColor('# 66182431') 255 } 256 .maxLines(1) 257 .textOverflow({ overflow: TextOverflow.Ellipsis }) 258 .width('100%') 259 .height(19) 260 .margin({ top: '2vp' }) 261 .padding({ left: '12vp' }) 262 .alignRules({ 263 top: { anchor: 'name', align: VerticalAlign.Bottom }, 264 left: { anchor: 'head', align: HorizontalAlign.End } 265 }) 266 .id('content') 267 } 268 .width('100%').height('100%') 269 .border({ width: 1, color: "# 6699FF" }) 270 } 271 .height('100%') 272 } 273} 274``` 275 276通过减少嵌套层数后可以发现,布局实现了相同的效果,但是组件层级减少了3层,使用组件数也减少了6个。 277 278``` 279└─┬RelativeContainer 280 ├──Text 281 ├──Text 282 ├──Text 283 └──Text 284``` 285 286从上述案例中可以看到,使用扁平化布局逻辑概念设计更清晰,避免使用不参与绘制的布局组件,优化性能并减少占用内存。这种将一棵深度很高的UI树,改造为将内容排布到同一个节点下的思路,为扁平化布局。如下图所示,采用扁平化布局去除了中间冗余的两层布局节点。 287 288 289 290使用扁平化布局推荐使用[RelativeContainer](../reference/apis-arkui/arkui-ts/ts-container-relativecontainer.md)、[绝对定位](../reference/apis-arkui/arkui-ts/ts-universal-attributes-location.md)、[Grid组件](../reference/apis-arkui/arkui-ts/ts-container-grid.md)等 291 292### 使用高性能布局组件 293 294**使用Column/Row替换Flex容器** 295 296如果使用Flex布局容器,只是为了实现横向或者纵向的布局。那直接使用Row、Column容器反而能够提升渲染性能。关于Flex带来的性能影响可以参考《[Flex布局性能提升使用指导](flex-development-performance-boost.md)》。 297 298使用Column、Row替换Flex容器组件避免二次渲染的案例见:《[性能提升的其他方法](arkts-performance-improvement-recommendation.md)》 299 300**适当减少使用if/else条件渲染** 301 302在ArkUI的build函数里,if/else也会被当成一个组件,在组件树上也是一个节点。对于一些在不同条件下展示不同效果的场景,基本布局不变,能够通过改变属性来进行控制界面变更的场景下,尽量减少if/else的方式来进行界面内容的切换,因为使用if/else不仅会增加一层节点,而且还有可能造成界面的重排与重绘。 303 304反例: 305 306下述代码中通过判断isVisible的值控制Image组件显示,这会导致在切换选择的过程中,不停创建和销毁Image组件元素。 307 308```ts 309@Entry 310@Component 311struct TopicItem { 312 @State isVisible : Boolean = true; 313 314 build() { 315 Stack() { 316 Column(){ 317 if (this.isVisible) { 318 Image($r('app.media.icon')).width('25%').height('12.5%') 319 Image($r('app.media.icon')).width('25%').height('12.5%') 320 Image($r('app.media.icon')).width('25%').height('12.5%') 321 Image($r('app.media.icon')).width('25%').height('12.5%') 322 } 323 } 324 Column() { 325 Row().width(300).height(200).backgroundColor(Color.Pink) 326 } 327 } 328 } 329} 330``` 331 332下图为isVisible值不同时组件树的情况: 333 334``` 335isVisible为true: 336└─┬Stack 337 ├─┬Column 338 │ ├──Image 339 │ ├──Image 340 │ ├──Image 341 │ └──Image 342 └─┬Column 343 └──Row 344 345isVisible为false: 346└─┬Stack 347 ├──Column 348 └─┬Column 349 └──Row 350``` 351 352正例: 353 354下面例子通过visibility属性来控制图片的显隐,避免if/else条件渲染可能带来的重排与重绘。 355 356```ts 357@Entry 358@Component 359struct TopicItem { 360 @State isVisible : Boolean = true; 361 362 build() { 363 Stack() { 364 Column(){ 365 Image($r('app.media.icon')) 366 .width('25%').height('12.5%').visibility(this.isVisible ? Visibility.Visible : Visibility.None) 367 Image($r('app.media.icon')) 368 .width('25%').height('12.5%').visibility(this.isVisible ? Visibility.Visible : Visibility.None) 369 Image($r('app.media.icon')) 370 .width('25%').height('12.5%').visibility(this.isVisible ? Visibility.Visible : Visibility.None) 371 Image($r('app.media.icon')) 372 .width('25%').height('12.5%').visibility(this.isVisible ? Visibility.Visible : Visibility.None) 373 } 374 Column() { 375 Row().width(300).height(200).backgroundColor(Color.Pink) 376 } 377 } 378 } 379} 380``` 381 382说明:上述情况考虑的是性能优先的场景,而当开发者优先考虑内存时,建议使用if/else控制图片的显隐。 383 384## 优化布局时间 385 386布局的嵌套层次过深会导致在创建节点及进行布局时耗费更多时间。因此开发者在开发时,应避免冗余的嵌套或者使用扁平化布局来优化嵌套层次。 387 388反例: 389使用线性布局,布局耗时166ms313us。 390 391```ts 392 393import measure from '@ohos.measure'; 394import prompt from '@ohos.prompt'; 395 396@Entry 397@Component 398struct PerformanceRelative { 399 @State message: string = 'Hello World' 400 @State textWidth: string = ""; 401 402 build() { 403 Column() { 404 Image($r("app.media.app_icon")).width("100%").height(300).margin({ bottom: 20 }) 405 Row() { 406 Blank() 407 Column() { 408 Image($r("app.media.app_icon")).margin({ bottom: 4 }).width(40).aspectRatio(1) 409 Text("Name") 410 }.margin({ left: 8, right: 8 }) 411 }.position({ y: 280 }).width("100%") 412 // Empty row 413 Row().height(this.textWidth) 414 Column() { 415 Row() { 416 Text("Singapore").fontSize(20).fontWeight(FontWeight.Bolder) 417 .margin(8) 418 .textAlign(TextAlign.Start) 419 }.width("100%").justifyContent(FlexAlign.Start) 420 421 Flex({ alignItems: ItemAlign.Center }) { 422 Text("Camera").flexShrink(0) 423 .margin({ right: 8 }) 424 TextInput() 425 }.margin(8) 426 427 Flex({ alignItems: ItemAlign.Center }) { 428 Text("Settings").flexShrink(0) 429 .margin({ right: 8 }) 430 TextInput() 431 }.margin(8) 432 433 Row() { 434 Column() { 435 Image($r("app.media.app_icon")).width(80).aspectRatio(1).margin({ bottom: 8 }) 436 Text("Description") 437 }.margin(8) 438 439 Column() { 440 Text("Title").fontWeight(FontWeight.Bold).margin({ bottom: 8 }) 441 Text("Long Text") 442 }.margin(8).layoutWeight(1).alignItems(HorizontalAlign.Start) 443 }.margin(8).width("100%").alignItems(VerticalAlign.Top) 444 }.layoutWeight(1) 445 446 Flex({ justifyContent: FlexAlign.End }) { 447 Button("Upload").margin(8) 448 Button("Discard").margin(8) 449 } 450 } 451 .width("100%").height("100%") 452 } 453} 454 455``` 456 457正例: 458使用相对布局上述界面,减少了组件的嵌套深度以及组件个数,布局耗时123ms278us。 459 460```ts 461 462@Entry 463@Component 464struct RelativePerformance { 465 @State message: string = 'Hello World' 466 467 build() { 468 RelativeContainer(){ 469 Image($r("app.media.app_icon")) 470 .height(300) 471 .width("100%") 472 .id("topImage") 473 .alignRules({ 474 left: { anchor: "__container__", align: HorizontalAlign.Start }, 475 top: {anchor: "__container__", align: VerticalAlign.Top } 476 }) 477 Image($r("app.media.app_icon")) 478 .width(40) 479 .aspectRatio(1) 480 .margin(4) 481 .id("topCornerImage") 482 .alignRules({ 483 right: { anchor: "__container__", align: HorizontalAlign.End }, 484 center: {anchor: "topImage", align: VerticalAlign.Bottom } 485 }) 486 Text("Name") 487 .id("name") 488 .margin(4) 489 .alignRules({ 490 left: { anchor: "topCornerImage", align: HorizontalAlign.Start }, 491 top: {anchor: "topCornerImage", align: VerticalAlign.Bottom } 492 }) 493 Text("Singapore") 494 .margin(8) 495 .fontWeight(FontWeight.Bolder) 496 .fontSize(20) 497 .id("singapore") 498 .alignRules({ 499 left: { anchor: "__container__", align: HorizontalAlign.Start }, 500 top: {anchor: "name", align: VerticalAlign.Bottom } 501 }) 502 Text("Camera") 503 .margin(8) 504 .id("camera") 505 .alignRules({ 506 left: { anchor: "__container__", align: HorizontalAlign.Start }, 507 top: {anchor: "singapore", align: VerticalAlign.Bottom } 508 }) 509 TextInput() 510 .id("cameraInput") 511 .alignRules({ 512 left: { anchor: "camera", align: HorizontalAlign.End }, 513 right:{ anchor: "__container__", align: HorizontalAlign.End }, 514 top: {anchor: "camera", align: VerticalAlign.Top }, 515 bottom: { anchor: "camera", align: VerticalAlign.Bottom } 516 }) 517 Text("Settings") 518 .margin(8) 519 .id("settings") 520 .alignRules({ 521 left: { anchor: "__container__", align: HorizontalAlign.Start }, 522 top: {anchor: "camera", align: VerticalAlign.Bottom } 523 }) 524 TextInput() 525 .id("settingInput") 526 .alignRules({ 527 left: { anchor: "settings", align: HorizontalAlign.End }, 528 right:{ anchor: "__container__", align: HorizontalAlign.End }, 529 top: {anchor: "settings", align: VerticalAlign.Top }, 530 bottom: { anchor: "settings", align: VerticalAlign.Bottom } 531 }) 532 Image($r("app.media.app_icon")) 533 .id("descriptionIcon") 534 .margin(8) 535 .width(80) 536 .aspectRatio(1) 537 .alignRules({ 538 left: { anchor: "__container__", align: HorizontalAlign.Start }, 539 top: {anchor: "settings", align: VerticalAlign.Bottom } 540 }) 541 Text("Description") 542 .id("description") 543 .margin(8) 544 .alignRules({ 545 left: { anchor: "__container__", align: HorizontalAlign.Start }, 546 top: {anchor: "descriptionIcon", align: VerticalAlign.Bottom } 547 }) 548 Text("Title") 549 .fontWeight(FontWeight.Bold) 550 .id("title") 551 .margin(8) 552 .alignRules({ 553 left: { anchor: "description", align: HorizontalAlign.End }, 554 top: {anchor: "descriptionIcon", align: VerticalAlign.Top } 555 }) 556 Text("Long Text") 557 .id("longText") 558 .margin(8) 559 .alignRules({ 560 left: { anchor: "description", align: HorizontalAlign.End }, 561 right: { anchor: "__container__", align: HorizontalAlign.End }, 562 top: {anchor: "title", align: VerticalAlign.Bottom } 563 }) 564 Button("Discard") 565 .id("discard") 566 .margin(8) 567 .alignRules({ 568 right: { anchor: "__container__", align: HorizontalAlign.End }, 569 bottom: {anchor: "__container__", align: VerticalAlign.Bottom } 570 }) 571 Button("Upload") 572 .id("upload") 573 .margin(8) 574 .alignRules({ 575 right: { anchor: "discard", align: HorizontalAlign.Start }, 576 bottom: {anchor: "__container__", align: VerticalAlign.Bottom } 577 }) 578 }.width("100%").height("100%") 579 } 580} 581 582``` 583 584## 应用节点数优化 585 586### 自定义组件引起新增节点 587 588自定义组件自身为非渲染节点,仅是组件树和状态数据的组合。因此常规使用自定义组件时并不会产生多余的节点。但是当自定义组件自身设置通用属性后,会作为一个整体节点进行处理。此时,在自定义组件内部会创建一个\__Common__节点,用于对内部的组件树进行操作,如背景色绘制、圆角绘制等指令,都会作用在该节点上,此时会出现多一个节点的问题。 589 590**反例** 591 592创建自定义组件NormalCustom,在页面中调用时添加全局属性height、width、backgroundColor。 593 594```ts 595@Entry 596@Component 597struct CustomComponentNormal { 598 build() { 599 Column() { 600 NormalCustom() 601 .height(100) 602 .width(100) 603 .backgroundColor(Color.Blue) 604 } 605 } 606} 607 608@Component 609struct NormalCustom { 610 build() { 611 Column() { 612 Text('Hello Word') 613 } 614 } 615} 616``` 617 618通过DevEco Studio内置ArkUI Inspector工具,查看组件树结构,会发现在自定义组件的外部,会多出一层\__Common__节点。如图1所示。通常情况下,可以通过将属性设置内移的方式减少这一层节点。但是在应用开发中,会遇到需要给整个自定义组件设置统一属性的需求,如果将所有的属性设置都内移,就会出现传递参数过多的问题,同时也会创建更多状态变量,并且增加参数的传递耗时。虽然降低了节点数量,但是却牺牲了性能。 619 620图1 常规设置自定义组件全局属性 621 622 623 624**正例** 625 626ArkUI提供了[动态属性设置(Modifier)](../reference/apis-arkui/arkui-ts/ts-universal-attributes-attribute-modifier.md)的接口,支持使用自定义Modifier构建组件并配置属性。 627 628```ts 629@Entry 630@Component 631struct CustomComponentModifier { 632 modifier: ColumnModifier = new ColumnModifier(); 633 634 aboutToAppear(): void { 635 this.modifier.width = 100; 636 this.modifier.height = 100; 637 this.modifier.backgroundColor = Color.Red; 638 } 639 640 build() { 641 Column() { 642 ModifierCustom({ modifier: this.modifier }) 643 } 644 } 645} 646 647@Component 648struct ModifierCustom { 649 @Require @Prop modifier: AttributeModifier<ColumnAttribute>; 650 651 build() { 652 Column() { 653 Text('Hello Word') 654 }.attributeModifier(this.modifier) 655 } 656} 657// 使用动态属性设置时,需要继承AttributeModifier,自行实现一个Modifier,然后设置到需要的组件上 658class ColumnModifier implements AttributeModifier<ColumnAttribute> { 659 width: number = 0; 660 height: number = 0; 661 backgroundColor: ResourceColor | undefined = undefined; 662 663 applyNormalAttribute(instance: ColumnAttribute): void { 664 instance.width(this.width); 665 instance.height(this.height); 666 instance.backgroundColor(this.backgroundColor); 667 } 668} 669``` 670 671通过DevEco Studio内置ArkUI Inspector工具,查看组件树结构,会发现在自定义组件的外部并没有多出\__Common__节点。如图2所示。 672 673图2 使用Modifier设置自定义组件全局属性 674 675 676 677 678 679### 布局嵌套场景 680 681在应用代码中,由于业务需求,经常会出现布局嵌套的情况。 682 683**反例** 684 685通常情况下,会使用Stack布局,将一个组件覆盖到另一个组件上。 686 687```ts 688@Entry 689@Component 690struct ComponentStackNormal { 691 build() { 692 Column() { 693 Stack() { 694 Image($r('app.media.image_1')) 695 .objectFit(ImageFit.Contain) 696 Text("This is overlayNode") 697 .fontSize(20) 698 .fontColor(Color.Black) 699 } 700 } 701 } 702} 703``` 704 705通过DevEco Studio内置ArkUI Inspector工具,查看组件树结构,如图3所示。 706 707图3 使用Stack实现遮罩效果 708 709 710 711**正例** 712 713ArkUI提供了overlay接口,可以直接给组件添加一个无交互、无动画场景的浮层,实现堆叠的效果。 714 715``` 716@Entry 717@Component 718struct ComponentStackOverlay { 719 @Builder 720 OverlayNode() { 721 Text("This is overlayNode").fontSize(20).fontColor(Color.Black) 722 } 723 724 build() { 725 Column() { 726 Image($r('app.media.image_1')) 727 .overlay(this.OverlayNode(), { align: Alignment.Center }) 728 .objectFit(ImageFit.Contain) 729 } 730 } 731} 732``` 733 734通过DevEco Studio内置ArkUI Inspector工具,查看组件树结构,如图4所示。和反例中的代码相比,虽然组件树层数相同,但是减少了Stack组件的创建,优化了性能。 735 736 737 738### 按压态效果 739 740**反例** 741 742应用为了实现按压遮罩效果,通常需要使用Stack,在组件上方增加遮罩层。 743 744```ts 745@Entry 746@Component 747struct MaskNormal { 748 build() { 749 Column() { 750 GrayScaleNormalCustom({ isGrayIcon: true }) 751 } 752 } 753} 754 755@Component 756struct GrayScaleNormalCustom { 757 @State isGrayIcon: boolean = true; 758 759 build() { 760 Stack() { 761 Column() 762 .width('90%') 763 .height(150) 764 .backgroundImage($r('app.media.image_1')) 765 766 Column() 767 .width('90%') 768 .height(150) 769 .backgroundColor(Color.Grey) 770 .opacity(this.isGrayIcon ? 0.5 : 0) 771 } 772 } 773} 774``` 775 776通过DevEco Studio内置ArkUI Inspector工具,查看组件树结构,如图5所示。 777 778图5 常规遮罩实现 779 780 781 782**正例** 783 784可以通过组件效果实现遮罩,而非直接通过组件实现。 785 786```ts 787@Entry 788@Component 789struct MaskGrayScale { 790 build() { 791 Column() { 792 GrayScaleCustom({ isGrayIcon: true }) 793 } 794 } 795} 796 797@Component 798struct GrayScaleCustom { 799 @State isGrayIcon: boolean = true; 800 801 build() { 802 Column() 803 .width('90%') 804 .height(150) 805 .backgroundImage($r('app.media.image_1')) 806 .grayscale(this.isGrayIcon ? 0.5 : 0) 807 } 808} 809``` 810 811通过DevEco Studio内置ArkUI Inspector工具,查看组件树结构,如图6所示,可以减少一层Stack组件的创建。 812 813图6 通过组件效果实现遮罩 814 815 816 817### 颜色叠加效果 818 819在应用开发中,有使用到颜色叠加显示的需求。通常情况下,会通过将2个组件放在Stack中叠加的方式实现,这样不仅会多出一层布局节点,还会因为绘制了两个除颜色外其他属性都相同的组件导致了重复绘制。 820 821**反例** 822 823``` 824@Component 825struct ColorNormal { 826 @Prop isSelected: boolean = false; 827 828 build() { 829 Stack() { 830 Column() 831 .width('100%') 832 .height(100) 833 .backgroundColor(this.isSelected ? Color.Blue : Color.Grey) 834 .borderRadius(12) 835 .alignItems(HorizontalAlign.Center) 836 .justifyContent(FlexAlign.Center) 837 Column() 838 .width('100%') 839 .height(100) 840 .backgroundColor(this.isSelected ? "#99000000" : Color.Grey) 841 .borderRadius(12) 842 .alignItems(HorizontalAlign.Center) 843 .justifyContent(FlexAlign.Center) 844 } 845 } 846} 847 848@Entry 849@Component 850struct Index { 851 @State isSelected: boolean = false; 852 853 build() { 854 Scroll() { 855 Column() { 856 ColorNormal({ isSelected: this.isSelected }) 857 .onClick(() => { 858 this.isSelected = !this.isSelected; 859 }) 860 } 861 } 862 } 863} 864``` 865 866通过DevEco Studio内置ArkUI Inspector工具,查看组件树结构,如图7所示。 867 868图7 869 870 871 872**正例** 873 874系统中提供了颜色计算的API,可以通过计算的方式,将两个颜色合并为一个,省去Stack层的布局节点,并且可以少绘制一个组件,减少重复绘制的情况发生。 875 876``` 877@Component 878struct ColorMeasure { 879 @Prop isSelected: boolean = false; 880 881 build() { 882 Column() 883 .width('100%') 884 .height(100) 885 .backgroundColor(this.isSelected ? this.getBlendColor(Color.Blue, "#99000000").color : Color.Grey) 886 .borderRadius(12) 887 .alignItems(HorizontalAlign.Center) 888 .justifyContent(FlexAlign.Center) 889 } 890 891 getBlendColor(baseColor: ResourceColor, addColor: ResourceColor): ColorMetrics { 892 let sourceColor: ColorMetrics; 893 try { 894 sourceColor = ColorMetrics.resourceColor(baseColor).blendColor(ColorMetrics.resourceColor(addColor)); 895 } catch (error) { 896 console.log("getBlendColor failed, code = " + (error as BusinessError).code + ", message = " + 897 (error as BusinessError).message); 898 sourceColor = ColorMetrics.resourceColor(addColor); 899 } 900 return sourceColor; 901 } 902} 903 904@Entry 905@Component 906struct Index { 907 @State isSelected: boolean = false; 908 909 build() { 910 Scroll() { 911 Column() { 912 ColorMeasure({ isSelected: this.isSelected }) 913 .onClick(() => { 914 this.isSelected = !this.isSelected; 915 }) 916 } 917 } 918 } 919} 920``` 921 922通过DevEco Studio内置ArkUI Inspector工具,查看组件树结构,如图8所示。 923 924图8 925 926 927 928## 优化布局工具介绍 929 930[DevEco Studio](../quick-start/deveco-studio-user-guide-for-openharmony.md)内置ArkUI Inspector工具,开发者可以使用ArkUI 931Inspector,在DevEco Studio上查看应用在真机上的UI显示效果。利用ArkUI Inspector工具,开发者可以快速定位布局不理想或其他UI相关问题,同时也可以观察和了解不同组件之间的布局关系和属性。