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