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