• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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@Component
484struct Details {
485  private imageSrc: Resource=$r('app.media.my_image_moon')
486  build() {
487    Column() {
488      Image(this.imageSrc)
489        .objectFit(ImageFit.Contain)
490        .height(300)
491        .width(300)
492    }
493    .justifyContent(FlexAlign.Center)
494    .width('100%')
495    .height('100%')
496  }
497}
498
499@Component
500struct Item {
501  private imageSrc?: Resource
502  private label?: string
503
504  build() {
505    NavRouter() {
506      Text(this.label)
507        .fontSize(24)
508        .fontWeight(FontWeight.Bold)
509        .borderRadius(5)
510        .backgroundColor('#FFFFFF')
511        .textAlign(TextAlign.Center)
512        .width(180)
513        .height(36)
514      NavDestination() {
515        Details({imageSrc: this.imageSrc})
516      }.title(this.label)
517      .backgroundColor('#FFFFFF')
518    }
519  }
520}
521
522@Entry
523@Component
524struct NavigationSample {
525  build() {
526    Navigation() {
527      Column({space: 30}) {
528        Item({label: 'moon', imageSrc: $r('app.media.my_image_moon')})
529        Item({label: 'sun', imageSrc: $r('app.media.my_image')})
530      }
531      .justifyContent(FlexAlign.Center)
532      .height('100%')
533      .width('100%')
534    }
535    .mode(NavigationMode.Auto)
536    .backgroundColor('#F1F3F5')
537    .height('100%')
538    .width('100%')
539    .navBarWidth(360)
540    .hideToolBar(true)
541    .title('Sample')
542  }
543}
544```
545
546
547
548## 三分栏
549
550**布局效果**
551
552| sm                                           | md                                      | lg                                      |
553| -------------------------------------------- | --------------------------------------- | --------------------------------------- |
554| 单栏显示。<br> 点击侧边栏控制按钮控制侧边栏的显示/隐藏。<br> 点击首页的选项可以进入到内容区,内容区点击返回按钮可返回首页。| 双栏显示。<br> 点击侧边栏控制按钮控制侧边栏的显示/隐藏。<br> 点击左侧导航区不同的选项可以刷新右侧内容区的显示。 | 三分栏显示。<br> 点击侧边栏控制按钮控制侧边栏的显示/隐藏,来回切换二分/三分栏显示。<br> 点击左侧导航区不同的选项可以刷新右侧内容区的显示。<br> 窗口宽度变化时,优先变化右侧内容区的宽度大小。 |
555| ![](figures/tripleColumn_sm.png)            | ![](figures/tripleColumn_md.png)       | ![](figures/tripleColumn_lg.png)       |
556| ![](figures/TripleColumn.gif)
557
558**场景说明**
559
560为充分利用设备的屏幕尺寸优势,应用在大屏设备上常常有二分栏或三分栏的设计,即“A+C”,“B+C”或“A+B+C”的组合,其中A是侧边导航区,B是列表导航区,C是内容区。在用户动态改变窗口宽度时,当窗口宽度大于或等于840vp时页面呈现A+B+C三列,放大缩小优先变化C列;当窗口宽度小于840vp大于等于600vp时呈现A+C列,放大缩小时优先变化C列;当窗口宽度小于600vp大于等于360vp时,仅呈现C列。
561
562**实现方案**
563
564三分栏场景可以组合使用[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#断点)监听窗口尺寸变化的同时设置不同的值并储存成一个全局对象。
565
566**参考代码**
567
568```ts
569// MainAbility.ts
570import { window, display } from '@kit.ArkUI'
571import { Ability } from '@kit.AbilityKit'
572
573export default class MainAbility extends Ability {
574  private windowObj?: window.Window
575  private curBp?: string
576  private myWidth?: number
577  // ...
578  // 根据当前窗口尺寸更新断点
579  private updateBreakpoint(windowWidth:number) :void{
580    // 将长度的单位由px换算为vp
581    let windowWidthVp = windowWidth / (display.getDefaultDisplaySync().densityDPI / 160)
582    let newBp: string = ''
583    let newWd: number
584    if (windowWidthVp < 320) {
585      newBp = 'xs'
586      newWd = 360
587    } else if (windowWidthVp < 600) {
588      newBp = 'sm'
589      newWd = 360
590    } else if (windowWidthVp < 840) {
591      newBp = 'md'
592      newWd = 600
593    } else {
594      newBp = 'lg'
595      newWd = 600
596    }
597    if (this.curBp !== newBp) {
598      this.curBp = newBp
599      this.myWidth = newWd
600      // 使用状态变量记录当前断点值
601      AppStorage.setOrCreate('currentBreakpoint', this.curBp)
602      // 使用状态变量记录当前minContentWidth值
603      AppStorage.setOrCreate('myWidth', this.myWidth)
604    }
605  }
606
607  onWindowStageCreate(windowStage: window.WindowStage) :void{
608    windowStage.getMainWindow().then((windowObj) => {
609      this.windowObj = windowObj
610      // 获取应用启动时的窗口尺寸
611      this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width)
612      // 注册回调函数,监听窗口尺寸变化
613      windowObj.on('windowSizeChange', (windowSize)=>{
614        this.updateBreakpoint(windowSize.width)
615      })
616    });
617   // ...
618  }
619
620  // 窗口销毁时,取消窗口尺寸变化监听
621  onWindowStageDestroy() :void {
622    if (this.windowObj) {
623      this.windowObj.off('windowSizeChange')
624    }
625  }
626  //...
627}
628
629
630// tripleColumn.ets
631@Component
632struct Details {
633  private imageSrc: Resource=$r('app.media.startIcon')
634  build() {
635    Column() {
636      Image(this.imageSrc)
637        .objectFit(ImageFit.Contain)
638        .height(300)
639        .width(300)
640    }
641    .justifyContent(FlexAlign.Center)
642    .width('100%')
643    .height('100%')
644  }
645}
646
647@Component
648struct Item {
649  private imageSrc?: Resource
650  private label?: string
651
652  build() {
653    NavRouter() {
654      Text(this.label)
655        .fontSize(24)
656        .fontWeight(FontWeight.Bold)
657        .backgroundColor('#66000000')
658        .textAlign(TextAlign.Center)
659        .width('100%')
660        .height('30%')
661      NavDestination() {
662        Details({imageSrc: this.imageSrc})
663      }.title(this.label)
664      .hideTitleBar(false)
665      .backgroundColor('#FFFFFF')
666    }
667    .margin(10)
668  }
669}
670
671@Entry
672@Component
673struct TripleColumnSample {
674  @State arr: number[] = [1, 2, 3]
675  @StorageProp('myWidth') myWidth: number = 360
676
677  @Builder NavigationTitle() {
678    Column() {
679      Text('Sample')
680        .fontColor('#000000')
681        .fontSize(24)
682        .width('100%')
683        .height('100%')
684        .align(Alignment.BottomStart)
685        .margin({left:'5%'})
686    }.alignItems(HorizontalAlign.Start)
687  }
688
689  build() {
690    SideBarContainer() {
691      Column() {
692        List() {
693          ForEach(this.arr, (item:number, index) => {
694            ListItem() {
695              Text('A'+item)
696                .width('100%').height("20%").fontSize(24)
697                .fontWeight(FontWeight.Bold)
698                .textAlign(TextAlign.Center).backgroundColor('#66000000')
699            }
700          })
701        }.divider({ strokeWidth: 5, color: '#F1F3F5' })
702      }.width('100%')
703      .height('100%')
704      .justifyContent(FlexAlign.SpaceEvenly)
705      .backgroundColor('#F1F3F5')
706
707      Column() {
708        Navigation() {
709          List(){
710            ListItem() {
711              Column() {
712                Item({ label: 'B1', imageSrc: $r('app.media.startIcon') })
713                Item({ label: 'B2', imageSrc: $r('app.media.startIcon') })
714              }
715            }.width('100%')
716          }
717        }
718        .mode(NavigationMode.Auto)
719        .minContentWidth(360)
720        .navBarWidth(240)
721        .backgroundColor('#FFFFFF')
722        .height('100%')
723        .width('100%')
724        .hideToolBar(true)
725        .title(this.NavigationTitle)
726      }.width('100%').height('100%')
727    }.sideBarWidth(240)
728    .minContentWidth(this.myWidth)
729  }
730}
731```
732
733
734
735## 自定义弹窗
736
737**布局效果**
738
739| sm                                           | md                                      | lg                                      |
740| -------------------------------------------- | --------------------------------------- | --------------------------------------- |
741| 弹窗横向居中,纵向位于底部显示,与窗口左右两侧各间距24vp。 | 弹窗横向居中,纵向位于底部显示。 | 弹窗居中显示,其宽度约为窗口宽度的1/3。 |
742| ![](figures/custom_dialog_sm.png)            | ![](figures/custom_dialog_md.png)       | ![](figures/custom_dialog_lg.png)       |
743
744**实现方案**
745
746自定义弹窗通常通过[CustomDialogController](../../reference/apis-arkui/arkui-ts/ts-methods-custom-dialog-box.md)实现,有两种方式实现本场景的目标效果:
747
748* 通过gridCount属性配置自定义弹窗的宽度。
749
750  系统默认对不同断点下的窗口进行了栅格化:sm断点下为4栅格,md断点下为8栅格,lg断点下为12栅格。通过gridCount属性可以配置弹窗占据栅格中的多少列,将该值配置为4即可实现目标效果。
751
752* 将customStyle设置为true,即弹窗的样式完全由开发者自定义。
753
754  开发者自定义弹窗样式时,开发者可以根据需要配置弹窗的宽高和背景色(非弹窗区域保持默认的半透明色)。自定义弹窗样式配合[栅格组件](../../reference/apis-arkui/arkui-ts/ts-container-gridrow.md)同样可以实现目标效果。
755
756**参考代码**
757
758```ts
759@Entry
760@Component
761struct CustomDialogSample {
762  // 通过gridCount配置弹窗的宽度
763  dialogControllerA: CustomDialogController = new CustomDialogController({
764    builder: CustomDialogA ({
765      cancel: this.onCancel,
766      confirm: this.onConfirm
767    }),
768    cancel: this.onCancel,
769    autoCancel: true,
770    gridCount: 4,
771    customStyle: false
772  })
773  // 自定义弹窗样式
774  dialogControllerB: CustomDialogController = new CustomDialogController({
775    builder: CustomDialogB ({
776      cancel: this.onCancel,
777      confirm: this.onConfirm
778    }),
779    cancel: this.onCancel,
780    autoCancel: true,
781    customStyle: true
782  })
783
784  onCancel() {
785    console.info('callback when dialog is canceled')
786  }
787
788  onConfirm() {
789    console.info('callback when dialog is confirmed')
790  }
791
792  build() {
793    Column() {
794      Button('CustomDialogA').margin(12)
795        .onClick(() => {
796          this.dialogControllerA.open()
797        })
798      Button('CustomDialogB').margin(12)
799        .onClick(() => {
800          this.dialogControllerB.open()
801        })
802    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
803  }
804}
805
806@CustomDialog
807struct CustomDialogA {
808  controller?: CustomDialogController
809  cancel?: () => void
810  confirm?: () => void
811
812  build() {
813    Column() {
814      Text('是否删除此联系人?')
815        .fontSize(16)
816        .fontColor('#E6000000')
817        .margin({bottom: 8, top: 24, left: 24, right: 24})
818      Row() {
819        Text('取消')
820          .fontColor('#007DFF')
821          .fontSize(16)
822          .layoutWeight(1)
823          .textAlign(TextAlign.Center)
824          .onClick(()=>{
825            if(this.controller){
826                 this.controller.close()
827             }
828            this.cancel!()
829          })
830        Line().width(1).height(24).backgroundColor('#33000000').margin({left: 4, right: 4})
831        Text('删除')
832          .fontColor('#FA2A2D')
833          .fontSize(16)
834          .layoutWeight(1)
835          .textAlign(TextAlign.Center)
836          .onClick(()=>{
837             if(this.controller){
838                 this.controller.close()
839             }
840            this.confirm!()
841          })
842      }.height(40)
843      .margin({left: 24, right: 24, bottom: 16})
844    }.borderRadius(24)
845  }
846}
847
848@CustomDialog
849struct CustomDialogB {
850  controller?: CustomDialogController
851  cancel?: () => void
852  confirm?: () => void
853
854  build() {
855    GridRow({columns: {sm: 4, md: 8, lg: 12}}) {
856      GridCol({span: 4, offset: {sm: 0, md: 2, lg: 4}}) {
857        Column() {
858          Text('是否删除此联系人?')
859            .fontSize(16)
860            .fontColor('#E6000000')
861            .margin({bottom: 8, top: 24, left: 24, right: 24})
862          Row() {
863            Text('取消')
864              .fontColor('#007DFF')
865              .fontSize(16)
866              .layoutWeight(1)
867              .textAlign(TextAlign.Center)
868              .onClick(()=>{
869                if(this.controller){
870                 this.controller.close()
871                }
872                this.cancel!()
873              })
874            Line().width(1).height(24).backgroundColor('#33000000').margin({left: 4, right: 4})
875            Text('删除')
876              .fontColor('#FA2A2D')
877              .fontSize(16)
878              .layoutWeight(1)
879              .textAlign(TextAlign.Center)
880              .onClick(()=>{
881                 if(this.controller){
882                 this.controller.close()
883                }
884                this.confirm!()
885              })
886          }.height(40)
887          .margin({left: 24, right: 24, bottom: 16})
888        }.borderRadius(24).backgroundColor('#FFFFFF')
889      }
890    }.margin({left: 24, right: 24})
891  }
892}
893```
894
895
896
897## 大图浏览
898
899**布局效果**
900
901
902| sm | md | lg |
903| -------- | -------- | -------- |
904| 图片长宽比不变,最长边充满全屏 | 图片长宽比不变,最长边充满全屏 | 图片长宽比不变,最长边充满全屏 |
905| ![大图浏览sm](figures/大图浏览sm.png) | ![大图浏览md](figures/大图浏览md.png) | ![大图浏览lg](figures/大图浏览lg.png) |
906
907**实现方案**
908
909图片通常使用[Image组件](../../reference/apis-arkui/arkui-ts/ts-basic-components-image.md)展示,Image组件的objectFit属性默认为ImageFit.Cover,即保持宽高比进行缩小或者放大以使得图片两边都大于或等于显示边界。在大图浏览场景下,因屏幕与图片的宽高比可能有差异,常常会发生图片被截断的问题。此时只需将Image组件的objectFit属性设置为ImageFit.Contain,即保持宽高比进行缩小或者放大并使得图片完全显示在显示边界内,即可解决该问题。
910
911
912**参考代码**
913
914
915```ts
916@Entry
917@Component
918struct BigImage {
919  build() {
920    Row() {
921      Image($r("app.media.image"))
922        .objectFit(ImageFit.Contain)
923    }
924  }
925}
926```
927
928
929## 操作入口
930
931**布局效果**
932
933| sm | md | lg |
934| -------- | -------- | -------- |
935| 列表项尺寸固定,超出内容可滚动查看 | 列表项尺寸固定,剩余空间均分 | 列表项尺寸固定,剩余空间均分 |
936| ![操作入口sm](figures/操作入口sm.png) | ![操作入口md](figures/操作入口md.png) | ![操作入口lg](figures/操作入口lg.png) |
937
938
939**实现方案**
940
941Scroll(内容超出宽度时可滚动) + Row(横向均分:justifyContent(FlexAlign.SpaceAround)、 最小宽度约束:constraintSize({ minWidth: '100%' })
942
943
944**参考代码**
945
946
947```ts
948interface OperationItem {
949  name: string
950  icon: Resource
951}
952
953@Entry
954@Component
955export default struct OperationEntries {
956  @State listData: Array<OperationItem> = [
957    { name: '私人FM', icon: $r('app.media.self_fm') },
958    { name: '歌手', icon: $r('app.media.singer') },
959    { name: '歌单', icon: $r('app.media.song_list') },
960    { name: '排行榜', icon: $r('app.media.rank') },
961    { name: '热门', icon: $r('app.media.hot') },
962    { name: '运动音乐', icon: $r('app.media.sport') },
963    { name: '音乐FM', icon: $r('app.media.audio_fm') },
964    { name: '福利', icon: $r('app.media.bonus') }]
965
966  build() {
967    Scroll() {
968      Row() {
969        ForEach(this.listData, (item:OperationItem) => {
970          Column() {
971            Image(item.icon)
972              .width(48)
973              .aspectRatio(1)
974            Text(item.name)
975              .margin({ top: 8 })
976              .fontSize(16)
977          }
978          .justifyContent(FlexAlign.Center)
979          .height(104)
980          .padding({ left: 12, right: 12 })
981        })
982      }
983      .constraintSize({ minWidth: '100%' }).justifyContent(FlexAlign.SpaceAround)
984    }
985    .width('100%')
986    .scrollable(ScrollDirection.Horizontal)
987  }
988}
989```
990
991
992## 顶部
993
994
995**布局效果**
996
997
998| sm | md | lg |
999| -------- | -------- | -------- |
1000| 标题和搜索框两行显示 | 标题和搜索框一行显示 | 标题和搜索框一行显示 |
1001| ![顶部布局sm](figures/顶部布局sm.png) | ![顶部布局md](figures/顶部布局md.png) | ![顶部布局lg](figures/顶部布局lg.png) |
1002
1003**实现方案**
1004
1005最外层使用栅格行组件GridRow布局
1006
1007文本标题使用栅格列组件GridCol
1008
1009搜索框使用栅格列组件GridCol
1010
1011
1012**参考代码**
1013
1014
1015```ts
1016@Entry
1017@Component
1018export default struct Header {
1019  @State needWrap: boolean = true
1020
1021  build() {
1022    GridRow() {
1023      GridCol({ span: { sm: 12, md: 6, lg: 7 } }) {
1024        Row() {
1025          Text('推荐').fontSize(24)
1026          Blank()
1027          Image($r('app.media.ic_public_more'))
1028            .width(32)
1029            .height(32)
1030            .objectFit(ImageFit.Contain)
1031            .visibility(this.needWrap ? Visibility.Visible : Visibility.None)
1032        }
1033        .width('100%').height(40)
1034        .alignItems(VerticalAlign.Center)
1035      }
1036
1037      GridCol({ span: { sm: 12, md: 6, lg: 5 } }) {
1038        Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
1039          Search({ placeholder: '猜您喜欢: 万水千山' })
1040            .placeholderFont({ size: 16 })
1041            .margin({ top: 4, bottom: 4 })
1042          Image($r('app.media.audio_fm'))
1043            .width(32)
1044            .height(32)
1045            .objectFit(ImageFit.Contain)
1046            .flexShrink(0)
1047            .margin({ left: 12 })
1048          Image($r('app.media.ic_public_more'))
1049            .width(32)
1050            .height(32)
1051            .objectFit(ImageFit.Contain)
1052            .flexShrink(0)
1053            .margin({ left: 12 })
1054            .visibility(this.needWrap ? Visibility.None : Visibility.Visible)
1055        }
1056      }
1057    }.onBreakpointChange((breakpoint: string) => {
1058      if (breakpoint === 'sm') {
1059        this.needWrap = true
1060      } else {
1061        this.needWrap = false
1062      }
1063    })
1064    .padding({ left: 12, right: 12 })
1065  }
1066}
1067```
1068
1069
1070## 缩进布局
1071
1072
1073**布局效果**
1074
1075
1076  | sm | md | lg |
1077| -------- | -------- | -------- |
1078| 栅格总列数为4,内容占满所有列 | 栅格总列数为8,内容占中间6列。 | 栅格总列数为12,内容占中间8列。 |
1079| ![indent_sm](figures/indent_sm.jpg) | ![indent_md](figures/indent_md.jpg) | ![indent_lg](figures/indent_lg.jpg) |
1080
1081
1082**实现方案**
1083
1084借助[栅格组件](../../reference/apis-arkui/arkui-ts/ts-container-gridrow.md),控制待显示内容在不同的断点下占据不同的列数,即可实现不同设备上的缩进效果。另外还可以调整不同断点下栅格组件与两侧的间距,获得更好的显示效果。
1085
1086
1087**参考代码**
1088
1089
1090```ts
1091@Entry
1092@Component
1093struct IndentationSample {
1094  @State private gridMargin: number = 24
1095  build() {
1096    Row() {
1097      GridRow({columns: {sm: 4, md: 8, lg: 12}, gutter: 24}) {
1098        GridCol({span: {sm: 4, md: 6, lg: 8}, offset: {md: 1, lg: 2}}) {
1099          Column() {
1100            ForEach([0, 1, 2, 4], () => {
1101              Column() {
1102                ItemContent()
1103              }
1104            })
1105          }.width('100%')
1106        }
1107      }
1108      .margin({left: this.gridMargin, right: this.gridMargin})
1109      .onBreakpointChange((breakpoint: string) => {
1110        if (breakpoint === 'lg') {
1111          this.gridMargin = 48
1112        } else if (breakpoint === 'md') {
1113          this.gridMargin = 32
1114        } else {
1115          this.gridMargin = 24
1116        }
1117      })
1118    }
1119    .height('100%')
1120    .alignItems((VerticalAlign.Center))
1121    .backgroundColor('#F1F3f5')
1122  }
1123}
1124
1125@Component
1126struct ItemContent {
1127  build() {
1128    Column() {
1129      Row() {
1130        Row() {
1131        }
1132        .width(28)
1133        .height(28)
1134        .borderRadius(14)
1135        .margin({ right: 15 })
1136        .backgroundColor('#E4E6E8')
1137
1138        Row() {
1139        }
1140        .width('30%').height(20).borderRadius(4)
1141        .backgroundColor('#E4E6E8')
1142      }.width('100%').height(28)
1143
1144      Row() {
1145      }
1146      .width('100%')
1147      .height(68)
1148      .borderRadius(16)
1149      .margin({ top: 12 })
1150      .backgroundColor('#E4E6E8')
1151    }
1152    .height(128)
1153    .borderRadius(24)
1154    .backgroundColor('#FFFFFF')
1155    .padding({ top: 12, bottom: 12, left: 18, right: 18 })
1156    .margin({ bottom: 12 })
1157  }
1158}
1159```
1160
1161
1162## 挪移布局
1163
1164**布局效果**
1165
1166  | sm | md | lg |
1167| -------- | -------- | -------- |
1168| 图片和文字上下布局 | 图片和文字左右布局 | 图片和文字左右布局 |
1169| ![diversion_sm](figures/diversion_sm.jpg) | ![diversion_md](figures/diversion_md.jpg) | ![diversion_lg](figures/diversion_lg.jpg) |
1170
1171
1172**实现方案**
1173
1174不同断点下,栅格子元素占据的列数会随着开发者的配置发生改变。当一行中的列数超过栅格组件在该断点的总列数时,可以自动换行,即实现”上下布局”与”左右布局”之间切换的效果。
1175
1176
1177**参考代码**
1178
1179
1180```ts
1181@Entry
1182@Component
1183struct DiversionSample {
1184  @State private currentBreakpoint: string = 'md'
1185  @State private imageHeight: number = 0
1186  build() {
1187    Row() {
1188      GridRow() {
1189        GridCol({span: {sm: 12, md: 6, lg: 6}}) {
1190          Image($r('app.media.illustrator'))
1191          .aspectRatio(1)
1192          .onAreaChange((oldValue: Area, newValue: Area) => {
1193            this.imageHeight = Number(newValue.height)
1194          })
1195          .margin({left: 12, right: 12})
1196        }
1197
1198        GridCol({span: {sm: 12, md: 6, lg: 6}}) {
1199          Column(){
1200            Text($r('app.string.user_improvement'))
1201              .textAlign(TextAlign.Center)
1202              .fontSize(20)
1203              .fontWeight(FontWeight.Medium)
1204            Text($r('app.string.user_improvement_tips'))
1205              .textAlign(TextAlign.Center)
1206              .fontSize(14)
1207              .fontWeight(FontWeight.Medium)
1208          }
1209          .margin({left: 12, right: 12})
1210          .justifyContent(FlexAlign.Center)
1211          .height(this.currentBreakpoint === 'sm' ? 100 : this.imageHeight)
1212        }
1213      }.onBreakpointChange((breakpoint: string) => {
1214        this.currentBreakpoint = breakpoint;
1215      })
1216    }
1217    .height('100%')
1218    .alignItems((VerticalAlign.Center))
1219    .backgroundColor('#F1F3F5')
1220  }
1221}
1222```
1223
1224
1225## 重复布局
1226
1227**布局效果**
1228
1229| sm | md | lg |
1230| -------- | -------- | -------- |
1231| 单列显示,共8个元素<br>可以通过上下滑动查看不同的元素 | 双列显示,共8个元素 | 双列显示,共8个元素 |
1232| ![repeat_sm](figures/repeat_sm.jpg) | ![repeat_md](figures/repeat_md.jpg)  | ![repeat_lg](figures/repeat_lg.jpg) |
1233
1234
1235**实现方案**
1236
1237不同断点下,配置栅格子组件占据不同的列数,即可实现“小屏单列显示、大屏双列显示”的效果。另外,还可以通过栅格组件的onBreakpointChange事件,调整页面中显示的元素数量。
1238
1239
1240**参考代码**
1241
1242
1243```ts
1244@Entry
1245@Component
1246struct RepeatSample {
1247  @State private currentBreakpoint: string = 'md'
1248  @State private listItems: number[] = [1, 2, 3, 4, 5, 6, 7, 8]
1249  @State private gridMargin: number = 24
1250
1251  build() {
1252    Row() {
1253      // 当目标区域不足以显示所有元素时,可以通过上下滑动查看不同的元素
1254      Scroll() {
1255        GridRow({gutter: 24}) {
1256          ForEach(this.listItems, () => {
1257           // 通过配置元素在不同断点下占的列数,实现不同的布局效果
1258            GridCol({span: {sm: 12, md: 6, lg: 6}}) {
1259              Column() {
1260                RepeatItemContent()
1261              }
1262            }
1263          })
1264        }
1265        .margin({left: this.gridMargin, right: this.gridMargin})
1266        .onBreakpointChange((breakpoint: string) => {
1267          this.currentBreakpoint = breakpoint;
1268          if (breakpoint === 'lg') {
1269            this.gridMargin = 48
1270          } else if (breakpoint === 'md') {
1271            this.gridMargin = 32
1272          } else {
1273            this.gridMargin = 24
1274          }
1275        })
1276      }.height(348)
1277    }
1278    .height('100%')
1279    .backgroundColor('#F1F3F5')
1280  }
1281}
1282
1283@Component
1284struct RepeatItemContent {
1285  build() {
1286    Flex() {
1287      Row() {
1288      }
1289      .width(43)
1290      .height(43)
1291      .borderRadius(12)
1292      .backgroundColor('#E4E6E8')
1293      .flexGrow(0)
1294
1295      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.SpaceAround }) {
1296        Row() {
1297        }
1298        .height(10)
1299        .width('80%')
1300        .backgroundColor('#E4E6E8')
1301
1302        Row() {
1303        }
1304        .height(10)
1305        .width('50%')
1306        .backgroundColor('#E4E6E8')
1307      }
1308      .flexGrow(1)
1309      .margin({ left: 13 })
1310    }
1311    .padding({ top: 13, bottom: 13, left: 13, right: 37 })
1312    .height(69)
1313    .backgroundColor('#FFFFFF')
1314    .borderRadius(24)
1315  }
1316}
1317```
1318