• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![zh-cn_image_0000001646921957](figures/zh-cn_image_0000001646921957.gif)
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![zh-cn_image_0000001599977924](figures/zh-cn_image_0000001599977924.gif)
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![zh-cn_image_0000001599643478](figures/zh-cn_image_0000001599643478.gif)
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![zh-cn_image_0000001600137920](figures/zh-cn_image_0000001600137920.gif)
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![zh-cn_image_0000001649282285](figures/zh-cn_image_0000001649282285.gif)
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![zh-cn_image_0000001597792146](figures/zh-cn_image_0000001597792146.gif)
724