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|  |  |  | 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|  |  |  | 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|  |  |  | 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|  |  |  | 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|  |  |  | 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|  |  |  | 556|  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|  |  |  | 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|  |  |  | 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|  |  |  | 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|  |  |  | 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|  |  |  | 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|  |  |  | 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|  |  |  | 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