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![输入图片说明](figures/layout-ui-view.png)
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![输入图片说明](figures/layout-relative-view.png)
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![输入图片说明](figures/layout-relative-introduce.png)
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![image-20240624191206151](figures/custom_component_node_1.png)
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![image-20240624192653146](figures/custom_component_node_2.png)
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![image-20240624195032415](figures/stack_normal.png)
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![image-20240624195635402](figures/stack_overlay.png)
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![image-20240624204921380](figures/mask_normal.png)
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![image-20240624205531083](figures/mask_grayscale.png)
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![image-20240628153525200](figures/color_normal.png)
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![image-20240628154436228](figures/color_measure.png)
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相关问题,同时也可以观察和了解不同组件之间的布局关系和属性。