1# 典型布局场景
2
3
4虽然不同应用的页面千变万化,但对其进行拆分和分析,页面中的很多布局场景是相似的。本小节将介绍如何借助自适应布局、响应式布局以及常见的容器类组件,实现应用中的典型布局场景。
5
6
7| 布局场景 | 实现方案 |
8| -------- | -------- |
9| [页签栏](#页签栏) | Tab组件 + 响应式布局 |
10| [运营横幅(Banner)](#运营横幅banner) | Swiper组件 + 响应式布局 |
11| [网格](#网格) | Grid组件 / List组件 + 响应式布局 |
12| [侧边栏](#侧边栏) | SideBar组件 + 响应式布局 |
13| [单/双栏](#单双栏) | Navigation组件 + 响应式布局 |
14| [三分栏](#三分栏) | SideBar组件 + Navigation组件 + 响应式布局 |
15| [自定义弹窗](#自定义弹窗) | CustomDialogController组件 + 响应式布局 |
16| [大图浏览](#大图浏览) | Image组件 |
17| [操作入口](#操作入口) | Scroll组件+Row组件横向均分 |
18| [顶部](#顶部) | 栅格组件 |
19| [缩进布局](#缩进布局) | 栅格组件 |
20| [挪移布局](#挪移布局) | 栅格组件 |
21| [重复布局](#重复布局) | 栅格组件 |
22
23
24> **说明:**
25> 在本文[媒体查询](responsive-layout.md#媒体查询)小节中已经介绍了如何通过媒体查询监听断点变化,后续的示例中不再重复介绍此部分代码。
26
27
28## 页签栏
29
30**布局效果**
31
32| sm | md | lg |
33| -------- | -------- | -------- |
34| 页签在底部<br/>页签的图标和文字垂直布局<br/>页签宽度均分<br/>页签高度固定72vp | 页签在底部<br/>页签的图标和文字水平布局<br/>页签宽度均分<br/>页签高度固定56vp | 页签在左边<br/>页签的图标和文字垂直布局<br/>页签宽度固定96vp<br/>页签高度总占比‘60%’后均分 |
35| ![页签布局](figures/页签布局sm.png) | ![页签布局](figures/页签布局md.png) | ![页签布局](figures/页签布局lg.png) |
36
37
38**实现方案**
39
40不同断点下,页签在页面中的位置及尺寸都有差异,可以结合响应式布局能力,设置不同断点下[Tab组件](../../reference/apis-arkui/arkui-ts/ts-container-tabs.md)的barPosition、vertical、barWidth和barHeight属性实现目标效果。
41
42另外,页签栏中的文字和图片的相对位置不同,同样可以通过设置不同断点下[tabBar](../../reference/apis-arkui/arkui-ts/ts-container-tabcontent.md#属性)对应的CustomBuilder中的布局方向,实现目标效果。
43
44
45**参考代码**
46
47
48```ts
49import { BreakpointSystem, BreakpointState } from '../common/breakpointsystem'
50
51interface TabBar  {
52  name: string
53  icon: Resource
54  selectIcon: Resource
55}
56interface marginGenerate {
57  top: number,
58  left?:number
59}
60
61@Entry
62@Component
63struct Home {
64  @State currentIndex: number = 0
65  @State tabs: Array<TabBar> = [{
66                                  name: '首页',
67                                  icon: $r('app.media.ic_music_home'),
68                                  selectIcon: $r('app.media.ic_music_home_selected')
69                                }, {
70                                  name: '排行榜',
71                                  icon: $r('app.media.ic_music_ranking'),
72                                  selectIcon: $r('app.media.ic_music_ranking_selected')
73                                }, {
74                                  name: '我的',
75                                  icon: $r('app.media.ic_music_me_nor'),
76                                  selectIcon: $r('app.media.ic_music_me_selected')
77                                }]
78  @State compStr: BreakpointState<string> = BreakpointState.of({ sm: "sm", md: "md", lg: "lg" })
79  @State compDirection: BreakpointState<FlexDirection> = BreakpointState.of({
80    sm: FlexDirection.Column,
81    md: FlexDirection.Row,
82    lg: FlexDirection.Column
83  });
84  @State compBarPose: BreakpointState<BarPosition> = BreakpointState.of({
85    sm: BarPosition.End,
86    md: BarPosition.End,
87    lg: BarPosition.Start
88  });
89  @State compVertical: BreakpointState<boolean> = BreakpointState.of({
90    sm: false,
91    md: false,
92    lg: true
93  });
94  @State compBarWidth: BreakpointState<string> = BreakpointState.of({
95    sm: '100%', md: '100%', lg: '96vp'
96  });
97  @State compBarHeight: BreakpointState<string> = BreakpointState.of({
98    sm: '72vp', md: '56vp', lg: '60%'
99  });
100  @State compMargin: BreakpointState<marginGenerate> = BreakpointState.of({
101    sm: ({ top: 4 } as marginGenerate),
102    md: ({ left: 8 } as marginGenerate),
103    lg: ({ top: 4 } as marginGenerate)
104  });
105
106  @Builder TabBarBuilder(index: number, tabBar: TabBar) {
107    Flex({
108      direction: this.compDirection.value,
109      justifyContent: FlexAlign.Center,
110      alignItems: ItemAlign.Center
111    }) {
112      Image(this.currentIndex === index ? tabBar.selectIcon : tabBar.icon)
113        .size({ width: 36, height: 36 })
114      Text(tabBar.name)
115        .fontColor(this.currentIndex === index ? '#FF1948' : '#999')
116        .margin(this.compMargin.value)
117        .fontSize(16)
118    }
119    .width('100%')
120    .height('100%')
121  }
122  aboutToAppear() {
123    BreakpointSystem.getInstance().attach(this.compStr)
124    BreakpointSystem.getInstance().attach(this.compDirection)
125    BreakpointSystem.getInstance().attach(this.compBarPose)
126    BreakpointSystem.getInstance().attach(this.compVertical)
127    BreakpointSystem.getInstance().attach(this.compBarWidth)
128    BreakpointSystem.getInstance().attach(this.compBarHeight)
129    BreakpointSystem.getInstance().attach(this.compMargin)
130    BreakpointSystem.getInstance().start()
131  }
132
133  aboutToDisappear() {
134    BreakpointSystem.getInstance().detach(this.compStr)
135    BreakpointSystem.getInstance().detach(this.compDirection)
136    BreakpointSystem.getInstance().detach(this.compBarPose)
137    BreakpointSystem.getInstance().detach(this.compVertical)
138    BreakpointSystem.getInstance().detach(this.compBarWidth)
139    BreakpointSystem.getInstance().detach(this.compBarHeight)
140    BreakpointSystem.getInstance().detach(this.compMargin)
141    BreakpointSystem.getInstance().stop()
142  }
143
144  build() {
145    Tabs({
146      barPosition:this.compBarPose.value
147    }) {
148      ForEach(this.tabs, (item:TabBar, index) => {
149        TabContent() {
150          Stack() {
151            Text(item.name).fontSize(30)
152          }.width('100%').height('100%')
153        }.tabBar(this.TabBarBuilder(index!, item))
154      })
155    }
156    .vertical(this.compVertical.value)
157    .barWidth(this.compBarWidth.value)
158    .barHeight(this.compBarHeight.value)
159    .animationDuration(0)
160    .onChange((index: number) => {
161      this.currentIndex = index
162    })
163  }
164}
165```
166
167
168## 运营横幅(Banner)
169
170**布局效果**
171
172| sm | md | lg |
173| -------- | -------- | -------- |
174| 展示一个内容项 | 展示两个内容项 | 展示三个内容项 |
175| ![banner_sm](figures/banner_sm.png) | ![banner_md](figures/banner_md.png) | ![banner_lg](figures/banner_lg.png) |
176
177**实现方案**
178
179运营横幅通常使用[Swiper组件](../../reference/apis-arkui/arkui-ts/ts-container-swiper.md)实现。不同断点下,运营横幅中展示的图片数量不同。只需要结合响应式布局,配置不同断点下Swiper组件的displayCount属性,即可实现目标效果。
180
181**参考代码**
182
183
184```ts
185import { BreakpointSystem, BreakPointType } from '../common/breakpointsystem'
186
187@Entry
188@Component
189export default struct Banner {
190  private data: Array<Resource> = [
191    $r('app.media.banner1'),
192    $r('app.media.banner2'),
193    $r('app.media.banner3'),
194    $r('app.media.banner4'),
195    $r('app.media.banner5'),
196    $r('app.media.banner6'),
197  ]
198  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
199  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
200
201  aboutToAppear() {
202    this.breakpointSystem.register()
203  }
204
205  aboutToDisappear() {
206    this.breakpointSystem.unregister()
207  }
208
209  build() {
210    Swiper() {
211      ForEach(this.data, (item:Resource) => {
212        Image(item)
213          .size({ width: '100%', height: 200 })
214          .borderRadius(12)
215          .padding(8)
216      })
217    }
218    .indicator(new BreakPointType({ sm: true, md: false, lg: false }).getValue(this.currentBreakpoint)!)
219    .displayCount(new BreakPointType({ sm: 1, md: 2, lg: 3 }).getValue(this.currentBreakpoint)!)
220  }
221}
222```
223
224
225## 网格
226
227**布局效果**
228
229| sm | md | lg |
230| -------- | -------- | -------- |
231| 展示两列 | 展示四列 | 展示六列 |
232| ![多列列表sm](figures/多列列表sm.png) | ![多列列表md](figures/多列列表md.png) | ![多列列表lg](figures/多列列表lg.png) |
233
234
235**实现方案**
236
237不同断点下,页面中图片的排布不同,此场景可以通过响应式布局能力结合[Grid组件](../../reference/apis-arkui/arkui-ts/ts-container-grid.md)实现,通过调整不同断点下的Grid组件的columnsTemplate属性即可实现目标效果。
238
239另外,由于本例中各列的宽度相同,也可以通过响应式布局能力结合[List组件](../../reference/apis-arkui/arkui-ts/ts-container-list.md)实现,通过调整不同断点下的List组件的lanes属性也可实现目标效果。
240
241
242**参考代码**
243
244通过Grid组件实现
245
246
247```ts
248import { BreakpointSystem, BreakPointType } from '../common/breakpointsystem'
249
250interface GridItemInfo {
251  name: string
252  image: Resource
253}
254
255@Entry
256@Component
257struct MultiLaneList {
258  private data: GridItemInfo[] = [
259    { name: '歌单集合1', image: $r('app.media.1') },
260    { name: '歌单集合2', image: $r('app.media.2') },
261    { name: '歌单集合3', image: $r('app.media.3') },
262    { name: '歌单集合4', image: $r('app.media.4') },
263    { name: '歌单集合5', image: $r('app.media.5') },
264    { name: '歌单集合6', image: $r('app.media.6') },
265    { name: '歌单集合7', image: $r('app.media.7') },
266    { name: '歌单集合8', image: $r('app.media.8') },
267    { name: '歌单集合9', image: $r('app.media.9') },
268    { name: '歌单集合10', image: $r('app.media.10') },
269    { name: '歌单集合11', image: $r('app.media.11') },
270    { name: '歌单集合12', image: $r('app.media.12') }
271  ]
272  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
273  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
274
275  aboutToAppear() {
276    this.breakpointSystem.register()
277  }
278
279  aboutToDisappear() {
280    this.breakpointSystem.unregister()
281  }
282
283  build() {
284    Grid() {
285      ForEach(this.data, (item: GridItemInfo) => {
286        GridItem() {
287          Column() {
288            Image(item.image)
289              .aspectRatio(1.8)
290            Text(item.name)
291              .margin({ top: 8 })
292              .fontSize(20)
293          }.padding(4)
294        }
295      })
296    }
297    .columnsTemplate(new BreakPointType({
298      sm: '1fr 1fr',
299      md: '1fr 1fr 1fr 1fr',
300      lg: '1fr 1fr 1fr 1fr 1fr 1fr'
301    }).getValue(this.currentBreakpoint)!)
302  }
303}
304```
305
306通过List组件实现
307
308
309```ts
310import { BreakpointSystem, BreakPointType } from '../common/breakpointsystem'
311
312interface ListItemInfo {
313  name: string
314  image: Resource
315}
316
317@Entry
318@Component
319struct MultiLaneList {
320  private data: ListItemInfo[] = [
321    { name: '歌单集合1', image: $r('app.media.1') },
322    { name: '歌单集合2', image: $r('app.media.2') },
323    { name: '歌单集合3', image: $r('app.media.3') },
324    { name: '歌单集合4', image: $r('app.media.4') },
325    { name: '歌单集合5', image: $r('app.media.5') },
326    { name: '歌单集合6', image: $r('app.media.6') },
327    { name: '歌单集合7', image: $r('app.media.7') },
328    { name: '歌单集合8', image: $r('app.media.8') },
329    { name: '歌单集合9', image: $r('app.media.9') },
330    { name: '歌单集合10', image: $r('app.media.10') },
331    { name: '歌单集合11', image: $r('app.media.11') },
332    { name: '歌单集合12', image: $r('app.media.12') }
333  ]
334  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
335  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
336
337  aboutToAppear() {
338    this.breakpointSystem.register()
339  }
340
341  aboutToDisappear() {
342    this.breakpointSystem.unregister()
343  }
344
345  build() {
346    List() {
347      ForEach(this.data, (item: ListItemInfo) => {
348        ListItem() {
349          Column() {
350            Image(item.image)
351            Text(item.name)
352              .margin({ top: 8 })
353              .fontSize(20)
354          }.padding(4)
355        }
356      })
357    }
358    .lanes(new BreakPointType({ sm: 2, md: 4, lg: 6 }).getValue(this.currentBreakpoint)!)
359    .width('100%')
360  }
361}
362```
363
364
365## 侧边栏
366
367**布局效果**
368
369| sm | md | lg |
370| -------- | -------- | -------- |
371| 默认隐藏侧边栏,同时提供侧边栏控制按钮,用户可以通过按钮控制侧边栏显示或隐藏。 | 始终显示侧边栏,不提供控制按钮,用户无法隐藏侧边栏。 | 始终显示侧边栏,不提供控制按钮,用户无法隐藏侧边栏。 |
372| ![侧边栏sm](figures/侧边栏sm.png) | ![侧边栏md](figures/侧边栏md.png) | ![侧边栏lg](figures/侧边栏lg.png) |
373
374**实现方案**
375
376侧边栏通常通过[SideBarContainer组件](../../reference/apis-arkui/arkui-ts/ts-container-sidebarcontainer.md)实现,结合响应式布局能力,在不同断点下为SideBarConContainer组件的sideBarWidth、showControlButton等属性配置不同的值,即可实现目标效果。
377
378**参考代码**
379
380
381```ts
382import { BreakpointSystem, BreakPointType } from '../common/breakpointsystem'
383
384interface imagesInfo{
385  label:string,
386  imageSrc:Resource
387}
388const images:imagesInfo[]=[
389  {
390    label:'moon',
391    imageSrc:$r('app.media.my_image_moon')
392  },
393  {
394    label:'sun',
395    imageSrc:$r('app.media.my_image')
396  }
397]
398
399@Entry
400@Component
401struct SideBarSample {
402  @StorageLink('currentBreakpoint') private currentBreakpoint: string = "md";
403  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
404  @State selectIndex: number = 0;
405  @State showSideBar:boolean=false;
406
407  aboutToAppear() {
408    this.breakpointSystem.register()
409  }
410
411  aboutToDisappear() {
412    this.breakpointSystem.unregister()
413  }
414
415  @Builder itemBuilder(index: number) {
416    Text(images[index].label)
417      .fontSize(24)
418      .fontWeight(FontWeight.Bold)
419      .borderRadius(5)
420      .margin(20)
421      .backgroundColor('#ffffff')
422      .textAlign(TextAlign.Center)
423      .width(180)
424      .height(36)
425      .onClick(() => {
426        this.selectIndex = index;
427        if(this.currentBreakpoint === 'sm'){
428          this.showSideBar=false
429        }
430      })
431  }
432
433  build() {
434    SideBarContainer(this.currentBreakpoint === 'sm' ? SideBarContainerType.Overlay : SideBarContainerType.Embed) {
435      Column() {
436        this.itemBuilder(0)
437        this.itemBuilder(1)
438      }.backgroundColor('#F1F3F5')
439      .justifyContent(FlexAlign.Center)
440
441      Column() {
442        Image(images[this.selectIndex].imageSrc)
443          .objectFit(ImageFit.Contain)
444          .height(300)
445          .width(300)
446      }
447      .justifyContent(FlexAlign.Center)
448      .width('100%')
449      .height('100%')
450    }
451    .height('100%')
452    .sideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
453    .minSideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
454    .maxSideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
455    .showControlButton(this.currentBreakpoint === 'sm')
456    .autoHide(false)
457    .showSideBar(this.currentBreakpoint !== 'sm'||this.showSideBar)
458    .onChange((isBarShow: boolean) => {
459      if(this.currentBreakpoint === 'sm'){
460          this.showSideBar=isBarShow
461        }
462    })
463  }
464}
465```
466
467## 单/双栏
468
469**布局效果**
470
471| sm                                                           | md                                               | lg                                               |
472| ------------------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ |
473| 单栏显示,在首页中点击选项可以显示详情。<br>点击详情上方的返回键图标或使用系统返回键可以返回到主页。 | 双栏显示,点击左侧不同的选项可以刷新右侧的显示。 | 双栏显示,点击左侧不同的选项可以刷新右侧的显示。 |
474| ![](figures/navigation_sm.png)                               | ![](figures/navigation_md.png)                   | ![](figures/navigation_lg.png)                   |
475
476**实现方案**
477
478单/双栏场景可以使用[Navigation组件](../../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md)实现,Navigation组件可以根据窗口宽度自动切换单/双栏显示,减少开发工作量。
479
480**参考代码**
481
482```ts
483
484// 工程配置文件module.json5中配置 {"routerMap": "$profile:route_map"}
485// route_map.json
486{
487  "routerMap": [
488    {
489      "name": "Moon",
490      "pageSourceFile": "src/main/ets/pages/Moon.ets",
491      "buildFunction": "MoonBuilder",
492      "data": {
493        "description": "this is Moon"
494      }
495    },
496    {
497      "name": "Sun",
498      "pageSourceFile": "src/main/ets/pages/Sun.ets",
499      "buildFunction": "SunBuilder"
500    }
501  ]
502}
503// Moon.ets
504@Builder
505export function MoonBuilder(name: string, param: Object) {
506  Moon()
507}
508@Component
509export struct Moon {
510  private imageSrc: Resource=$r('app.media.my_image_moon')
511  private label: string='moon'
512  build() {
513    Column(){
514    NavDestination(){
515    Column() {
516      Image(this.imageSrc)
517        .objectFit(ImageFit.Contain)
518        .height(300)
519        .width(300)
520    }
521    .justifyContent(FlexAlign.Center)
522    .width('100%')
523    .height('100%')
524     }.title(this.label)
525     }
526  }
527}
528// Sun.ets
529@Builder
530export function SunBuilder(name: string, param: Object) {
531  Sun()
532}
533@Component
534export struct Sun {
535  private imageSrc: Resource=$r('app.media.my_image')
536  private label: string='Sun'
537  build() {
538    Column(){
539    NavDestination(){
540    Column() {
541      Image(this.imageSrc)
542        .objectFit(ImageFit.Contain)
543        .height(300)
544        .width(300)
545    }
546    .justifyContent(FlexAlign.Center)
547    .width('100%')
548    .height('100%')
549     }.title(this.label)
550     }
551  }
552}
553//NavigationSample.ets
554interface arrSample{
555  label:string,
556  pagePath:string
557}
558
559@Entry
560@Component
561struct NavigationSample {
562  pageInfos: NavPathStack = new NavPathStack();
563  private arr:arrSample[]=[
564    {
565      label:'moon',
566      pagePath:'Moon'
567    },
568    {
569      label:'sun',
570      pagePath:'Sun'
571    }
572  ]
573  build() {
574    Navigation(this.pageInfos) {
575      Column({space: 30}) {
576      ForEach(this.arr, (item: arrSample) => {
577         Text(item.label)
578        .fontSize(24)
579        .fontWeight(FontWeight.Bold)
580        .borderRadius(5)
581        .backgroundColor('#FFFFFF')
582        .textAlign(TextAlign.Center)
583        .width(180)
584        .height(36)
585        .onClick(()=>{
586          this.pageInfos.clear();
587          this.pageInfos.pushPath({name:item.pagePath})
588        })
589        })
590      }
591      .justifyContent(FlexAlign.Center)
592      .height('100%')
593      .width('100%')
594    }
595    .mode(NavigationMode.Auto)
596    .backgroundColor('#F1F3F5')
597    .height('100%')
598    .width('100%')
599    .navBarWidth(360)
600    .hideToolBar(true)
601    .title('Sample')
602  }
603}
604```
605
606
607
608## 三分栏
609
610**布局效果**
611
612| sm                                           | md                                      | lg                                      |
613| -------------------------------------------- | --------------------------------------- | --------------------------------------- |
614| 单栏显示。<br> 点击侧边栏控制按钮控制侧边栏的显示/隐藏。<br> 点击首页的选项可以进入到内容区,内容区点击返回按钮可返回首页。| 双栏显示。<br> 点击侧边栏控制按钮控制侧边栏的显示/隐藏。<br> 点击左侧导航区不同的选项可以刷新右侧内容区的显示。 | 三分栏显示。<br> 点击侧边栏控制按钮控制侧边栏的显示/隐藏,来回切换二分/三分栏显示。<br> 点击左侧导航区不同的选项可以刷新右侧内容区的显示。<br> 窗口宽度变化时,优先变化右侧内容区的宽度大小。 |
615| ![](figures/tripleColumn_sm.png)            | ![](figures/tripleColumn_md.png)       | ![](figures/tripleColumn_lg.png)       |
616| ![](figures/TripleColumn.gif)
617
618**场景说明**
619
620为充分利用设备的屏幕尺寸优势,应用在大屏设备上常常有二分栏或三分栏的设计,即“A+C”,“B+C”或“A+B+C”的组合,其中A是侧边导航区,B是列表导航区,C是内容区。在用户动态改变窗口宽度时,当窗口宽度大于或等于840vp时页面呈现A+B+C三列,放大缩小优先变化C列;当窗口宽度小于840vp大于等于600vp时呈现B+C列,放大缩小时优先变化C列;当窗口宽度小于600vp大于等于360vp时,仅呈现C列。
621
622**实现方案**
623
624三分栏场景可以组合使用[SideBarContainer](../../reference/apis-arkui/arkui-ts/ts-container-sidebarcontainer.md)组件与[Navigation组件](../../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md)实现,SideBarContainer组件可以通过侧边栏控制按钮控制显示/隐藏,Navigation组件可以根据窗口宽度自动切换该组件内单/双栏显示,结合响应式布局能力,在不同断点下为SideBarConContainer组件的minContentWidth属性配置不同的值,即可实现目标效果。设置minContentWidth属性的值可以通过[断点](../multi-device-app-dev/responsive-layout.md#断点)监听窗口尺寸变化的同时设置不同的值并储存成一个全局对象。
625
626**参考代码**
627
628```ts
629
630// 工程配置文件module.json5中配置 {"routerMap": "$profile:route_map"}
631// route_map.json
632{
633  "routerMap": [
634    {
635      "name": "B1Page",
636      "pageSourceFile": "src/main/ets/pages/B1Page.ets",
637      "buildFunction": "B1PageBuilder",
638      "data": {
639        "description": "this is B1Page"
640      }
641    },
642    {
643      "name": "B2Page",
644      "pageSourceFile": "src/main/ets/pages/B2Page.ets",
645      "buildFunction": "B2PageBuilder"
646    }
647  ]
648}
649// EntryAbility.ts
650import { window, display } from '@kit.ArkUI'
651import { Ability,UIAbility } from '@kit.AbilityKit'
652
653export default class EntryAbility extends UIAbility {
654  private windowObj?: window.Window
655  private curBp?: string
656  private myWidth?: number
657  // ...
658  // 根据当前窗口尺寸更新断点
659  private updateBreakpoint(windowWidth:number) :void{
660    // 将长度的单位由px换算为vp
661    let windowWidthVp = windowWidth / (display.getDefaultDisplaySync().densityDPI / 160)
662    let newBp: string = ''
663    let newWd: number
664    if (windowWidthVp < 320) {
665      newBp = 'xs'
666      newWd = 360
667    } else if (windowWidthVp < 600) {
668      newBp = 'sm'
669      newWd = 360
670    } else if (windowWidthVp < 840) {
671      newBp = 'md'
672      newWd = 600
673    } else {
674      newBp = 'lg'
675      newWd = 600
676    }
677    if (this.curBp !== newBp) {
678      this.curBp = newBp
679      this.myWidth = newWd
680      // 使用状态变量记录当前断点值
681      AppStorage.setOrCreate('currentBreakpoint', this.curBp)
682      // 使用状态变量记录当前minContentWidth值
683      AppStorage.setOrCreate('myWidth', this.myWidth)
684    }
685  }
686
687  onWindowStageCreate(windowStage: window.WindowStage) :void{
688    windowStage.getMainWindow().then((windowObj) => {
689      this.windowObj = windowObj
690      // 获取应用启动时的窗口尺寸
691      this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width)
692      // 注册回调函数,监听窗口尺寸变化
693      windowObj.on('windowSizeChange', (windowSize)=>{
694        this.updateBreakpoint(windowSize.width)
695      })
696    });
697   // ...应用启动页面
698   windowStage.loadContent('pages/Index', (err) => {
699      if (err.code) {
700        return;
701      }
702    });
703  }
704
705  // 窗口销毁时,取消窗口尺寸变化监听
706  onWindowStageDestroy() :void {
707    if (this.windowObj) {
708      this.windowObj.off('windowSizeChange')
709    }
710  }
711  //...
712}
713
714
715// B1Page.ets
716@Builder
717export function B1PageBuilder() {
718  B1Page()
719}
720@Component
721export struct B1Page {
722  private imageSrc: Resource = $r('app.media.startIcon');
723  private label: string = "B1"
724  build() {
725    Column() {
726      NavDestination() {
727        Column() {
728          Image(this.imageSrc)
729            .objectFit(ImageFit.Contain)
730            .height(300)
731            .width(300)
732        }
733        .justifyContent(FlexAlign.Center)
734        .width('100%')
735        .height('100%')
736      }.title(this.label)
737    }
738  }
739}
740// B2Page.ets
741@Builder
742export function B2PageBuilder() {
743  B2Page()
744}
745@Component
746export struct B2Page {
747  private imageSrc: Resource = $r('app.media.startIcon');
748  private label: string = "B2"
749  build() {
750    Column() {
751      NavDestination() {
752        Column() {
753          Image(this.imageSrc)
754            .objectFit(ImageFit.Contain)
755            .height(300)
756            .width(300)
757        }
758        .justifyContent(FlexAlign.Center)
759        .width('100%')
760        .height('100%')
761      }.title(this.label)
762    }
763  }
764}
765
766//TripleColumnSample.ets
767interface arrSampleObj{
768  label:string,
769  pagePath:string
770}
771@Entry
772@Component
773struct TripleColumnSample {
774  @State arr: number[] = [1, 2, 3];
775  @StorageProp('myWidth') myWidth: number = 360;
776  pageInfos:NavPathStack = new NavPathStack();
777  @State arrSample: arrSampleObj[] = [
778    {
779        label:'B1',
780        pagePath:'B1Page'
781    },
782    {
783        label:'B2',
784        pagePath:'B2Page'
785    }
786  ];
787
788  @Builder NavigationTitle() {
789    Column() {
790      Text('Sample')
791        .fontColor('#000000')
792        .fontSize(24)
793        .width('100%')
794        .height('100%')
795        .align(Alignment.BottomStart)
796        .margin({left:'5%'})
797    }.alignItems(HorizontalAlign.Start)
798  }
799
800  build() {
801    SideBarContainer() {
802      Column() {
803        List() {
804          ForEach(this.arr, (item: number, index) => {
805            ListItem() {
806              Text('A' + item)
807                .width('100%')
808                .height("20%")
809                .fontSize(24)
810                .fontWeight(FontWeight.Bold)
811                .textAlign(TextAlign.Center)
812                .backgroundColor('#66000000')
813            }
814          })
815        }.divider({ strokeWidth: 5, color: '#F1F3F5' })
816      }.width('100%')
817      .height('100%')
818      .justifyContent(FlexAlign.SpaceEvenly)
819      .backgroundColor('#F1F3F5')
820
821      Column() {
822        Navigation(this.pageInfos) {
823          List() {
824            ListItem() {
825              Column() {
826                ForEach(this.arrSample, (item: arrSampleObj, index) => {
827                  ListItem() {
828                    Text(item.label)
829                      .fontSize(24)
830                      .fontWeight(FontWeight.Bold)
831                      .backgroundColor('#66000000')
832                      .textAlign(TextAlign.Center)
833                      .width('100%')
834                      .height('30%')
835                      .margin({
836                        bottom:10
837                      })
838                  }.onClick(() => {
839                    this.pageInfos.clear();
840                    this.pageInfos.pushPath({ name: item.pagePath })
841                  })
842                })
843              }
844            }.width('100%')
845          }
846        }
847        .mode(NavigationMode.Auto)
848        .minContentWidth(360)
849        .navBarWidth(240)
850        .backgroundColor('#FFFFFF')
851        .height('100%')
852        .width('100%')
853        .hideToolBar(true)
854        .title(this.NavigationTitle)
855      }.width('100%').height('100%')
856    }.sideBarWidth(240)
857    .minContentWidth(this.myWidth)
858  }
859}
860```
861
862
863
864## 自定义弹窗
865
866**布局效果**
867
868| sm                                           | md                                      | lg                                      |
869| -------------------------------------------- | --------------------------------------- | --------------------------------------- |
870| 弹窗横向居中,纵向位于底部显示,与窗口左右两侧各间距24vp。 | 弹窗横向居中,纵向位于底部显示。 | 弹窗居中显示,其宽度约为窗口宽度的1/3。 |
871| ![](figures/custom_dialog_sm.png)            | ![](figures/custom_dialog_md.png)       | ![](figures/custom_dialog_lg.png)       |
872
873**实现方案**
874
875自定义弹窗通常通过[CustomDialogController](../../reference/apis-arkui/arkui-ts/ts-methods-custom-dialog-box.md)实现,有两种方式实现本场景的目标效果:
876
877* 通过gridCount属性配置自定义弹窗的宽度。
878
879  系统默认对不同断点下的窗口进行了栅格化:sm断点下为4栅格,md断点下为8栅格,lg断点下为12栅格。通过gridCount属性可以配置弹窗占据栅格中的多少列,将该值配置为4即可实现目标效果。
880
881* 将customStyle设置为true,即弹窗的样式完全由开发者自定义。
882
883  开发者自定义弹窗样式时,开发者可以根据需要配置弹窗的宽高和背景色(非弹窗区域保持默认的半透明色)。自定义弹窗样式配合[栅格组件](../../reference/apis-arkui/arkui-ts/ts-container-gridrow.md)同样可以实现目标效果。
884
885**参考代码**
886
887```ts
888@Entry
889@Component
890struct CustomDialogSample {
891  // 通过gridCount配置弹窗的宽度
892  dialogControllerA: CustomDialogController = new CustomDialogController({
893    builder: CustomDialogA ({
894      cancel: this.onCancel,
895      confirm: this.onConfirm
896    }),
897    cancel: this.onCancel,
898    autoCancel: true,
899    gridCount: 4,
900    customStyle: false
901  })
902  // 自定义弹窗样式
903  dialogControllerB: CustomDialogController = new CustomDialogController({
904    builder: CustomDialogB ({
905      cancel: this.onCancel,
906      confirm: this.onConfirm
907    }),
908    cancel: this.onCancel,
909    autoCancel: true,
910    customStyle: true
911  })
912
913  onCancel() {
914    console.info('callback when dialog is canceled')
915  }
916
917  onConfirm() {
918    console.info('callback when dialog is confirmed')
919  }
920
921  build() {
922    Column() {
923      Button('CustomDialogA').margin(12)
924        .onClick(() => {
925          this.dialogControllerA.open()
926        })
927      Button('CustomDialogB').margin(12)
928        .onClick(() => {
929          this.dialogControllerB.open()
930        })
931    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
932  }
933}
934
935@CustomDialog
936struct CustomDialogA {
937  controller?: CustomDialogController
938  cancel?: () => void
939  confirm?: () => void
940
941  build() {
942    Column() {
943      Text('是否删除此联系人?')
944        .fontSize(16)
945        .fontColor('#E6000000')
946        .margin({bottom: 8, top: 24, left: 24, right: 24})
947      Row() {
948        Text('取消')
949          .fontColor('#007DFF')
950          .fontSize(16)
951          .layoutWeight(1)
952          .textAlign(TextAlign.Center)
953          .onClick(()=>{
954            if(this.controller){
955                 this.controller.close()
956             }
957            this.cancel!()
958          })
959        Line().width(1).height(24).backgroundColor('#33000000').margin({left: 4, right: 4})
960        Text('删除')
961          .fontColor('#FA2A2D')
962          .fontSize(16)
963          .layoutWeight(1)
964          .textAlign(TextAlign.Center)
965          .onClick(()=>{
966             if(this.controller){
967                 this.controller.close()
968             }
969            this.confirm!()
970          })
971      }.height(40)
972      .margin({left: 24, right: 24, bottom: 16})
973    }.borderRadius(24)
974  }
975}
976
977@CustomDialog
978struct CustomDialogB {
979  controller?: CustomDialogController
980  cancel?: () => void
981  confirm?: () => void
982
983  build() {
984    GridRow({columns: {sm: 4, md: 8, lg: 12}}) {
985      GridCol({span: 4, offset: {sm: 0, md: 2, lg: 4}}) {
986        Column() {
987          Text('是否删除此联系人?')
988            .fontSize(16)
989            .fontColor('#E6000000')
990            .margin({bottom: 8, top: 24, left: 24, right: 24})
991          Row() {
992            Text('取消')
993              .fontColor('#007DFF')
994              .fontSize(16)
995              .layoutWeight(1)
996              .textAlign(TextAlign.Center)
997              .onClick(()=>{
998                if(this.controller){
999                 this.controller.close()
1000                }
1001                this.cancel!()
1002              })
1003            Line().width(1).height(24).backgroundColor('#33000000').margin({left: 4, right: 4})
1004            Text('删除')
1005              .fontColor('#FA2A2D')
1006              .fontSize(16)
1007              .layoutWeight(1)
1008              .textAlign(TextAlign.Center)
1009              .onClick(()=>{
1010                 if(this.controller){
1011                 this.controller.close()
1012                }
1013                this.confirm!()
1014              })
1015          }.height(40)
1016          .margin({left: 24, right: 24, bottom: 16})
1017        }.borderRadius(24).backgroundColor('#FFFFFF')
1018      }
1019    }.margin({left: 24, right: 24})
1020  }
1021}
1022```
1023
1024
1025
1026## 大图浏览
1027
1028**布局效果**
1029
1030
1031| sm | md | lg |
1032| -------- | -------- | -------- |
1033| 图片长宽比不变,最长边充满全屏 | 图片长宽比不变,最长边充满全屏 | 图片长宽比不变,最长边充满全屏 |
1034| ![大图浏览sm](figures/大图浏览sm.png) | ![大图浏览md](figures/大图浏览md.png) | ![大图浏览lg](figures/大图浏览lg.png) |
1035
1036**实现方案**
1037
1038图片通常使用[Image组件](../../reference/apis-arkui/arkui-ts/ts-basic-components-image.md)展示,Image组件的objectFit属性默认为ImageFit.Cover,即保持宽高比进行缩小或者放大以使得图片两边都大于或等于显示边界。在大图浏览场景下,因屏幕与图片的宽高比可能有差异,常常会发生图片被截断的问题。此时只需将Image组件的objectFit属性设置为ImageFit.Contain,即保持宽高比进行缩小或者放大并使得图片完全显示在显示边界内,即可解决该问题。
1039
1040
1041**参考代码**
1042
1043
1044```ts
1045@Entry
1046@Component
1047struct BigImage {
1048  build() {
1049    Row() {
1050      Image($r("app.media.image"))
1051        .objectFit(ImageFit.Contain)
1052    }
1053  }
1054}
1055```
1056
1057
1058## 操作入口
1059
1060**布局效果**
1061
1062| sm | md | lg |
1063| -------- | -------- | -------- |
1064| 列表项尺寸固定,超出内容可滚动查看 | 列表项尺寸固定,剩余空间均分 | 列表项尺寸固定,剩余空间均分 |
1065| ![操作入口sm](figures/操作入口sm.png) | ![操作入口md](figures/操作入口md.png) | ![操作入口lg](figures/操作入口lg.png) |
1066
1067
1068**实现方案**
1069
1070Scroll(内容超出宽度时可滚动) + Row(横向均分:justifyContent(FlexAlign.SpaceAround)、 最小宽度约束:constraintSize({ minWidth: '100%' })
1071
1072
1073**参考代码**
1074
1075
1076```ts
1077interface OperationItem {
1078  name: string
1079  icon: Resource
1080}
1081
1082@Entry
1083@Component
1084export default struct OperationEntries {
1085  @State listData: Array<OperationItem> = [
1086    { name: '私人FM', icon: $r('app.media.self_fm') },
1087    { name: '歌手', icon: $r('app.media.singer') },
1088    { name: '歌单', icon: $r('app.media.song_list') },
1089    { name: '排行榜', icon: $r('app.media.rank') },
1090    { name: '热门', icon: $r('app.media.hot') },
1091    { name: '运动音乐', icon: $r('app.media.sport') },
1092    { name: '音乐FM', icon: $r('app.media.audio_fm') },
1093    { name: '福利', icon: $r('app.media.bonus') }]
1094
1095  build() {
1096    Scroll() {
1097      Row() {
1098        ForEach(this.listData, (item:OperationItem) => {
1099          Column() {
1100            Image(item.icon)
1101              .width(48)
1102              .aspectRatio(1)
1103            Text(item.name)
1104              .margin({ top: 8 })
1105              .fontSize(16)
1106          }
1107          .justifyContent(FlexAlign.Center)
1108          .height(104)
1109          .padding({ left: 12, right: 12 })
1110        })
1111      }
1112      .constraintSize({ minWidth: '100%' }).justifyContent(FlexAlign.SpaceAround)
1113    }
1114    .width('100%')
1115    .scrollable(ScrollDirection.Horizontal)
1116  }
1117}
1118```
1119
1120
1121## 顶部
1122
1123
1124**布局效果**
1125
1126
1127| sm | md | lg |
1128| -------- | -------- | -------- |
1129| 标题和搜索框两行显示 | 标题和搜索框一行显示 | 标题和搜索框一行显示 |
1130| ![顶部布局sm](figures/顶部布局sm.png) | ![顶部布局md](figures/顶部布局md.png) | ![顶部布局lg](figures/顶部布局lg.png) |
1131
1132**实现方案**
1133
1134最外层使用栅格行组件GridRow布局
1135
1136文本标题使用栅格列组件GridCol
1137
1138搜索框使用栅格列组件GridCol
1139
1140
1141**参考代码**
1142
1143
1144```ts
1145@Entry
1146@Component
1147export default struct Header {
1148  @State needWrap: boolean = true
1149
1150  build() {
1151    GridRow() {
1152      GridCol({ span: { sm: 12, md: 6, lg: 7 } }) {
1153        Row() {
1154          Text('推荐').fontSize(24)
1155          Blank()
1156          Image($r('app.media.ic_public_more'))
1157            .width(32)
1158            .height(32)
1159            .objectFit(ImageFit.Contain)
1160            .visibility(this.needWrap ? Visibility.Visible : Visibility.None)
1161        }
1162        .width('100%').height(40)
1163        .alignItems(VerticalAlign.Center)
1164      }
1165
1166      GridCol({ span: { sm: 12, md: 6, lg: 5 } }) {
1167        Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
1168          Search({ placeholder: '猜您喜欢: 万水千山' })
1169            .placeholderFont({ size: 16 })
1170            .margin({ top: 4, bottom: 4 })
1171          Image($r('app.media.audio_fm'))
1172            .width(32)
1173            .height(32)
1174            .objectFit(ImageFit.Contain)
1175            .flexShrink(0)
1176            .margin({ left: 12 })
1177          Image($r('app.media.ic_public_more'))
1178            .width(32)
1179            .height(32)
1180            .objectFit(ImageFit.Contain)
1181            .flexShrink(0)
1182            .margin({ left: 12 })
1183            .visibility(this.needWrap ? Visibility.None : Visibility.Visible)
1184        }
1185      }
1186    }.onBreakpointChange((breakpoint: string) => {
1187      if (breakpoint === 'sm') {
1188        this.needWrap = true
1189      } else {
1190        this.needWrap = false
1191      }
1192    })
1193    .padding({ left: 12, right: 12 })
1194  }
1195}
1196```
1197
1198
1199## 缩进布局
1200
1201
1202**布局效果**
1203
1204
1205  | sm | md | lg |
1206| -------- | -------- | -------- |
1207| 栅格总列数为4,内容占满所有列 | 栅格总列数为8,内容占中间6列。 | 栅格总列数为12,内容占中间8列。 |
1208| ![indent_sm](figures/indent_sm.jpg) | ![indent_md](figures/indent_md.jpg) | ![indent_lg](figures/indent_lg.jpg) |
1209
1210
1211**实现方案**
1212
1213借助[栅格组件](../../reference/apis-arkui/arkui-ts/ts-container-gridrow.md),控制待显示内容在不同的断点下占据不同的列数,即可实现不同设备上的缩进效果。另外还可以调整不同断点下栅格组件与两侧的间距,获得更好的显示效果。
1214
1215
1216**参考代码**
1217
1218
1219```ts
1220@Entry
1221@Component
1222struct IndentationSample {
1223  @State private gridMargin: number = 24
1224  build() {
1225    Row() {
1226      GridRow({columns: {sm: 4, md: 8, lg: 12}, gutter: 24}) {
1227        GridCol({span: {sm: 4, md: 6, lg: 8}, offset: {md: 1, lg: 2}}) {
1228          Column() {
1229            ForEach([0, 1, 2, 4], () => {
1230              Column() {
1231                ItemContent()
1232              }
1233            })
1234          }.width('100%')
1235        }
1236      }
1237      .margin({left: this.gridMargin, right: this.gridMargin})
1238      .onBreakpointChange((breakpoint: string) => {
1239        if (breakpoint === 'lg') {
1240          this.gridMargin = 48
1241        } else if (breakpoint === 'md') {
1242          this.gridMargin = 32
1243        } else {
1244          this.gridMargin = 24
1245        }
1246      })
1247    }
1248    .height('100%')
1249    .alignItems((VerticalAlign.Center))
1250    .backgroundColor('#F1F3f5')
1251  }
1252}
1253
1254@Component
1255struct ItemContent {
1256  build() {
1257    Column() {
1258      Row() {
1259        Row() {
1260        }
1261        .width(28)
1262        .height(28)
1263        .borderRadius(14)
1264        .margin({ right: 15 })
1265        .backgroundColor('#E4E6E8')
1266
1267        Row() {
1268        }
1269        .width('30%').height(20).borderRadius(4)
1270        .backgroundColor('#E4E6E8')
1271      }.width('100%').height(28)
1272
1273      Row() {
1274      }
1275      .width('100%')
1276      .height(68)
1277      .borderRadius(16)
1278      .margin({ top: 12 })
1279      .backgroundColor('#E4E6E8')
1280    }
1281    .height(128)
1282    .borderRadius(24)
1283    .backgroundColor('#FFFFFF')
1284    .padding({ top: 12, bottom: 12, left: 18, right: 18 })
1285    .margin({ bottom: 12 })
1286  }
1287}
1288```
1289
1290
1291## 挪移布局
1292
1293**布局效果**
1294
1295  | sm | md | lg |
1296| -------- | -------- | -------- |
1297| 图片和文字上下布局 | 图片和文字左右布局 | 图片和文字左右布局 |
1298| ![diversion_sm](figures/diversion_sm.jpg) | ![diversion_md](figures/diversion_md.jpg) | ![diversion_lg](figures/diversion_lg.jpg) |
1299
1300
1301**实现方案**
1302
1303不同断点下,栅格子元素占据的列数会随着开发者的配置发生改变。当一行中的列数超过栅格组件在该断点的总列数时,可以自动换行,即实现”上下布局”与”左右布局”之间切换的效果。
1304
1305
1306**参考代码**
1307
1308
1309```ts
1310@Entry
1311@Component
1312struct DiversionSample {
1313  @State private currentBreakpoint: string = 'md'
1314  @State private imageHeight: number = 0
1315  build() {
1316    Row() {
1317      GridRow() {
1318        GridCol({span: {sm: 12, md: 6, lg: 6}}) {
1319          Image($r('app.media.illustrator'))
1320          .aspectRatio(1)
1321          .onAreaChange((oldValue: Area, newValue: Area) => {
1322            this.imageHeight = Number(newValue.height)
1323          })
1324          .margin({left: 12, right: 12})
1325        }
1326
1327        GridCol({span: {sm: 12, md: 6, lg: 6}}) {
1328          Column(){
1329            Text($r('app.string.user_improvement'))
1330              .textAlign(TextAlign.Center)
1331              .fontSize(20)
1332              .fontWeight(FontWeight.Medium)
1333            Text($r('app.string.user_improvement_tips'))
1334              .textAlign(TextAlign.Center)
1335              .fontSize(14)
1336              .fontWeight(FontWeight.Medium)
1337          }
1338          .margin({left: 12, right: 12})
1339          .justifyContent(FlexAlign.Center)
1340          .height(this.currentBreakpoint === 'sm' ? 100 : this.imageHeight)
1341        }
1342      }.onBreakpointChange((breakpoint: string) => {
1343        this.currentBreakpoint = breakpoint;
1344      })
1345    }
1346    .height('100%')
1347    .alignItems((VerticalAlign.Center))
1348    .backgroundColor('#F1F3F5')
1349  }
1350}
1351```
1352
1353
1354## 重复布局
1355
1356**布局效果**
1357
1358| sm | md | lg |
1359| -------- | -------- | -------- |
1360| 单列显示,共8个元素<br>可以通过上下滑动查看不同的元素 | 双列显示,共8个元素 | 双列显示,共8个元素 |
1361| ![repeat_sm](figures/repeat_sm.jpg) | ![repeat_md](figures/repeat_md.jpg)  | ![repeat_lg](figures/repeat_lg.jpg) |
1362
1363
1364**实现方案**
1365
1366不同断点下,配置栅格子组件占据不同的列数,即可实现“小屏单列显示、大屏双列显示”的效果。另外,还可以通过栅格组件的onBreakpointChange事件,调整页面中显示的元素数量。
1367
1368
1369**参考代码**
1370
1371
1372```ts
1373@Entry
1374@Component
1375struct RepeatSample {
1376  @State private currentBreakpoint: string = 'md'
1377  @State private listItems: number[] = [1, 2, 3, 4, 5, 6, 7, 8]
1378  @State private gridMargin: number = 24
1379
1380  build() {
1381    Row() {
1382      // 当目标区域不足以显示所有元素时,可以通过上下滑动查看不同的元素
1383      Scroll() {
1384        GridRow({gutter: 24}) {
1385          ForEach(this.listItems, () => {
1386           // 通过配置元素在不同断点下占的列数,实现不同的布局效果
1387            GridCol({span: {sm: 12, md: 6, lg: 6}}) {
1388              Column() {
1389                RepeatItemContent()
1390              }
1391            }
1392          })
1393        }
1394        .margin({left: this.gridMargin, right: this.gridMargin})
1395        .onBreakpointChange((breakpoint: string) => {
1396          this.currentBreakpoint = breakpoint;
1397          if (breakpoint === 'lg') {
1398            this.gridMargin = 48
1399          } else if (breakpoint === 'md') {
1400            this.gridMargin = 32
1401          } else {
1402            this.gridMargin = 24
1403          }
1404        })
1405      }.height(348)
1406    }
1407    .height('100%')
1408    .backgroundColor('#F1F3F5')
1409  }
1410}
1411
1412@Component
1413struct RepeatItemContent {
1414  build() {
1415    Flex() {
1416      Row() {
1417      }
1418      .width(43)
1419      .height(43)
1420      .borderRadius(12)
1421      .backgroundColor('#E4E6E8')
1422      .flexGrow(0)
1423
1424      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.SpaceAround }) {
1425        Row() {
1426        }
1427        .height(10)
1428        .width('80%')
1429        .backgroundColor('#E4E6E8')
1430
1431        Row() {
1432        }
1433        .height(10)
1434        .width('50%')
1435        .backgroundColor('#E4E6E8')
1436      }
1437      .flexGrow(1)
1438      .margin({ left: 13 })
1439    }
1440    .padding({ top: 13, bottom: 13, left: 13, right: 37 })
1441    .height(69)
1442    .backgroundColor('#FFFFFF')
1443    .borderRadius(24)
1444  }
1445}
1446```
1447