• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 典型布局场景
2
3
4虽然不同应用的页面千变万化,但对其进行拆分和分析,页面中的很多布局场景是相似的。本小节将介绍如何借助自适应布局、响应式布局以及常见的容器类组件,实现应用中的典型布局场景。
5
6
7| 布局场景 | 实现方案 |
8| -------- | -------- |
9| [页签栏](#页签栏) | Tab组件 + 响应式布局 |
10| [运营横幅(Banner)](#运营横幅banner) | Swiper组件 + 响应式布局 |
11| [网格](#网格) | Grid组件 / List组件 + 响应式布局 |
12| [侧边栏](#侧边栏) | SiderBar组件 + 响应式布局 |
13| [单/双栏](#单/双栏) | Navigation组件 + 响应式布局 |
14| [自定义弹窗](#自定义弹窗) | CustomDialogController组件 + 响应式布局 |
15| [大图浏览](#大图浏览) | Image组件 |
16| [操作入口](#操作入口) | Scroll组件+Row组件横向均分 |
17| [顶部](#顶部) | 栅格组件 |
18| [缩进布局](#缩进布局) | 栅格组件 |
19| [挪移布局](#挪移布局) | 栅格组件 |
20| [重复布局](#重复布局) | 栅格组件 |
21
22
23> ![icon-note.gif](public_sys-resources/icon-note.gif) **说明:**
24> 在本文[媒体查询](responsive-layout.md#媒体查询)小节中已经介绍了如何通过媒体查询监听断点变化,后续的示例中不再重复介绍此部分代码。
25
26
27## 页签栏
28
29**布局效果**
30
31| sm | md | lg |
32| -------- | -------- | -------- |
33| 页签在底部<br/>页签的图标和文字垂直布局<br/>页签宽度均分<br/>页签高度固定72vp | 页签在底部<br/>页签的图标和文字水平布局<br/>页签宽度均分<br/>页签高度固定56vp | 页签在左边<br/>页签的图标和文字垂直布局<br/>页签宽度固定96vp<br/>页签高度总占比‘60%’后均分 |
34| ![页签布局](figures/页签布局sm.png) | ![页签布局](figures/页签布局md.png) | ![页签布局](figures/页签布局lg.png) |
35
36
37**实现方案**
38
39不同断点下,页签在页面中的位置及尺寸都有差异,可以结合响应式布局能力,设置不同断点下[Tab组件](../../reference/arkui-ts/ts-container-tabs.md)的barPosition、vertical、barWidth和barHeight属性实现目标效果。
40
41另外,页签栏中的文字和图片的相对位置不同,同样可以通过设置不同断点下[tabBar](../../reference/arkui-ts/ts-container-tabcontent.md#属性)对应的CustomBuilder中的布局方向,实现目标效果。
42
43
44**参考代码**
45
46
47```
48import { BreakpointSystem, BreakPointType } from 'common/BreakpointSystem'
49
50type TabBar = {
51  name: string
52  icon: Resource
53  selectIcon: Resource
54}
55
56@Entry
57@Component
58struct Home {
59  @State currentIndex: number = 0
60  @State tabs: Array<TabBar> = [{
61                                  name: '首页',
62                                  icon: $r('app.media.ic_music_home'),
63                                  selectIcon: $r('app.media.ic_music_home_selected')
64                                }, {
65                                  name: '排行榜',
66                                  icon: $r('app.media.ic_music_ranking'),
67                                  selectIcon: $r('app.media.ic_music_ranking_selected')
68                                }, {
69                                  name: '我的',
70                                  icon: $r('app.media.ic_music_me_nor'),
71                                  selectIcon: $r('app.media.ic_music_me_selected')
72                                }]
73
74  @Builder TabBarBuilder(index: number, tabBar: TabBar) {
75    Flex({
76      direction: new BreakPointType({
77        sm: FlexDirection.Column,
78        md: FlexDirection.Row,
79        lg: FlexDirection.Column
80      }).getValue(this.currentBreakpoint),
81      justifyContent: FlexAlign.Center,
82      alignItems: ItemAlign.Center
83    }) {
84      Image(this.currentIndex === index ? tabBar.selectIcon : tabBar.icon)
85        .size({ width: 36, height: 36 })
86      Text(tabBar.name)
87        .fontColor(this.currentIndex === index ? '#FF1948' : '#999')
88        .margin(new BreakPointType({
89          sm: { top: 4 },
90          md: { left: 8 },
91          lg: { top: 4 } }).getValue(this.currentBreakpoint))
92        .fontSize(16)
93    }
94    .width('100%')
95    .height('100%')
96  }
97
98  @StorageLink('currentBreakpoint') currentBreakpoint: string = 'md'
99  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
100
101  aboutToAppear() {
102    this.breakpointSystem.register()
103  }
104
105  aboutToDisappear() {
106    this.breakpointSystem.unregister()
107  }
108
109  build() {
110    Tabs({
111      barPosition: new BreakPointType({
112        sm: BarPosition.End,
113        md: BarPosition.End,
114        lg: BarPosition.Start
115      }).getValue(this.currentBreakpoint)
116    }) {
117      ForEach(this.tabs, (item, index) => {
118        TabContent() {
119          Stack() {
120            Text(item.name).fontSize(30)
121          }.width('100%').height('100%')
122        }.tabBar(this.TabBarBuilder(index, item))
123      })
124    }
125    .vertical(new BreakPointType({ sm: false, md: false, lg: true }).getValue(this.currentBreakpoint))
126    .barWidth(new BreakPointType({ sm: '100%', md: '100%', lg: '96vp' }).getValue(this.currentBreakpoint))
127    .barHeight(new BreakPointType({ sm: '72vp', md: '56vp', lg: '60%' }).getValue(this.currentBreakpoint))
128    .animationDuration(0)
129    .onChange((index: number) => {
130      this.currentIndex = index
131    })
132  }
133}
134```
135
136
137## 运营横幅(Banner)
138
139**布局效果**
140
141| sm | md | lg |
142| -------- | -------- | -------- |
143| 展示一个内容项 | 展示两个内容项 | 展示三个内容项 |
144| ![banner_sm](figures/banner_sm.png) | ![banner_md](figures/banner_md.png) | ![banner_lg](figures/banner_lg.png) |
145
146**实现方案**
147
148运营横幅通常使用[Swiper组件](../../reference/arkui-ts/ts-container-swiper.md)实现。不同断点下,运营横幅中展示的图片数量不同。只需要结合响应式布局,配置不同断点下Swiper组件的displayCount属性,即可实现目标效果。
149
150**参考代码**
151
152
153```
154import { BreakpointSystem, BreakPointType } from 'common/BreakpointSystem'
155
156@Entry
157@Component
158export default struct Banner {
159  private data: Array<Resource> = [
160    $r('app.media.banner1'),
161    $r('app.media.banner2'),
162    $r('app.media.banner3'),
163    $r('app.media.banner4'),
164    $r('app.media.banner5'),
165    $r('app.media.banner6'),
166  ]
167  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
168  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
169
170  aboutToAppear() {
171    this.breakpointSystem.register()
172  }
173
174  aboutToDisappear() {
175    this.breakpointSystem.unregister()
176  }
177
178  build() {
179    Swiper() {
180      ForEach(this.data, (item) => {
181        Image(item)
182          .size({ width: '100%', height: 200 })
183          .borderRadius(12)
184          .padding(8)
185      })
186    }
187    .indicator(new BreakPointType({ sm: true, md: false, lg: false }).getValue(this.currentBreakpoint))
188    .displayCount(new BreakPointType({ sm: 1, md: 2, lg: 3 }).getValue(this.currentBreakpoint))
189  }
190}
191```
192
193
194## 网格
195
196**布局效果**
197
198| sm | md | lg |
199| -------- | -------- | -------- |
200| 展示两列 | 展示四列 | 展示六列 |
201| ![多列列表sm](figures/多列列表sm.png) | ![多列列表md](figures/多列列表md.png) | ![多列列表lg](figures/多列列表lg.png) |
202
203
204**实现方案**
205
206不同断点下,页面中图片的排布不同,此场景可以通过响应式布局能力结合[Grid组件](../../reference/arkui-ts/ts-container-grid.md)实现,通过调整不同断点下的Grid组件的columnsTemplate属性即可实现目标效果。
207
208另外,由于本例中各列的宽度相同,也可以通过响应式布局能力结合[List组件](../../reference/arkui-ts/ts-container-list.md)实现,通过调整不同断点下的List组件的lanes属性也可实现目标效果。
209
210
211**参考代码**
212
213通过Grid组件实现
214
215
216```
217import { BreakpointSystem, BreakPointType } from 'common/breakpointsystem'
218
219type GridItemInfo = {
220  name: string
221  image: Resource
222}
223
224@Entry
225@Component
226struct MultiLaneList {
227  private data: GridItemInfo[] = [
228    { name: '歌单集合1', image: $r('app.media.1') },
229    { name: '歌单集合2', image: $r('app.media.2') },
230    { name: '歌单集合3', image: $r('app.media.3') },
231    { name: '歌单集合4', image: $r('app.media.4') },
232    { name: '歌单集合5', image: $r('app.media.5') },
233    { name: '歌单集合6', image: $r('app.media.6') },
234    { name: '歌单集合7', image: $r('app.media.7') },
235    { name: '歌单集合8', image: $r('app.media.8') },
236    { name: '歌单集合9', image: $r('app.media.9') },
237    { name: '歌单集合10', image: $r('app.media.10') },
238    { name: '歌单集合11', image: $r('app.media.11') },
239    { name: '歌单集合12', image: $r('app.media.12') }
240  ]
241  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
242  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
243
244  aboutToAppear() {
245    this.breakpointSystem.register()
246  }
247
248  aboutToDisappear() {
249    this.breakpointSystem.unregister()
250  }
251
252  build() {
253    Grid() {
254      ForEach(this.data, (item: GridItemInfo) => {
255        GridItem() {
256          Column() {
257            Image(item.image)
258              .aspectRatio(1.8)
259            Text(item.name)
260              .margin({ top: 8 })
261              .fontSize(20)
262          }.padding(4)
263        }
264      })
265    }
266    .columnsTemplate(new BreakPointType({
267      sm: '1fr 1fr',
268      md: '1fr 1fr 1fr 1fr',
269      lg: '1fr 1fr 1fr 1fr 1fr 1fr'
270    }).GetValue(this.currentBreakpoint))
271  }
272}
273```
274
275通过List组件实现
276
277
278```
279import { BreakpointSystem, BreakPointType } from 'common/BreakpointSystem'
280
281type ListItemInfo = {
282  name: string
283  image: Resource
284}
285
286@Entry
287@Component
288struct MultiLaneList {
289  private data: ListItemInfo[] = [
290    { name: '歌单集合1', image: $r('app.media.1') },
291    { name: '歌单集合2', image: $r('app.media.2') },
292    { name: '歌单集合3', image: $r('app.media.3') },
293    { name: '歌单集合4', image: $r('app.media.4') },
294    { name: '歌单集合5', image: $r('app.media.5') },
295    { name: '歌单集合6', image: $r('app.media.6') },
296    { name: '歌单集合7', image: $r('app.media.7') },
297    { name: '歌单集合8', image: $r('app.media.8') },
298    { name: '歌单集合9', image: $r('app.media.9') },
299    { name: '歌单集合10', image: $r('app.media.10') },
300    { name: '歌单集合11', image: $r('app.media.11') },
301    { name: '歌单集合12', image: $r('app.media.12') }
302  ]
303  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
304  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'md'
305
306  aboutToAppear() {
307    this.breakpointSystem.register()
308  }
309
310  aboutToDisappear() {
311    this.breakpointSystem.unregister()
312  }
313
314  build() {
315    List() {
316      ForEach(this.data, (item: ListItemInfo) => {
317        ListItem() {
318          Column() {
319            Image(item.image)
320            Text(item.name)
321              .margin({ top: 8 })
322              .fontSize(20)
323          }.padding(4)
324        }
325      })
326    }
327    .lanes(new BreakPointType({ sm: 2, md: 4, lg: 6 }).getValue(this.currentBreakpoint))
328    .width('100%')
329  }
330}
331```
332
333
334## 侧边栏
335
336**布局效果**
337
338| sm | md | lg |
339| -------- | -------- | -------- |
340| 默认隐藏侧边栏,同时提供侧边栏控制按钮,用户可以通过按钮控制侧边栏显示或隐藏。 | 始终显示侧边栏,不提供控制按钮,用户无法隐藏侧边栏。 | 始终显示侧边栏,不提供控制按钮,用户无法隐藏侧边栏。 |
341| ![侧边栏sm](figures/侧边栏sm.png) | ![侧边栏md](figures/侧边栏md.png) | ![侧边栏lg](figures/侧边栏lg.png) |
342
343**实现方案**
344
345侧边栏通常通过[SideBarContainer组件](../../reference/arkui-ts/ts-container-sidebarcontainer.md)实现,结合响应式布局能力,在不同断点下为SiderBarConContainer组件的sideBarWidth、showControlButton等属性配置不同的值,即可实现目标效果。
346
347**参考代码**
348
349
350```
351@Entry
352@Component
353struct SideBarSample {
354  @StorageLink('currentBreakpoint') private currentBreakpoint: string = "md";
355  private breakpointSystem: BreakpointSystem = new BreakpointSystem()
356  @State showSideBar: boolean = false
357  @State selectIndex: number = 0;
358
359  aboutToAppear() {
360    this.breakpointSystem.register()
361    if (this.currentBreakpoint === 'sm') {
362      this.showSideBar = false
363    } else {
364      this.showSideBar = true
365    }
366  }
367
368  aboutToDisappear() {
369    this.breakpointSystem.unregister()
370  }
371
372  @Builder itemBuilder(index: number) {
373    Text(images[index].label)
374      .fontSize(24)
375      .fontWeight(FontWeight.Bold)
376      .borderRadius(5)
377      .margin(20)
378      .backgroundColor('#ffffff')
379      .textAlign(TextAlign.Center)
380      .width(180)
381      .height(36)
382      .onClick(() => {
383        this.selectIndex = index
384        if (this.currentBreakpoint === 'sm') {
385          this.showSideBar = false
386        }
387      })
388  }
389
390  build() {
391    SideBarContainer(this.currentBreakpoint === 'sm' ? SideBarContainerType.Overlay : SideBarContainerType.Embed) {
392      Column() {
393        this.itemBuilder(0)
394        this.itemBuilder(1)
395      }.backgroundColor('#F1F3F5')
396      .justifyContent(FlexAlign.Center)
397
398      Column() {
399        Image(images[this.selectIndex].imageSrc)
400          .objectFit(ImageFit.Contain)
401          .height(300)
402          .width(300)
403      }
404      .justifyContent(FlexAlign.Center)
405      .width('100%')
406      .height('100%')
407    }
408    .height('100%')
409    .sideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
410    .minSideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
411    .maxSideBarWidth(this.currentBreakpoint === 'sm' ? '100%' : '33.33%')
412    .showControlButton(this.currentBreakpoint === 'sm')
413    .autoHide(false)
414    .showSideBar(this.showSideBar)
415    .onChange((isBarShow: boolean) => {
416      this.showSideBar = isBarShow
417    })
418  }
419}
420```
421
422## 单/双栏
423
424**布局效果**
425
426| sm                                                           | md                                               | lg                                               |
427| ------------------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ |
428| 单栏显示,在首页中点击选项可以显示详情。<br>点击详情上方的返回键图标或使用系统返回键可以返回到主页。 | 双栏显示,点击左侧不同的选项可以刷新右侧的显示。 | 双栏显示,点击左侧不同的选项可以刷新右侧的显示。 |
429| ![](figures/navigation_sm.png)                               | ![](figures/navigation_md.png)                   | ![](figures/navigation_lg.png)                   |
430
431**实现方案**
432
433单/双栏场景可以使用[Navigation组件](../../reference/arkui-ts/ts-basic-components-navigation.md)实现,Navigation组件可以根据窗口宽度自动切换单/双栏显示,减少开发工作量。
434
435**参考代码**
436
437```
438@Component
439struct Details {
440  private imageSrc: Resource
441  build() {
442    Column() {
443      Image(this.imageSrc)
444        .objectFit(ImageFit.Contain)
445        .height(300)
446        .width(300)
447    }
448    .justifyContent(FlexAlign.Center)
449    .width('100%')
450    .height('100%')
451  }
452}
453
454@Component
455struct Item {
456  private imageSrc: Resource
457  private label: string
458
459  build() {
460    NavRouter() {
461      Text(this.label)
462        .fontSize(24)
463        .fontWeight(FontWeight.Bold)
464        .borderRadius(5)
465        .backgroundColor('#FFFFFF')
466        .textAlign(TextAlign.Center)
467        .width(180)
468        .height(36)
469      NavDestination() {
470        Details({imageSrc: this.imageSrc})
471      }.title(this.label)
472      .backgroundColor('#FFFFFF')
473    }
474  }
475}
476
477@Entry
478@Component
479struct NavigationSample {
480  build() {
481    Navigation() {
482      Column({space: 30}) {
483        Item({label: 'moon', imageSrc: $r('app.media.my_image_moon')})
484        Item({label: 'sun', imageSrc: $r('app.media.my_image')})
485      }
486      .justifyContent(FlexAlign.Center)
487      .height('100%')
488      .width('100%')
489    }
490    .mode(NavigationMode.Auto)
491    .backgroundColor('#F1F3F5')
492    .height('100%')
493    .width('100%')
494    .navBarWidth(360)
495    .hideToolBar(true)
496    .title('Sample')
497  }
498}
499```
500
501
502
503## 自定义弹窗
504
505**布局效果**
506
507| sm                                           | md                                      | lg                                      |
508| -------------------------------------------- | --------------------------------------- | --------------------------------------- |
509| 弹窗居中显示,<br>与窗口左右两侧各间距24vp。 | 弹窗居中显示,其宽度约为窗口宽度的1/2。 | 弹窗居中显示,其宽度约为窗口宽度的1/3。 |
510| ![](figures/custom_dialog_sm.png)            | ![](figures/custom_dialog_md.png)       | ![](figures/custom_dialog_lg.png)       |
511
512**实现方案**
513
514自定义弹窗通常通过[CustomDialogController](../../reference/arkui-ts/ts-methods-custom-dialog-box.md)实现,有两种方式实现本场景的目标效果:
515
516* 通过gridCount属性配置自定义弹窗的宽度。
517
518  系统默认对不同断点下的窗口进行了栅格化:sm断点下为4栅格,md断点下为8栅格,lg断点下为12栅格。通过gridCount属性可以配置弹窗占据栅格中的多少列,将该值配置为4即可实现目标效果。
519
520* 将customStyle设置为true,即弹窗的样式完全由开发者自定义。
521
522  开发者自定义弹窗样式时,开发者可以根据需要配置弹窗的宽高和背景色(非弹窗区域保持默认的半透明色)。自定义弹窗样式配合[栅格组件](../../reference/arkui-ts/ts-container-gridrow.md)同样可以实现目标效果。
523
524**参考代码**
525
526```
527@Entry
528@Component
529struct CustomDialogSample {
530  // 通过gridCount配置弹窗的宽度
531  dialogControllerA: CustomDialogController = new CustomDialogController({
532    builder: CustomDialogA ({
533      cancel: this.onCancel,
534      confirm: this.onConfirm
535    }),
536    cancel: this.onCancel,
537    autoCancel: true,
538    gridCount: 4,
539    customStyle: false
540  })
541  // 自定义弹窗样式
542  dialogControllerB: CustomDialogController = new CustomDialogController({
543    builder: CustomDialogB ({
544      cancel: this.onCancel,
545      confirm: this.onConfirm
546    }),
547    cancel: this.onCancel,
548    autoCancel: true,
549    customStyle: true
550  })
551
552  onCancel() {
553    console.info('callback when dialog is canceled')
554  }
555
556  onConfirm() {
557    console.info('callback when dialog is confirmed')
558  }
559
560  build() {
561    Column() {
562      Button('CustomDialogA').margin(12)
563        .onClick(() => {
564          this.dialogControllerA.open()
565        })
566      Button('CustomDialogB').margin(12)
567        .onClick(() => {
568          this.dialogControllerB.open()
569        })
570    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
571  }
572}
573
574@CustomDialog
575struct CustomDialogA {
576  controller: CustomDialogController
577  cancel: () => void
578  confirm: () => void
579
580  build() {
581    Column() {
582      Text('是否删除此联系人?')
583        .fontSize(16)
584        .fontColor('#E6000000')
585        .margin({bottom: 8, top: 24, left: 24, right: 24})
586      Row() {
587        Text('取消')
588          .fontColor('#007DFF')
589          .fontSize(16)
590          .layoutWeight(1)
591          .textAlign(TextAlign.Center)
592          .onClick(()=>{
593            this.controller.close()
594            this.cancel()
595          })
596        Line().width(1).height(24).backgroundColor('#33000000').margin({left: 4, right: 4})
597        Text('删除')
598          .fontColor('#FA2A2D')
599          .fontSize(16)
600          .layoutWeight(1)
601          .textAlign(TextAlign.Center)
602          .onClick(()=>{
603            this.controller.close()
604            this.confirm()
605          })
606      }.height(40)
607      .margin({left: 24, right: 24, bottom: 16})
608    }.borderRadius(24)
609  }
610}
611
612@CustomDialog
613struct CustomDialogB {
614  controller: CustomDialogController
615  cancel: () => void
616  confirm: () => void
617
618  build() {
619    GridRow({columns: {sm: 4, md: 8, lg: 12}}) {
620      GridCol({span: 4, offset: {sm: 0, md: 2, lg: 4}}) {
621        Column() {
622          Text('是否删除此联系人?')
623            .fontSize(16)
624            .fontColor('#E6000000')
625            .margin({bottom: 8, top: 24, left: 24, right: 24})
626          Row() {
627            Text('取消')
628              .fontColor('#007DFF')
629              .fontSize(16)
630              .layoutWeight(1)
631              .textAlign(TextAlign.Center)
632              .onClick(()=>{
633                this.controller.close()
634                this.cancel()
635              })
636            Line().width(1).height(24).backgroundColor('#33000000').margin({left: 4, right: 4})
637            Text('删除')
638              .fontColor('#FA2A2D')
639              .fontSize(16)
640              .layoutWeight(1)
641              .textAlign(TextAlign.Center)
642              .onClick(()=>{
643                this.controller.close()
644                this.confirm()
645              })
646          }.height(40)
647          .margin({left: 24, right: 24, bottom: 16})
648        }.borderRadius(24).backgroundColor('#FFFFFF')
649      }
650    }.margin({left: 24, right: 24})
651  }
652}
653```
654
655
656
657## 大图浏览
658
659**布局效果**
660
661
662| sm | md | lg |
663| -------- | -------- | -------- |
664| 图片长宽比不变,最长边充满全屏 | 图片长宽比不变,最长边充满全屏 | 图片长宽比不变,最长边充满全屏 |
665| ![大图浏览sm](figures/大图浏览sm.png) | ![大图浏览md](figures/大图浏览md.png) | ![大图浏览lg](figures/大图浏览lg.png) |
666
667**实现方案**
668
669图片通常使用[Image组件](../../reference/arkui-ts/ts-basic-components-image.md)展示,Image组件的objectFit属性默认为ImageFit.Cover,即保持宽高比进行缩小或者放大以使得图片两边都大于或等于显示边界。在大图浏览场景下,因屏幕与图片的宽高比可能有差异,常常会发生图片被截断的问题。此时只需将Image组件的objectFit属性设置为ImageFit.Contain,即保持宽高比进行缩小或者放大并使得图片完全显示在显示边界内,即可解决该问题。
670
671
672**参考代码**
673
674
675```
676@Entry
677@Component
678struct BigImage {
679  build() {
680    Row() {
681      Image($r("app.media.image"))
682        .objectFit(ImageFit.Contain)
683    }
684  }
685}
686```
687
688
689## 操作入口
690
691**布局效果**
692
693| sm | md | lg |
694| -------- | -------- | -------- |
695| 列表项尺寸固定,超出内容可滚动查看 | 列表项尺寸固定,剩余空间均分 | 列表项尺寸固定,剩余空间均分 |
696| ![操作入口sm](figures/操作入口sm.png) | ![操作入口md](figures/操作入口md.png) | ![操作入口lg](figures/操作入口lg.png) |
697
698
699**实现方案**
700
701Scroll(内容超出宽度时可滚动) + Row(横向均分:justifyContent(FlexAlign.SpaceAround)、 最小宽度约束:constraintSize({ minWidth: '100%' })
702
703
704**参考代码**
705
706
707```
708type OperationItem = {
709  name: string
710  icon: Resource
711}
712
713@Entry
714@Component
715export default struct OperationEntries {
716  @State listData: Array<OperationItem> = [
717    { name: '私人FM', icon: $r('app.media.self_fm') },
718    { name: '歌手', icon: $r('app.media.singer') },
719    { name: '歌单', icon: $r('app.media.song_list') },
720    { name: '排行榜', icon: $r('app.media.rank') },
721    { name: '热门', icon: $r('app.media.hot') },
722    { name: '运动音乐', icon: $r('app.media.sport') },
723    { name: '音乐FM', icon: $r('app.media.audio_fm') },
724    { name: '福利', icon: $r('app.media.bonus') }]
725
726  build() {
727    Scroll() {
728      Row() {
729        ForEach(this.listData, item => {
730          Column() {
731            Image(item.icon)
732              .width(48)
733              .aspectRatio(1)
734            Text(item.name)
735              .margin({ top: 8 })
736              .fontSize(16)
737          }
738          .justifyContent(FlexAlign.Center)
739          .height(104)
740          .padding({ left: 12, right: 12 })
741        })
742      }
743      .constraintSize({ minWidth: '100%' }).justifyContent(FlexAlign.SpaceAround)
744    }
745    .width('100%')
746    .scrollable(ScrollDirection.Horizontal)
747  }
748}
749```
750
751
752## 顶部
753
754
755**布局效果**
756
757
758| sm | md | lg |
759| -------- | -------- | -------- |
760| 标题和搜索框两行显示 | 标题和搜索框一行显示 | 标题和搜索框一行显示 |
761| ![顶部布局sm](figures/顶部布局sm.png) | ![顶部布局md](figures/顶部布局md.png) | ![顶部布局lg](figures/顶部布局lg.png) |
762
763**实现方案**
764
765最外层使用栅格行组件GridRow布局
766
767文本标题使用栅格列组件GridCol
768
769搜索框使用栅格列组件GridCol
770
771
772**参考代码**
773
774
775```
776@Entry
777@Component
778export default struct Header {
779  @State needWrap: boolean = true
780
781  build() {
782    GridRow() {
783      GridCol({ span: { sm: 12, md: 6, lg: 7 } }) {
784        Row() {
785          Text('推荐').fontSize(24)
786          Blank()
787          Image($r('app.media.ic_public_more'))
788            .width(32)
789            .height(32)
790            .objectFit(ImageFit.Contain)
791            .visibility(this.needWrap ? Visibility.Visible : Visibility.None)
792        }
793        .width('100%').height(40)
794        .alignItems(VerticalAlign.Center)
795      }
796
797      GridCol({ span: { sm: 12, md: 6, lg: 5 } }) {
798        Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
799          Search({ placeholder: '猜您喜欢: 万水千山' })
800            .placeholderFont({ size: 16 })
801            .margin({ top: 4, bottom: 4 })
802          Image($r('app.media.audio_fm'))
803            .width(32)
804            .height(32)
805            .objectFit(ImageFit.Contain)
806            .flexShrink(0)
807            .margin({ left: 12 })
808          Image($r('app.media.ic_public_more'))
809            .width(32)
810            .height(32)
811            .objectFit(ImageFit.Contain)
812            .flexShrink(0)
813            .margin({ left: 12 })
814            .visibility(this.needWrap ? Visibility.None : Visibility.Visible)
815        }
816      }
817    }.onBreakpointChange((breakpoint: string) => {
818      if (breakpoint === 'sm') {
819        this.needWrap = true
820      } else {
821        this.needWrap = false
822      }
823    })
824    .padding({ left: 12, right: 12 })
825  }
826}
827```
828
829
830## 缩进布局
831
832
833**布局效果**
834
835
836  | sm | md | lg |
837| -------- | -------- | -------- |
838| 栅格总列数为4,内容占满所有列 | 栅格总列数为8,内容占中间6列。 | 栅格总列数为12,内容占中间8列。 |
839| ![indent_sm](figures/indent_sm.jpg) | ![indent_md](figures/indent_md.jpg) | ![indent_lg](figures/indent_lg.jpg) |
840
841
842**实现方案**
843
844借助[栅格组件](../../reference/arkui-ts/ts-container-gridrow.md),控制待显示内容在不同的断点下占据不同的列数,即可实现不同设备上的缩进效果。另外还可以调整不同断点下栅格组件与两侧的间距,获得更好的显示效果。
845
846
847**参考代码**
848
849
850```
851@Entry
852@Component
853struct IndentationSample {
854  @State private gridMargin: number = 24
855  build() {
856    Row() {
857      GridRow({columns: {sm: 4, md: 8, lg: 12}, gutter: 24}) {
858        GridCol({span: {sm: 4, md: 6, lg: 8}, offset: {md: 1, lg: 2}}) {
859          Column() {
860            ForEach([0, 1, 2, 4], () => {
861              Column() {
862                ItemContent()
863              }
864            })
865          }.width('100%')
866        }
867      }
868      .margin({left: this.gridMargin, right: this.gridMargin})
869      .onBreakpointChange((breakpoint: string) => {
870        if (breakpoint === 'lg') {
871          this.gridMargin = 48
872        } else if (breakpoint === 'md') {
873          this.gridMargin = 32
874        } else {
875          this.gridMargin = 24
876        }
877      })
878    }
879    .height('100%')
880    .alignItems((VerticalAlign.Center))
881    .backgroundColor('#F1F3f5')
882  }
883}
884
885@Component
886struct ItemContent {
887  build() {
888    Column() {
889      Row() {
890        Row() {
891        }
892        .width(28)
893        .height(28)
894        .borderRadius(14)
895        .margin({ right: 15 })
896        .backgroundColor('#E4E6E8')
897
898        Row() {
899        }
900        .width('30%').height(20).borderRadius(4)
901        .backgroundColor('#E4E6E8')
902      }.width('100%').height(28)
903
904      Row() {
905      }
906      .width('100%')
907      .height(68)
908      .borderRadius(16)
909      .margin({ top: 12 })
910      .backgroundColor('#E4E6E8')
911    }
912    .height(128)
913    .borderRadius(24)
914    .backgroundColor('#FFFFFF')
915    .padding({ top: 12, bottom: 12, left: 18, right: 18 })
916    .margin({ bottom: 12 })
917  }
918}
919```
920
921
922## 挪移布局
923
924**布局效果**
925
926  | sm | md | lg |
927| -------- | -------- | -------- |
928| 图片和文字上下布局 | 图片和文字左右布局 | 图片和文字左右布局 |
929| ![diversion_sm](figures/diversion_sm.jpg) | ![diversion_md](figures/diversion_md.jpg) | ![diversion_lg](figures/diversion_lg.jpg) |
930
931
932**实现方案**
933
934不同断点下,栅格子元素占据的列数会随着开发者的配置发生改变。当一行中的列数超过栅格组件在该断点的总列数时,可以自动换行,即实现”上下布局”与”左右布局”之间切换的效果。
935
936
937**参考代码**
938
939
940```
941@Entry
942@Component
943struct DiversionSample {
944  @State private currentBreakpoint: string = 'md'
945  @State private imageHeight: number = 0
946  build() {
947    Row() {
948      GridRow() {
949        GridCol({span: {sm: 12, md: 6, lg: 6}}) {
950          Image($r('app.media.illustrator'))
951          .aspectRatio(1)
952          .onAreaChange((oldValue: Area, newValue: Area) => {
953            this.imageHeight = Number(newValue.height)
954          })
955          .margin({left: 12, right: 12})
956        }
957
958        GridCol({span: {sm: 12, md: 6, lg: 6}}) {
959          Column(){
960            Text($r('app.string.user_improvement'))
961              .textAlign(TextAlign.Center)
962              .fontSize(20)
963              .fontWeight(FontWeight.Medium)
964            Text($r('app.string.user_improvement_tips'))
965              .textAlign(TextAlign.Center)
966              .fontSize(14)
967              .fontWeight(FontWeight.Medium)
968          }
969          .margin({left: 12, right: 12})
970          .justifyContent(FlexAlign.Center)
971          .height(this.currentBreakpoint === 'sm' ? 100 : this.imageHeight)
972        }
973      }.onBreakpointChange((breakpoint: string) => {
974        this.currentBreakpoint = breakpoint;
975      })
976    }
977    .height('100%')
978    .alignItems((VerticalAlign.Center))
979    .backgroundColor('#F1F3F5')
980  }
981}
982```
983
984
985## 重复布局
986
987**布局效果**
988
989| sm | md | lg |
990| -------- | -------- | -------- |
991| 单列显示,共8个元素<br>可以通过上下滑动查看不同的元素 | 双列显示,共8个元素 | 双列显示,共8个元素 |
992| ![repeat_sm](figures/repeat_sm.jpg) | ![repeat_md](figures/repeat_md.jpg)  | ![repeat_lg](figures/repeat_lg.jpg) |
993
994
995**实现方案**
996
997不同断点下,配置栅格子组件占据不同的列数,即可实现“小屏单列显示、大屏双列显示”的效果。另外,还可以通过栅格组件的onBreakpointChange事件,调整页面中显示的元素数量。
998
999
1000**参考代码**
1001
1002
1003```
1004@Entry
1005@Component
1006struct RepeatSample {
1007  @State private currentBreakpoint: string = 'md'
1008  @State private listItems: number[] = [1, 2, 3, 4, 5, 6, 7, 8]
1009  @State private gridMargin: number = 24
1010
1011  build() {
1012    Row() {
1013      // 当目标区域不足以显示所有元素时,可以通过上下滑动查看不同的元素
1014      Scroll() {
1015        GridRow({gutter: 24}) {
1016          ForEach(this.listItems, () => {
1017           // 通过配置元素在不同断点下占的列数,实现不同的布局效果
1018            GridCol({span: {sm: 12, md: 6, lg: 6}}) {
1019              Column() {
1020                RepeatItemContent()
1021              }
1022            }
1023          })
1024        }
1025        .margin({left: this.gridMargin, right: this.gridMargin})
1026        .onBreakpointChange((breakpoint: string) => {
1027          this.currentBreakpoint = breakpoint;
1028          if (breakpoint === 'lg') {
1029            this.gridMargin = 48
1030          } else if (breakpoint === 'md') {
1031            this.gridMargin = 32
1032          } else {
1033            this.gridMargin = 24
1034          }
1035        })
1036      }.height(348)
1037    }
1038    .height('100%')
1039    .backgroundColor('#F1F3F5')
1040  }
1041}
1042
1043@Component
1044struct RepeatItemContent {
1045  build() {
1046    Flex() {
1047      Row() {
1048      }
1049      .width(43)
1050      .height(43)
1051      .borderRadius(12)
1052      .backgroundColor('#E4E6E8')
1053      .flexGrow(0)
1054
1055      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.SpaceAround }) {
1056        Row() {
1057        }
1058        .height(10)
1059        .width('80%')
1060        .backgroundColor('#E4E6E8')
1061
1062        Row() {
1063        }
1064        .height(10)
1065        .width('50%')
1066        .backgroundColor('#E4E6E8')
1067      }
1068      .flexGrow(1)
1069      .margin({ left: 13 })
1070    }
1071    .padding({ top: 13, bottom: 13, left: 13, right: 37 })
1072    .height(69)
1073    .backgroundColor('#FFFFFF')
1074    .borderRadius(24)
1075  }
1076}
1077```
1078