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