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