Bootstrap

鸿蒙UI系统组件18——模态交互框(ModelDialog)

1、概 述

模态(Modal)指的是UI组件或视图的一种状态。在模态组件消失前,用户只能对处于模态的组件或视图进行响应,不能操作其他非模态的组件或视图。

ArkUI中可通过使用AlertDialog、CustomDialog、ActionSheet、Popup、Menu、ContextMenu等组件实现模态类弹窗能力。

  • AlertDialog

通常用来展示用户当前需要或必须关注的信息或操作。如用户操作一个敏感行为时响应一个二次确认的弹窗。

  • ActionSheet

当需要用户关注或确认的信息存在列表选择时使用。

  • CustomDialog

当用户需要自定义弹窗内的组件和内容时使用。

  • Popup

用于为指定的组件做信息提示。如点击一个问号图标弹出一段气泡提示。

  • Menu/ContextMenu

用于给指定的组件绑定用户可执行的操作,如长按图标展示操作选项等。

  • 我们可以通过配置参数(例如:CustomDialog的isModal)调整弹窗为非模态弹窗,从而满足不同的使用场景。

  • 移动设备中,子窗模式的弹窗当前无法超出主窗口。

  • 多个弹窗组件先后弹出时,后弹出的组件的层级高于先弹出的层级,退出时按照层级从高到低的顺序逐次退出。

2、全局弹窗

全局弹窗不与任何组件绑定,一般用于针对用户触发的操作进行必要提示时使用。ArkUI当前提供了定制和自定义两类弹窗组件。

  • 定制:AlertDialog、ActionSheet、promptAction.showDialog、promptAction.showActionMenu。使用此类组件需要指定显示的文本内容和按钮操作即可完成简单的交互效果。

  • 自定义:CustomDialog、promptAction.openCustomDialog。需要根据场景传入自定义组件填充在弹窗中实现自定义的弹窗内容。

下面以AlertDialog、ActionSheet 和 CustomDialog为例说明相应的弹窗效果与使用方法。

2.1、AlertDialog

AlertDialog: 警告弹窗,需要向用户提问或得到用户的许可。

  • 警告弹窗用来提示重要信息,但会中断当前任务,尽量提供必要的信息和有用的操作。

  • 避免仅使用警告弹窗提供信息,用户不喜欢被信息丰富但不可操作的警告打断。

  • 必选内容包含:标题、可选信息文本、最多3个按钮。

  • 可选内容包含:输入框、icon、checkBox和HelpButton。

图片

@Entry@Componentstruct AlertDialogExample {  build() {    Column({ space: 5 }) {      Button('two button dialog')        .onClick(() => {          AlertDialog.show(            {              title: 'title',              subtitle: 'subtitle',              message: 'text',              autoCancel: true,              alignment: DialogAlignment.Bottom,              gridCount: 4,              offset: { dx: 0, dy: -20 },              primaryButton: {                value: 'cancel',                action: () => {                  console.info('Callback when the first button is clicked')                }              },              secondaryButton: {                enabled: true,                defaultFocus: true,                style: DialogButtonStyle.HIGHLIGHT,                value: 'ok',                action: () => {                  console.info('Callback when the second button is clicked')                }              }            }          )        }).backgroundColor(0x317aff)    }.width('100%').margin({ top: 5 })  }}

2.2、ActionSheet

ActionSheet: 列表选择弹窗。

  • 适合展示多个操作项,尤其是除了操作列表以外没有其他的展示内容。

图片

@Entry@Componentstruct ActionSheetExample {  build() {    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {      Button('Click to Show ActionSheet')        .onClick(() => {          ActionSheet.show({            title: 'ActionSheet title',            subtitle: 'ActionSheet subtitle',            message: 'message',            autoCancel: true,            confirm: {              defaultFocus: true,              value: 'Confirm button',              action: () => {                console.log('Get Alert Dialog handled')              }            },            alignment: DialogAlignment.Bottom,            offset: { dx: 0, dy: -10 },            sheets: [              {                title: 'apples',                action: () => {                  console.log('apples')                }              },              {                title: 'bananas',                action: () => {                  console.log('bananas')                }              },              {                title: 'pears',                action: () => {                  console.log('pears')                }              }            ]          })        })    }.width('100%')    .height('100%')  }}

2.3、CustomDialog

当我们需要自定义弹窗的内容和样式时,可选择CustomDialog。【更建议使用promptAction.openCustomDialog】

图片

// xxx.ets@CustomDialog@Componentstruct CustomDialogExample {  @Link textValue: string  @Link inputValue: string  controller?: CustomDialogController  cancel: () => void = () => {  }  confirm: () => void = () => {  }  build() {    Column() {      Text('Change text').fontSize(20).margin({ top: 10, bottom: 10 })      TextInput({ placeholder: '', text: this.textValue }).height(60).width('90%')        .onChange((value: string) => {          this.textValue = value        })      Text('Whether to change a text?').fontSize(16).margin({ bottom: 10 })      Flex({ justifyContent: FlexAlign.SpaceAround }) {        Button('cancel')          .onClick(() => {            if (this.controller != undefined) {              this.controller.close()              this.cancel()            }          }).backgroundColor(0xffffff).fontColor(Color.Black)        Button('confirm')          .onClick(() => {            if (this.controller != undefined) {              this.inputValue = this.textValue              this.controller.close()              this.confirm()            }          }).backgroundColor(0xffffff).fontColor(Color.Red)      }.margin({ bottom: 10 })    }.borderRadius(10)  }}@Entry@Componentstruct CustomDialogUser {  @State textValue: string = ''  @State inputValue: string = 'click me'  dialogController: CustomDialogController | null = new CustomDialogController({    builder: CustomDialogExample({      cancel: () => {        this.onCancel()      },      confirm: () => {        this.onAccept()      },      textValue: $textValue,      inputValue: $inputValue    }),    cancel: this.exitApp,    autoCancel: true,    onWillDismiss: (dismissDialogAction: DismissDialogAction) => {      console.info("reason=" + JSON.stringify(dismissDialogAction.reason))      console.log("dialog onWillDismiss")      if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {        dismissDialogAction.dismiss()      }      if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) {        dismissDialogAction.dismiss()      }    },    alignment: DialogAlignment.Bottom,    offset: { dx: 0, dy: -20 },    gridCount: 4,    customStyle: false,    cornerRadius: 10,  })  // 在自定义组件即将析构销毁时将dialogController置空  aboutToDisappear() {    this.dialogController = null // 将dialogController置空  }  onCancel() {    console.info('Callback when the first button is clicked')  }  onAccept() {    console.info('Callback when the second button is clicked')  }  exitApp() {    console.info('Click the callback in the blank area')  }  build() {    Column() {      Button(this.inputValue)        .onClick(() => {          if (this.dialogController != null) {            this.dialogController.open()          }        }).backgroundColor(0x317aff)    }.width('100%').margin({ top: 5 })  }}

3、交互辅助弹窗

3.1、气泡Popup

当点击目标组件或交互区时,弹出内容在其他内容之上,可使用Popup来指示当前功能如何操作。

图片

@Entry@Componentstruct PopupExample {  @State handlePopup: boolean = false  build() {    Flex({ direction: FlexDirection.Column }) {      // PopupOptions 类型设置弹框内容      Button('PopupOptions')        .onClick(() => {          this.handlePopup = !this.handlePopup        })        .bindPopup(this.handlePopup, {          message: 'This is a popup with PopupOptions',          placementOnTop: true,          showInSubWindow: false,          primaryButton: {            value: 'confirm',            action: () => {              this.handlePopup = !this.handlePopup              console.info('confirm Button click')            }          },          // 第二个按钮          secondaryButton: {            value: 'cancel',            action: () => {              this.handlePopup = !this.handlePopup              console.info('cancel Button click')            }          },          onStateChange: (e) => {            console.info(JSON.stringify(e.isVisible))            if (!e.isVisible) {              this.handlePopup = false            }          }        })        .position({ x: 100, y: 150 })    }.width('100%').padding({ top: 5 })  }}

3.2、菜单Menu

很多时候需要通过交互弹出一些菜单选项,用于用户操作。此时可通过Menu和MenuItem组件组合成需要弹出的菜单选项内容,然后借助bindMenu和bindContextMenu方法将菜单和组件绑定。他们的主要区别如下:

  • bindMenu:无需预览图场景,需要在非子窗场景显示。

  • bindContextMenu: 需要预览图场景使用,只能在子窗中显示。

👉🏻 bindMenu

一种临时性弹出组件,用于展示用户可执行的操作。

    ⭐️ 一级Menu样式如下:

图片

    ⭐️ 多级Menu样式如下:

图片

代码示例如下:

@Entry@Componentstruct Index {  @State select: boolean = true  @Builder  MyMenu() {    Menu() {      MenuItem({ content: "菜单选项" })      MenuItem({ content: "菜单选项" })      MenuItem({ content: "菜单选项" })      MenuItem({ content: "菜单选项" })    }  }  build() {    Row() {      Column() {        Text('click to show menu')          .fontSize(50)          .fontWeight(FontWeight.Bold)      }      .bindMenu(this.MyMenu)      .width('100%')    }    .height('100%')  }}

👉🏻 bindContextMenu

 内容包括菜单、预览图、蒙层(通常在长按桌面图标时使用)

    ⭐️ 相对父组件区域弹出样式如下:

图片

    ⭐️ 相对右键点击位置弹出样式如下:

图片

代码示例如下:

@Entry@Componentstruct Index {  @State select: boolean = true  @Builder  MyMenu(){    Menu() {      MenuItem({ content: "菜单选项" })      MenuItem({ content: "菜单选项" })      MenuItem({ content: "菜单选项" })      MenuItem({ content: "菜单选项" })    }  }  build() {    Row() {      Column() {        Text('click to show menu')          .fontSize(50)          .fontWeight(FontWeight.Bold)      }      .bindContextMenu(this.MyMenu, ResponseType.LongPress,{        placement: Placement.Left,        preview: MenuPreviewMode.IMAGE      })      .width('100%')    }    .height('100%')  }}

4、类PC场景

在2in1设备上,使用模态类弹窗时,会出现超出主窗口显示的场景,如下图所示。

图片

我们可通过支持子窗口和默认子窗口实现超出应用界面效果。

4.1、支持子窗口

自定义弹窗(CustomDialog)、警告弹窗(AlertDialog)、列表选择弹窗(ActionSheet)、气泡提示(Popup)可通过showInSubWindow设置弹窗或气泡在子窗口中,从而实现超出主窗口的显示效果。示例代码如下(37行):​​​​​​​

// xxx.ets    @CustomDialogstruct CustomDialogExample {  controller?: CustomDialogController  cancel: () => void = () => {  }  confirm: () => void = () => {  }  build() {    Column() {      Text('可展示在主窗口外的弹窗')        .fontSize(30)        .height(100)      Button('点我关闭弹窗')        .onClick(() => {          if (this.controller != undefined) {            this.controller.close()          }        })        .margin(20)    }  }}@Entry@Componentstruct CustomDialogUser {  dialogController: CustomDialogController | null = new CustomDialogController({    builder: CustomDialogExample({      cancel: ()=> { this.onCancel() },      confirm: ()=> { this.onAccept() }    }),    cancel: this.existApp,    autoCancel: true,    alignment: DialogAlignment.Center,    offset: { dx: 0, dy: -20 },    gridCount: 4,    showInSubWindow: true,    isModal: true,    customStyle: false,    cornerRadius: 10,  })  // 在自定义组件即将析构销毁时将dialogController置空  aboutToDisappear() {    this.dialogController = null // 将dialogController置空  }  onCancel() {    console.info('Callback when the first button is clicked')  }  onAccept() {    console.info('Callback when the second button is clicked')  }  existApp() {    console.info('Click the callback in the blank area')  }  build() {    Column() {      Button('click me')        .onClick(() => {          if (this.dialogController != null) {            this.dialogController.open()          }        }).backgroundColor(0x317aff)    }.width('100%').margin({ top: 5 })  }}

4.2、默认子窗口

可使用 bindContextMenu为组件绑定菜单,触发方式为长按或者右键点击,弹出菜单项需要自定义。​​​​​​​

@Entry@Componentstruct ContextMenuExample {  @Builder MenuBuilder() {    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {      Text('Test menu item 1')        .fontSize(20)        .width(100)        .height(50)        .textAlign(TextAlign.Center)      Divider().height(10)      Text('Test menu item 2')        .fontSize(20)        .width(100)        .height(50)        .textAlign(TextAlign.Center)    }.width(100)  }  build() {    Column() {      Text('LongPress for menu')    }    .width('100%')    .margin({ top: 5 })    .bindContextMenu(this.MenuBuilder, ResponseType.LongPress)  }}

;