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![zh-cn_image_0000001646921957](figures/zh-cn_image_0000001646921957.gif)
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![zh-cn_image_0000001599977924](figures/zh-cn_image_0000001599977924.gif)
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![zh-cn_image_0000001599643478](figures/zh-cn_image_0000001599643478.gif)
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![zh-cn_image_0000001600137920](figures/zh-cn_image_0000001600137920.gif)
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![zh-cn_image_0000001649282285](figures/zh-cn_image_0000001649282285.gif)
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![zh-cn_image_0000001597792146](figures/zh-cn_image_0000001597792146.gif)
717