一、簡介

模態轉場是新的界面覆蓋在舊的界面上,舊的界面不消失的一種轉場方式。

模態轉場接口

接口

説明

使用場景

bindContentCover

彈出全屏的模態組件。

用於自定義全屏的模態展示界面,結合轉場動畫和共享元素動畫可實現複雜轉場動畫效果,如縮略圖片點擊後查看大圖。

bindSheet

彈出半模態組件。

用於半模態展示界面,如分享框。

bindMenu

彈出菜單,點擊組件後彈出。

需要Menu菜單的場景,如一般應用的“+”號鍵。

bindContextMenu

彈出菜單,長按或者右鍵點擊後彈出。

長按浮起效果,一般結合拖拽框架使用,如桌面圖標長按浮起。

bindPopup

彈出Popup彈框。

Popup彈框場景,如點擊後對某個組件進行臨時説明。

if

通過if新增或刪除組件。

用來在某個狀態下臨時顯示一個界面,這種方式的返回導航需要由開發者監聽接口實現。

二、使用bindContentCover構建全屏模態轉場效果

bindContentCover接口用於為組件綁定全屏模態頁面,在組件出現和消失時可通過設置轉場參數ModalTransition添加過渡動效。

  1. 定義全屏模態轉場效果bindContentCover
  2. 定義模態展示界面。
// 通過@Builder構建模態展示界面
@Builder MyBuilder() {
  Column() {
    Text('my model view')
  }
  // 通過轉場動畫實現出現消失轉場動畫效果,transition需要加在builder下的第一個組件 
  .transition(TransitionEffect.translate({ y: 1000 }).animation({ curve: curves.springMotion(0.6, 0.8) }))
}
  1. 通過模態接口調起模態展示界面,通過轉場動畫或者共享元素動畫去實現對應的動畫效果。
// 模態轉場控制變量
@State isPresent: boolean = false;

Button('Click to present model view')
  // 通過選定的模態接口,綁定模態展示界面,ModalTransition是內置的ContentCover轉場動畫類型,這裏選擇None代表系統不加默認動畫,通過onDisappear控制狀態變量變換
  .bindContentCover(this.isPresent, this.MyBuilder(), {
            modalTransition: ModalTransition.NONE,
            onDisappear: () => {
              if (this.isPresent) {
                this.isPresent = !this.isPresent;
              }
            }
          })
  .onClick(() => {
    // 改變狀態變量,顯示模態界面
    this.isPresent = !this.isPresent;
  })
效果圖

HarmonyOS:轉場動畫-模態轉場_鴻蒙

示例代碼

import { curves } from '@kit.ArkUI';

interface PersonList {
  name: string,
  cardNum: string
}

@Entry
@Component
struct BindContentCoverDemo {
  private personList: Array<PersonList> = [
    { name: '王**', cardNum: '1234***********789' },
    { name: '宋*', cardNum: '2345***********789' },
    { name: '許**', cardNum: '3456***********789' },
    { name: '唐*', cardNum: '4567***********789' }
  ];
  // 第一步:定義全屏模態轉場效果bindContentCover
  // 模態轉場控制變量
  @State isPresent: boolean = false;

  // 第二步:定義模態展示界面
  // 通過@Builder構建模態展示界面
  @Builder
  MyBuilder() {
    Column() {
      Row() {
        Text('選擇乘車人')
          .fontSize(20)
          .fontColor(Color.White)
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 30, bottom: 15 })
      }
      .backgroundColor(0x007dfe)

      Row() {
        Text('+ 添加乘車人')
          .fontSize(16)
          .fontColor(0x333333)
          .margin({ top: 10 })
          .padding({ top: 20, bottom: 20 })
          .width('92%')
          .borderRadius(10)
          .textAlign(TextAlign.Center)
          .backgroundColor(Color.White)
      }

      Column() {
        ForEach(this.personList, (item: PersonList, index: number) => {
          Row() {
            Column() {
              if (index % 2 == 0) {
                Column()
                  .width(20)
                  .height(20)
                  .border({ width: 1, color: 0x007dfe })
                  .backgroundColor(0x007dfe)
              } else {
                Column()
                  .width(20)
                  .height(20)
                  .border({ width: 1, color: 0x007dfe })
              }
            }
            .width('20%')

            Column() {
              Text(item.name)
                .fontColor(0x333333)
                .fontSize(18)
              Text(item.cardNum)
                .fontColor(0x666666)
                .fontSize(14)
            }
            .width('60%')
            .alignItems(HorizontalAlign.Start)

            Column() {
              Text('編輯')
                .fontColor(0x007dfe)
                .fontSize(16)
            }
            .width('20%')
          }
          .padding({ top: 10, bottom: 10 })
          .border({ width: { bottom: 1 }, color: 0xf1f1f1 })
          .width('92%')
          .backgroundColor(Color.White)
        })
      }
      .padding({ top: 20, bottom: 20 })

      Text('確認')
        .width('90%')
        .height(40)
        .textAlign(TextAlign.Center)
        .borderRadius(10)
        .fontColor(Color.White)
        .backgroundColor(0x007dfe)
        .onClick(() => {
          this.isPresent = !this.isPresent;
        })
    }
    .size({ width: '100%', height: '100%' })
    .backgroundColor(0xf5f5f5)
    // 通過轉場動畫實現出現消失轉場動畫效果
    .transition(TransitionEffect.translate({ y: 1000 }).animation({ curve: curves.springMotion(0.6, 0.8) }))
  }

  build() {
    Column() {
      Row() {
        Text('確認訂單')
          .fontSize(20)
          .fontColor(Color.White)
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 30, bottom: 60 })
      }
      .backgroundColor(0x007dfe)

      Column() {
        Row() {
          Column() {
            Text('00:25')
            Text('始發站')
          }
          .width('30%')

          Column() {
            Text('G1234')
            Text('8時1分')
          }
          .width('30%')

          Column() {
            Text('08:26')
            Text('終點站')
          }
          .width('30%')
        }
      }
      .width('92%')
      .padding(15)
      .margin({ top: -30 })
      .backgroundColor(Color.White)
      .shadow({ radius: 30, color: '#aaaaaa' })
      .borderRadius(10)

      Column() {
        Text('+ 選擇乘車人')
          .fontSize(18)
          .fontColor(Color.Orange)
          .fontWeight(FontWeight.Bold)
          .padding({ top: 10, bottom: 10 })
          .width('60%')
          .textAlign(TextAlign.Center)
          .borderRadius(15)// 通過選定的模態接口,綁定模態展示界面,ModalTransition是內置的ContentCover轉場動畫類型,這裏選擇DEFAULT代表設置上下切換動畫效果,通過onDisappear控制狀態變量變換。
          .bindContentCover(this.isPresent, this.MyBuilder(), {
            modalTransition: ModalTransition.DEFAULT,
            onDisappear: () => {
              if (this.isPresent) {
                this.isPresent = !this.isPresent;
              }
            }
          })
          .onClick(() => {
            // 第三步:通過模態接口調起模態展示界面,通過轉場動畫或者共享元素動畫去實現對應的動畫效果
            // 改變狀態變量,顯示模態界面
            this.isPresent = !this.isPresent;
          })
      }
      .padding({ top: 60 })
    }
  }
}

三、使用bindSheet構建半模態轉場效果

bindSheet屬性可為組件綁定半模態頁面,在組件出現時可通過設置自定義或默認的內置高度確定半模態大小。構建半模態轉場動效的步驟基本與使用bindContentCover構建全屏模態轉場動效相同。

效果圖

HarmonyOS:轉場動畫-模態轉場_HarmonyOS_02

示例代碼

@Entry
@Component
struct BindSheetDemo {
  // 半模態轉場顯示隱藏控制
  @State isShowSheet: boolean = false;
  private menuList: string[] = ['不要辣', '少放辣', '多放辣', '不要香菜', '不要香葱', '不要一次性餐具', '需要一次性餐具'];

  // 通過@Builder構建半模態展示界面
  @Builder
  mySheet() {
    Column() {
      Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {
        ForEach(this.menuList, (item: string) => {
          Text(item)
            .fontSize(16)
            .fontColor(0x333333)
            .backgroundColor(0xf1f1f1)
            .borderRadius(8)
            .margin(10)
            .padding(10)
        })
      }
      .padding({ top: 18 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
  }

  build() {
    Column() {
      Text('口味與餐具')
        .fontSize(28)
        .padding({ top: 30, bottom: 30 })
      Column() {
        Row() {
          Row()
            .width(10)
            .height(10)
            .backgroundColor('#a8a8a8')
            .margin({ right: 12 })
            .borderRadius(20)

          Column() {
            Text('選擇點餐口味和餐具')
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
          }
          .alignItems(HorizontalAlign.Start)

          Blank()

          Row()
            .width(12)
            .height(12)
            .margin({ right: 15 })
            .border({
              width: { top: 2, right: 2 },
              color: 0xcccccc
            })
            .rotate({ angle: 45 })
        }
        .borderRadius(15)
        .shadow({ radius: 100, color: '#ededed' })
        .width('90%')
        .alignItems(VerticalAlign.Center)
        .padding({ left: 15, top: 15, bottom: 15 })
        .backgroundColor(Color.White)
        // 通過選定的半模態接口,綁定模態展示界面,style中包含兩個參數,一個是設置半模態的高度,不設置時默認高度是Large,一個是是否顯示控制條DragBar,默認是true顯示控制條,通過onDisappear控制狀態變量變換。
        .bindSheet(this.isShowSheet, this.mySheet(), {
          height: 300,
          dragBar: false,
          onDisappear: () => {
            this.isShowSheet = !this.isShowSheet;
          }
        })
        .onClick(() => {
          this.isShowSheet = !this.isShowSheet;
        })
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor(0xf1f1f1)
  }
}

四、使用bindMenu實現菜單彈出效果

bindMenu為組件綁定彈出式菜單,通過點擊觸發。完整示例和效果如下。

效果圖

HarmonyOS:轉場動畫-模態轉場_HarmonyOS_03

示例代碼

class BMD{
  value:ResourceStr = ''
  action:() => void = () => {}
}
@Entry
@Component
struct BindMenuDemo {

  // 第一步: 定義一組數據用來表示菜單按鈕項
  @State items:BMD[] = [
    {
      value: '菜單項1',
      action: () => {
        console.info('handle Menu1 select')
      }
    },
    {
      value: '菜單項2',
      action: () => {
        console.info('handle Menu2 select')
      }
    },
  ]

  build() {
    Column() {
      Button('click')
        .backgroundColor(0x409eff)
        .borderRadius(5)
          // 第二步: 通過bindMenu接口將菜單數據綁定給元素
        .bindMenu(this.items)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height(437)
  }
}

五、使用bindContextMenu實現菜單彈出效果

bindContextMenu為組件綁定彈出式菜單,通過長按或右鍵點擊觸發。
完整示例和效果如下。

效果圖

HarmonyOS:轉場動畫-模態轉場_鴻蒙_04

示例代碼

@Entry
@Component
struct BindContextMenuDemo {
  private menu: string[] = ['保存圖片', '收藏', '搜一搜'];
  // $r('app.media.xxx')需要替換為開發者所需的圖像資源文件。
  private pics: Resource[] = [$r('app.media.tutorial_pic1'), $r('app.media.tutorial_pic2')];

  // 通過@Builder構建自定義菜單項
  @Builder
  myMenu() {
    Column() {
      ForEach(this.menu, (item: string) => {
        Row() {
          Text(item)
            .fontSize(18)
            .width('100%')
            .textAlign(TextAlign.Center)
        }
        .padding(15)
        .border({ width: { bottom: 1 }, color: 0xcccccc })
      })
    }
    .width(140)
    .borderRadius(15)
    .shadow({ radius: 15, color: 0xf1f1f1 })
    .backgroundColor(0xf1f1f1)
  }

  build() {
    Column() {
      Row() {
        Text('查看圖片')
          .fontSize(20)
          .fontColor(Color.White)
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 20, bottom: 20 })
      }
      .backgroundColor(0x007dfe)

      Column() {
        ForEach(this.pics, (item: Resource) => {
          Row() {
            Image(item)
              .width('100%')
              .draggable(false)
          }
          .padding({
            top: 20,
            bottom: 20,
            left: 10,
            right: 10
          })
          .bindContextMenu(this.myMenu, ResponseType.LongPress)
        })
      }
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

六、使用bindPopup實現氣泡彈窗效果

bindPopup屬性可為組件綁定彈窗,並設置彈窗內容,交互邏輯和顯示狀態。
完整示例和代碼如下。

效果圖

HarmonyOS:轉場動畫-模態轉場_鴻蒙_05

@Entry
@Component
struct BindPopupDemo {

  // 第一步:定義變量控制彈窗顯示
  @State customPopup: boolean = false;

  // 第二步:popup構造器定義彈框內容
  @Builder popupBuilder() {
    Column({ space: 2 }) {
      Row().width(64)
        .height(64)
        .backgroundColor(0x409eff)
      Text('Popup')
        .fontSize(10)
        .fontColor(Color.White)
    }
    .justifyContent(FlexAlign.SpaceAround)
    .width(100)
    .height(100)
    .padding(5)
  }

  build() {
    Column() {

      Button('click')
        // 第四步:創建點擊事件,控制彈窗顯隱
        .onClick(() => {
          this.customPopup = !this.customPopup;
        })
        .backgroundColor(0xf56c6c)
          // 第三步:使用bindPopup接口將彈窗內容綁定給元素
        .bindPopup(this.customPopup, {
          builder: this.popupBuilder,
          placement: Placement.Top,
          maskColor: 0x33000000,
          popupColor: 0xf56c6c,
          enableArrow: true,
          onStateChange: (e) => {
            if (!e.isVisible) {
              this.customPopup = false;
            }
          }
        })
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height(437)
  }
}

七、使用if實現模態轉場

上述模態轉場接口需要綁定到其他組件上,通過監聽狀態變量改變調起模態界面。同時,也可以通過if範式,通過新增/刪除組件實現模態轉場效果。
完整示例和代碼如下。

效果圖

HarmonyOS:轉場動畫-模態轉場_HarmonyOS_06

示例代碼

@Entry
@Component
struct ModalTransitionWithIf {
  private listArr: string[] = ['WLAN', '藍牙', '個人熱點', '連接與共享'];
  private shareArr: string[] = ['投屏', '打印', 'VPN', '私人DNS', 'NFC'];
  // 第一步:定義狀態變量控制頁面顯示
  @State isShowShare: boolean = false;
  private shareFunc(): void {
    this.getUIContext()?.animateTo({ duration: 500 }, () => {
      this.isShowShare = !this.isShowShare;
    })
  }

  build(){
    // 第二步:定義Stack佈局顯示當前頁面和模態頁面
    Stack() {
      Column() {
        Column() {
          Text('設置')
            .fontSize(28)
            .fontColor(0x333333)
        }
        .width('90%')
        .padding({ top: 30, bottom: 15 })
        .alignItems(HorizontalAlign.Start)

        TextInput({ placeholder: '輸入關鍵字搜索' })
          .width('90%')
          .height(40)
          .margin({ bottom: 10 })
          .focusable(false)

        List({ space: 12, initialIndex: 0 }) {
          ForEach(this.listArr, (item: string, index: number) => {
            ListItem() {
              Row() {
                Row() {
                  Text(`${item.slice(0, 1)}`)
                    .fontColor(Color.White)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                }
                .width(30)
                .height(30)
                .backgroundColor('#a8a8a8')
                .margin({ right: 12 })
                .borderRadius(20)
                .justifyContent(FlexAlign.Center)

                Column() {
                  Text(item)
                    .fontSize(16)
                    .fontWeight(FontWeight.Medium)
                }
                .alignItems(HorizontalAlign.Start)

                Blank()

                Row()
                  .width(12)
                  .height(12)
                  .margin({ right: 15 })
                  .border({
                    width: { top: 2, right: 2 },
                    color: 0xcccccc
                  })
                  .rotate({ angle: 45 })
              }
              .borderRadius(15)
              .shadow({ radius: 100, color: '#ededed' })
              .width('90%')
              .alignItems(VerticalAlign.Center)
              .padding({ left: 15, top: 15, bottom: 15 })
              .backgroundColor(Color.White)
            }
            .width('100%')
            .onClick(() => {
              // 第五步:改變狀態變量,顯示模態頁面
              if(item.slice(-2) === '共享'){
                this.shareFunc();
              }
            })
          }, (item: string): string => item)
        }
        .width('100%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor(0xfefefe)

      // 第三步:在if中定義模態頁面,顯示在最上層,通過if控制模態頁面出現消失
      if(this.isShowShare){
        Column() {
          Column() {
            Row() {
              Row() {
                Row()
                  .width(16)
                  .height(16)
                  .border({
                    width: { left: 2, top: 2 },
                    color: 0x333333
                  })
                  .rotate({ angle: -45 })
              }
              .padding({ left: 15, right: 10 })
              .onClick(() => {
                this.shareFunc();
              })
              Text('連接與共享')
                .fontSize(28)
                .fontColor(0x333333)
            }
            .padding({ top: 30 })
          }
          .width('90%')
          .padding({bottom: 15})
          .alignItems(HorizontalAlign.Start)

          List({ space: 12, initialIndex: 0 }) {
            ForEach(this.shareArr, (item: string) => {
              ListItem() {
                Row() {
                  Row() {
                    Text(`${item.slice(0, 1)}`)
                      .fontColor(Color.White)
                      .fontSize(14)
                      .fontWeight(FontWeight.Bold)
                  }
                  .width(30)
                  .height(30)
                  .backgroundColor('#a8a8a8')
                  .margin({ right: 12 })
                  .borderRadius(20)
                  .justifyContent(FlexAlign.Center)

                  Column() {
                    Text(item)
                      .fontSize(16)
                      .fontWeight(FontWeight.Medium)
                  }
                  .alignItems(HorizontalAlign.Start)

                  Blank()

                  Row()
                    .width(12)
                    .height(12)
                    .margin({ right: 15 })
                    .border({
                      width: { top: 2, right: 2 },
                      color: 0xcccccc
                    })
                    .rotate({ angle: 45 })
                }
                .borderRadius(15)
                .shadow({ radius: 100, color: '#ededed' })
                .width('90%')
                .alignItems(VerticalAlign.Center)
                .padding({ left: 15, top: 15, bottom: 15 })
                .backgroundColor(Color.White)
              }
              .width('100%')
            }, (item: string): string => item)
          }
          .width('100%')
        }
        .width('100%')
        .height('100%')
        .backgroundColor(0xffffff)
        // 第四步:定義模態頁面出現消失轉場方式
        .transition(TransitionEffect.OPACITY
          .combine(TransitionEffect.translate({ x: '100%' }))
          .combine(TransitionEffect.scale({ x: 0.95, y: 0.95 })))
      }
    }
  }
}