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