1# 模态转场 2 3 4模态转场是新的界面覆盖在旧的界面上,旧的界面不消失的一种转场方式。 5 6 7**表1** 模态转场接口 8| 接口 | 说明 | 使用场景 | 9| ---------------------------------------- | ----------------- | ---------------------------------------- | 10| [bindContentCover](../reference/apis-arkui/arkui-ts/ts-universal-attributes-modal-transition.md) | 弹出全屏的模态组件。 | 用于自定义全屏的模态展示界面,结合转场动画和共享元素动画可实现复杂转场动画效果,如缩略图片点击后查看大图。 | 11| [bindSheet](../reference/apis-arkui/arkui-ts/ts-universal-attributes-sheet-transition.md) | 弹出半模态组件。 | 用于半模态展示界面,如分享框。 | 12| [bindMenu](../reference/apis-arkui/arkui-ts/ts-universal-attributes-menu.md) | 弹出菜单,点击组件后弹出。 | 需要Menu菜单的场景,如一般应用的“+”号键。 | 13| [bindContextMenu](../reference/apis-arkui/arkui-ts/ts-universal-attributes-menu.md) | 弹出菜单,长按或者右键点击后弹出。 | 长按浮起效果,一般结合拖拽框架使用,如桌面图标长按浮起。 | 14| [bindPopup](../reference/apis-arkui/arkui-ts/ts-universal-attributes-popup.md) | 弹出Popup弹框。 | Popup弹框场景,如点击后对某个组件进行临时说明。 | 15| if | 通过if新增或删除组件。 | 用来在某个状态下临时显示一个界面,这种方式的返回导航需要由开发者监听接口实现。 | 16 17 18## 使用bindContentCover构建全屏模态转场效果 19 20[bindContentCover](../reference/apis-arkui/arkui-ts/ts-universal-attributes-modal-transition.md)接口用于为组件绑定全屏模态页面,在组件出现和消失时可通过设置转场参数ModalTransition添加过渡动效。 21 221. 定义全屏模态转场效果[bindContentCover](../reference/apis-arkui/arkui-ts/ts-universal-attributes-modal-transition.md)。 23 242. 定义模态展示界面。 25 26 ```ts 27 // 通过@Builder构建模态展示界面 28 @Builder MyBuilder() { 29 Column() { 30 Text('my model view') 31 } 32 // 通过转场动画实现出现消失转场动画效果,transition需要加在builder下的第一个组件 33 .transition(TransitionEffect.translate({ y: 1000 }).animation({ curve: curves.springMotion(0.6, 0.8) })) 34 } 35 ``` 36 373. 通过模态接口调起模态展示界面,通过转场动画或者共享元素动画去实现对应的动画效果。 38 39 ```ts 40 // 模态转场控制变量 41 @State isPresent: boolean = false; 42 43 Button('Click to present model view') 44 // 通过选定的模态接口,绑定模态展示界面,ModalTransition是内置的ContentCover转场动画类型,这里选择None代表系统不加默认动画 45 .bindContentCover(this.isPresent, this.MyBuilder, ModalTransition.NONE) 46 .onClick(() => { 47 // 改变状态变量,显示模态界面 48 this.isPresent = !this.isPresent; 49 }) 50 ``` 51 52 53完整示例代码和效果如下。 54 55 56 57```ts 58import curves from '@ohos.curves'; 59 60interface PersonList { 61 name: string, 62 cardnum: string 63} 64 65@Entry 66@Component 67struct BindContentCoverDemo { 68 private personList: Array<PersonList> = [ 69 { name: '王**', cardnum: '1234***********789' }, 70 { name: '宋*', cardnum: '2345***********789' }, 71 { name: '许**', cardnum: '3456***********789' }, 72 { name: '唐*', cardnum: '4567***********789' } 73 ]; 74 75 // 第一步:定义全屏模态转场效果bindContentCover 76 // 模态转场控制变量 77 @State isPresent: boolean = false; 78 79 // 第二步:定义模态展示界面 80 // 通过@Builder构建模态展示界面 81 @Builder MyBuilder() { 82 Column() { 83 Row() { 84 Text('选择乘车人') 85 .fontSize(20) 86 .fontColor(Color.White) 87 .width('100%') 88 .textAlign(TextAlign.Center) 89 .padding({ top: 30, bottom: 15 }) 90 } 91 .backgroundColor(0x007dfe) 92 93 Row() { 94 Text('+ 添加乘车人') 95 .fontSize(16) 96 .fontColor(0x333333) 97 .margin({ top: 10 }) 98 .padding({ top: 20, bottom: 20 }) 99 .width('92%') 100 .borderRadius(10) 101 .textAlign(TextAlign.Center) 102 .backgroundColor(Color.White) 103 } 104 105 Column() { 106 ForEach(this.personList, (item: PersonList, index: number) => { 107 Row() { 108 Column() { 109 if (index % 2 == 0) { 110 Column() 111 .width(20) 112 .height(20) 113 .border({ width: 1, color: 0x007dfe }) 114 .backgroundColor(0x007dfe) 115 } else { 116 Column() 117 .width(20) 118 .height(20) 119 .border({ width: 1, color: 0x007dfe }) 120 } 121 } 122 .width('20%') 123 Column() { 124 Text(item.name) 125 .fontColor(0x333333) 126 .fontSize(18) 127 Text(item.cardnum) 128 .fontColor(0x666666) 129 .fontSize(14) 130 } 131 .width('60%') 132 .alignItems(HorizontalAlign.Start) 133 Column() { 134 Text('编辑') 135 .fontColor(0x007dfe) 136 .fontSize(16) 137 } 138 .width('20%') 139 } 140 .padding({ top: 10, bottom: 10 }) 141 .border({ width: { bottom: 1 }, color: 0xf1f1f1 }) 142 .width('92%') 143 .backgroundColor(Color.White) 144 }) 145 } 146 .padding({ top: 20, bottom: 20 }) 147 148 Text('确认') 149 .width('90%') 150 .height(40) 151 .textAlign(TextAlign.Center) 152 .borderRadius(10) 153 .fontColor(Color.White) 154 .backgroundColor(0x007dfe) 155 .onClick(() => { 156 this.isPresent = !this.isPresent; 157 }) 158 } 159 .size({ width: '100%', height: '100%' }) 160 .backgroundColor(0xf5f5f5) 161 // 通过转场动画实现出现消失转场动画效果 162 .transition(TransitionEffect.translate({ y: 1000 }).animation({ curve: curves.springMotion(0.6, 0.8) })) 163 } 164 165 build() { 166 Column() { 167 Row() { 168 Text('确认订单') 169 .fontSize(20) 170 .fontColor(Color.White) 171 .width('100%') 172 .textAlign(TextAlign.Center) 173 .padding({ top: 30, bottom: 60 }) 174 } 175 .backgroundColor(0x007dfe) 176 177 Column() { 178 Row() { 179 Column() { 180 Text('00:25') 181 Text('始发站') 182 } 183 .width('30%') 184 Column() { 185 Text('G1234') 186 Text('8时1分') 187 } 188 .width('30%') 189 Column() { 190 Text('08:26') 191 Text('终点站') 192 } 193 .width('30%') 194 } 195 } 196 .width('92%') 197 .padding(15) 198 .margin({ top: -30 }) 199 .backgroundColor(Color.White) 200 .shadow({ radius: 30, color: '#aaaaaa' }) 201 .borderRadius(10) 202 203 Column() { 204 Text('+ 选择乘车人') 205 .fontSize(18) 206 .fontColor(Color.Orange) 207 .fontWeight(FontWeight.Bold) 208 .padding({ top: 10, bottom: 10 }) 209 .width('60%') 210 .textAlign(TextAlign.Center) 211 .borderRadius(15) 212 // 通过选定的模态接口,绑定模态展示界面,ModalTransition是内置的ContentCover转场动画类型,这里选择DEFAULT代表设置上下切换动画效果。 213 .bindContentCover(this.isPresent, this.MyBuilder(), ModalTransition.DEFAULT) 214 .onClick(() => { 215 // 第三步:通过模态接口调起模态展示界面,通过转场动画或者共享元素动画去实现对应的动画效果 216 // 改变状态变量,显示模态界面 217 this.isPresent = !this.isPresent; 218 }) 219 } 220 .padding({ top: 60 }) 221 } 222 } 223} 224``` 225 226 227 228 229 230 231 232## 使用bindSheet构建半模态转场效果 233 234[bindSheet](../reference/apis-arkui/arkui-ts/ts-universal-attributes-sheet-transition.md)属性可为组件绑定半模态页面,在组件出现时可通过设置自定义或默认的内置高度确定半模态大小。构建半模态转场动效的步骤基本与使用bindContentCover构建全屏模态转场动效相同。 235 236完整示例和效果如下。 237 238 239```ts 240@Entry 241@Component 242struct BindSheetDemo { 243 // 半模态转场显示隐藏控制 244 @State isShowSheet: boolean = false; 245 private menuList: string[] = ['不要辣', '少放辣', '多放辣', '不要香菜', '不要香葱', '不要一次性餐具', '需要一次性餐具']; 246 // 通过@Builder构建半模态展示界面 247 @Builder mySheet() { 248 Column() { 249 Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) { 250 ForEach(this.menuList, (item: string) => { 251 Text(item) 252 .fontSize(16) 253 .fontColor(0x333333) 254 .backgroundColor(0xf1f1f1) 255 .borderRadius(8) 256 .margin(10) 257 .padding(10) 258 }) 259 } 260 .padding({top: 18}) 261 } 262 .width('100%') 263 .height('100%') 264 .backgroundColor(Color.White) 265 } 266 267 build() { 268 Column() { 269 Text('口味与餐具') 270 .fontSize(28) 271 .padding({ top: 30, bottom: 30 }) 272 Column() { 273 Row() { 274 Row() 275 .width(10) 276 .height(10) 277 .backgroundColor('#a8a8a8') 278 .margin({ right: 12 }) 279 .borderRadius(20) 280 281 Column() { 282 Text('选择点餐口味和餐具') 283 .fontSize(16) 284 .fontWeight(FontWeight.Medium) 285 } 286 .alignItems(HorizontalAlign.Start) 287 288 Blank() 289 290 Row() 291 .width(12) 292 .height(12) 293 .margin({ right: 15 }) 294 .border({ 295 width: { top: 2, right: 2 }, 296 color: 0xcccccc 297 }) 298 .rotate({ angle: 45 }) 299 } 300 .borderRadius(15) 301 .shadow({ radius: 100, color: '#ededed' }) 302 .width('90%') 303 .alignItems(VerticalAlign.Center) 304 .padding({ left: 15, top: 15, bottom: 15 }) 305 .backgroundColor(Color.White) 306 // 通过选定的半模态接口,绑定模态展示界面,style中包含两个参数,一个是设置半模态的高度,不设置时默认高度是Large,一个是是否显示控制条DragBar,默认是true显示控制条 307 .bindSheet(this.isShowSheet, this.mySheet(), { 308 height: 300, 309 dragBar: false 310 }) 311 .onClick(() => { 312 this.isShowSheet = !this.isShowSheet; 313 }) 314 } 315 .width('100%') 316 } 317 .width('100%') 318 .height('100%') 319 .backgroundColor(0xf1f1f1) 320 } 321} 322``` 323 324 325 326 327## 使用bindMenu实现菜单弹出效果 328 329[bindMenu](../reference/apis-arkui/arkui-ts/ts-universal-attributes-menu.md)为组件绑定弹出式菜单,通过点击触发。完整示例和效果如下。 330 331 332```ts 333class BMD{ 334 value:ResourceStr = '' 335 action:() => void = () => {} 336} 337@Entry 338@Component 339struct BindMenuDemo { 340 341 // 第一步: 定义一组数据用来表示菜单按钮项 342 @State items:BMD[] = [ 343 { 344 value: '菜单项1', 345 action: () => { 346 console.info('handle Menu1 select') 347 } 348 }, 349 { 350 value: '菜单项2', 351 action: () => { 352 console.info('handle Menu2 select') 353 } 354 }, 355 ] 356 357 build() { 358 Column() { 359 Button('click') 360 .backgroundColor(0x409eff) 361 .borderRadius(5) 362 // 第二步: 通过bindMenu接口将菜单数据绑定给元素 363 .bindMenu(this.items) 364 } 365 .justifyContent(FlexAlign.Center) 366 .width('100%') 367 .height(437) 368 } 369} 370``` 371 372 373 374 375## 使用bindContextMenu实现菜单弹出效果 376 377[bindContextMenu](../reference/apis-arkui/arkui-ts/ts-universal-attributes-menu.md)为组件绑定弹出式菜单,通过长按或右键点击触发。完整示例和效果如下。 378 379完整示例和效果如下。 380 381 382```ts 383@Entry 384@Component 385struct BindContextMenuDemo { 386 private menu: string[] = ['保存图片', '收藏', '搜一搜']; 387 private pics: Resource[] = [$r('app.media.icon_1'), $r('app.media.icon_2')]; 388 389 // 通过@Builder构建自定义菜单项 390 @Builder myMenu() { 391 Column() { 392 ForEach(this.menu, (item: string) => { 393 Row() { 394 Text(item) 395 .fontSize(18) 396 .width('100%') 397 .textAlign(TextAlign.Center) 398 } 399 .padding(15) 400 .border({ width: { bottom: 1 }, color: 0xcccccc }) 401 }) 402 } 403 .width(140) 404 .borderRadius(15) 405 .shadow({ radius: 15, color: 0xf1f1f1 }) 406 .backgroundColor(0xf1f1f1) 407 } 408 409 build() { 410 Column() { 411 Row() { 412 Text('查看图片') 413 .fontSize(20) 414 .fontColor(Color.White) 415 .width('100%') 416 .textAlign(TextAlign.Center) 417 .padding({ top: 20, bottom: 20 }) 418 } 419 .backgroundColor(0x007dfe) 420 421 Column() { 422 ForEach(this.pics, (item: Resource) => { 423 Row(){ 424 Image(item) 425 .width('100%') 426 } 427 .padding({ top: 20, bottom: 20, left: 10, right: 10 }) 428 .bindContextMenu(this.myMenu, ResponseType.LongPress) 429 }) 430 } 431 } 432 .width('100%') 433 .alignItems(HorizontalAlign.Center) 434 } 435} 436``` 437 438 439 440 441## 使用bindPopUp实现气泡弹窗效果 442 443[bindpopup](../reference/apis-arkui/arkui-ts/ts-universal-attributes-popup.md)属性可为组件绑定弹窗,并设置弹窗内容,交互逻辑和显示状态。 444 445完整示例和代码如下。 446 447 448```ts 449@Entry 450@Component 451struct BindPopupDemo { 452 453 // 第一步:定义变量控制弹窗显示 454 @State customPopup: boolean = false; 455 456 // 第二步:popup构造器定义弹框内容 457 @Builder popupBuilder() { 458 Column({ space: 2 }) { 459 Row().width(64) 460 .height(64) 461 .backgroundColor(0x409eff) 462 Text('Popup') 463 .fontSize(10) 464 .fontColor(Color.White) 465 } 466 .justifyContent(FlexAlign.SpaceAround) 467 .width(100) 468 .height(100) 469 .padding(5) 470 } 471 472 build() { 473 Column() { 474 475 Button('click') 476 // 第四步:创建点击事件,控制弹窗显隐 477 .onClick(() => { 478 this.customPopup = !this.customPopup; 479 }) 480 .backgroundColor(0xf56c6c) 481 // 第三步:使用bindPopup接口将弹窗内容绑定给元素 482 .bindPopup(this.customPopup, { 483 builder: this.popupBuilder, 484 placement: Placement.Top, 485 maskColor: 0x33000000, 486 popupColor: 0xf56c6c, 487 enableArrow: true, 488 onStateChange: (e) => { 489 if (!e.isVisible) { 490 this.customPopup = false; 491 } 492 } 493 }) 494 } 495 .justifyContent(FlexAlign.Center) 496 .width('100%') 497 .height(437) 498 } 499} 500``` 501 502 503 504 505 506 507## 使用if实现模态转场 508 509上述模态转场接口需要绑定到其他组件上,通过监听状态变量改变调起模态界面。同时,也可以通过if范式,通过新增/删除组件实现模态转场效果。 510 511完整示例和代码如下。 512 513 514```ts 515@Entry 516@Component 517struct ModalTransitionWithIf { 518 private listArr: string[] = ['WLAN', '蓝牙', '个人热点', '连接与共享']; 519 private shareArr: string[] = ['投屏', '打印', 'VPN', '私人DNS', 'NFC']; 520 // 第一步:定义状态变量控制页面显示 521 @State isShowShare: boolean = false; 522 private shareFunc(): void { 523 animateTo({ duration: 500 }, () => { 524 this.isShowShare = !this.isShowShare; 525 }) 526 } 527 528 build(){ 529 // 第二步:定义Stack布局显示当前页面和模态页面 530 Stack() { 531 Column() { 532 Column() { 533 Text('设置') 534 .fontSize(28) 535 .fontColor(0x333333) 536 } 537 .width('90%') 538 .padding({ top: 30, bottom: 15 }) 539 .alignItems(HorizontalAlign.Start) 540 541 TextInput({ placeholder: '输入关键字搜索' }) 542 .width('90%') 543 .height(40) 544 .margin({ bottom: 10 }) 545 .focusable(false) 546 547 List({ space: 12, initialIndex: 0 }) { 548 ForEach(this.listArr, (item: string, index: number) => { 549 ListItem() { 550 Row() { 551 Row() { 552 Text(`${item.slice(0, 1)}`) 553 .fontColor(Color.White) 554 .fontSize(14) 555 .fontWeight(FontWeight.Bold) 556 } 557 .width(30) 558 .height(30) 559 .backgroundColor('#a8a8a8') 560 .margin({ right: 12 }) 561 .borderRadius(20) 562 .justifyContent(FlexAlign.Center) 563 564 Column() { 565 Text(item) 566 .fontSize(16) 567 .fontWeight(FontWeight.Medium) 568 } 569 .alignItems(HorizontalAlign.Start) 570 571 Blank() 572 573 Row() 574 .width(12) 575 .height(12) 576 .margin({ right: 15 }) 577 .border({ 578 width: { top: 2, right: 2 }, 579 color: 0xcccccc 580 }) 581 .rotate({ angle: 45 }) 582 } 583 .borderRadius(15) 584 .shadow({ radius: 100, color: '#ededed' }) 585 .width('90%') 586 .alignItems(VerticalAlign.Center) 587 .padding({ left: 15, top: 15, bottom: 15 }) 588 .backgroundColor(Color.White) 589 } 590 .width('100%') 591 .onClick(() => { 592 // 第五步:改变状态变量,显示模态页面 593 if(item.slice(-2) === '共享'){ 594 this.shareFunc(); 595 } 596 }) 597 }, (item: string): string => item) 598 } 599 .width('100%') 600 } 601 .width('100%') 602 .height('100%') 603 .backgroundColor(0xfefefe) 604 605 // 第三步:在if中定义模态页面,显示在最上层,通过if控制模态页面出现消失 606 if(this.isShowShare){ 607 Column() { 608 Column() { 609 Row() { 610 Row() { 611 Row() 612 .width(16) 613 .height(16) 614 .border({ 615 width: { left: 2, top: 2 }, 616 color: 0x333333 617 }) 618 .rotate({ angle: -45 }) 619 } 620 .padding({ left: 15, right: 10 }) 621 .onClick(() => { 622 this.shareFunc(); 623 }) 624 Text('连接与共享') 625 .fontSize(28) 626 .fontColor(0x333333) 627 } 628 .padding({ top: 30 }) 629 } 630 .width('90%') 631 .padding({bottom: 15}) 632 .alignItems(HorizontalAlign.Start) 633 634 List({ space: 12, initialIndex: 0 }) { 635 ForEach(this.shareArr, (item: string) => { 636 ListItem() { 637 Row() { 638 Row() { 639 Text(`${item.slice(0, 1)}`) 640 .fontColor(Color.White) 641 .fontSize(14) 642 .fontWeight(FontWeight.Bold) 643 } 644 .width(30) 645 .height(30) 646 .backgroundColor('#a8a8a8') 647 .margin({ right: 12 }) 648 .borderRadius(20) 649 .justifyContent(FlexAlign.Center) 650 651 Column() { 652 Text(item) 653 .fontSize(16) 654 .fontWeight(FontWeight.Medium) 655 } 656 .alignItems(HorizontalAlign.Start) 657 658 Blank() 659 660 Row() 661 .width(12) 662 .height(12) 663 .margin({ right: 15 }) 664 .border({ 665 width: { top: 2, right: 2 }, 666 color: 0xcccccc 667 }) 668 .rotate({ angle: 45 }) 669 } 670 .borderRadius(15) 671 .shadow({ radius: 100, color: '#ededed' }) 672 .width('90%') 673 .alignItems(VerticalAlign.Center) 674 .padding({ left: 15, top: 15, bottom: 15 }) 675 .backgroundColor(Color.White) 676 } 677 .width('100%') 678 }, (item: string): string => item) 679 } 680 .width('100%') 681 } 682 .width('100%') 683 .height('100%') 684 .backgroundColor(0xffffff) 685 // 第四步:定义模态页面出现消失转场方式 686 .transition(TransitionEffect.OPACITY 687 .combine(TransitionEffect.translate({ x: '100%' })) 688 .combine(TransitionEffect.scale({ x: 0.95, y: 0.95 }))) 689 } 690 } 691 } 692} 693``` 694 695 696