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