【HarmonyOS】超好用的弹窗实现方案

1,088 阅读3分钟

API12之前的弹窗实现方案

ArkUI提供了@CustomDialog用于自定义弹窗,虽然可以完成业务要求的样式和功能,但是在实际项目开发中存在两个问题:

  1. 每次使用都需创建CustomDialogController实例并配置各项参数
  2. CustomDialogController只能在@CustomDialog@Component struct中使用

在组件中每次使用都要创建控制器实例,增加多余代码。在方法或类中不能使用弹窗,在使用弹窗时视图和逻辑耦合过高。

解决方案

1、通过创建window模拟弹窗。

如果点击弹窗按钮进行路由跳转,新页面加载在弹窗window上。

2、使用Navigation.Dialog模拟弹窗。

项目路由跳转都是使用router控制,难以整体更换Navigation组件。

3、使用空组件配置CustomDialogController和调用打开关闭弹窗方法,在类中在调用空组件的方法


@CustomDialog
struct DialogTest {
  controller?: CustomDialogController
  build() {
    Text('弹窗展示内容')
  }
}

// 这个空组件要在使用弹窗之前使用一下,保证temp.applyDialog(this)执行
@Component
export struct DialogComponent {
  private controller = new CustomDialogController({
    builder: DialogTest()
  })

  open() {
    this.controller.open()
  }

  close() {
    this.controller.close()
  }

  aboutToAppear(): void {
    temp.applyDialog(this)
  }

  build() {
  }
}

class DialogManage {

  private d: DialogComponent | null = null

  applyDialog(com: DialogComponent) {
    this.d = com
  }

  open() {
    this.d?.open()
  }

  close() {
    this.d?.close()
  }
}

const temp = new DialogManage()

export default temp

使用API12的openCustomDialog和ComponentContent实现

openCustomDialog

openCustomDialog时UIcontext上API12新增的方法,传入ComponentContent类型和弹窗相关配置即可使用弹窗。

UIcontext在struct中使用this.getUIContext()获取。在非struct中使用时,需要在onWindowStageCreate 钩子函数中通过AppStorage.setOrCreate('windowStage', windowStage)保存供全局使用,使用windowStage.getMainWindowSync().getUIContext()在任何位置都可获取UIcontext

ComponentContent

API12提供的模块,主要就是解决弹窗耦合问题的,底层使用的BuilderNode,类似前端的虚拟节点,在js中创建合适的时机插入页面中。

通过BuilderNode提前创建加载的特性,可以做到耗时组件秒开的效果,例如web组件加载h5页面

简单使用


Button('弹窗').onClick(() => {
  const context = this.getUIContext()
  const node = new ComponentContent(context,wrapBuilder(testDialog), '测试弹窗')
  context.getPromptAction().openCustomDialog(node)
})

// 弹窗主体
@Builder
function testDialog(params: string) {
  Column() {
    Text(params)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .margin({bottom: 36})
  }
  .width('80%')
  .height('40%')
  .backgroundColor('#FFF0F0F0')
}

在项目中使用openCustomDialog创建弹窗其配置项基本相同,只有展示主体不同例如温馨提示弹窗、活动弹窗、引导弹窗等等。定义抽象类、基本弹窗配置并完成公共的相关配置和方法,项目中定义弹窗时继承此抽象类实现抽象方法即可快速创建弹窗。

基础配置类BaseDialogParam.ets


export class BaseDialogParam {
  // 弹窗关闭回调
  dismissAction: Function = () => {
  }
  // 弹窗将要显示的回调
  showAction: Function = () => {

  }
  // 点击遮罩是否关闭弹窗 false: 不关闭
  autoCancel: boolean = false
  // 弹窗主体位置
  alignment: DialogAlignment = DialogAlignment.Center
  // 关闭打开时的动画效果
  transition?: TransitionEffect
  // 可以控制是否可以通过设备上的返回键、左、右滑、键盘的ESC关闭弹窗
  onWillDismiss: (action: DismissDialogAction) => void = () => {

  }
  // 弹窗关闭时执行的方法
  onDidDisappear?: () => void
}

抽象类BaseDialog.ets

// 继承此类的类属性是按照业务需求定制的弹窗,除基本参数外还有一些其它功能需要的参数
export abstract class BaseDialog<T extends BaseDialogParam> {
  private promptAction?: PromptAction
  private componentContent?: ComponentContent<T>
  private context: UIContext
  
  // 获取UIcontext
  constructor(context?: UIContext) {
    const windowStage = AppStorage.get<window.WindowStage>('windowStage')!
    this.context = context ?? windowStage.getMainWindowSync().getUIContext()
  }

  show(param: T): Promise<void> {
    this.promptAction = this.context.getPromptAction()
    // 打开之前先关闭弹窗
    param.dismissAction = () => {
      this.dismiss()
    }
    this.componentContent = new ComponentContent(this.context, this.dialogBuilder(), param)
    // 弹窗配置
    return this.promptAction.openCustomDialog(this.componentContent, {
      alignment: param.alignment,
      autoCancel: param.autoCancel,
      transition: param.transition,
      onDidDisappear: param.onDidDisappear,
      onWillDismiss: param.onWillDismiss,
      onWillAppear: () => {
        param.showAction()
      }
    }).catch(() => {
      this.release()
    })
  }
  // 更新传参
  update(param: T) {
    if (this.componentContent) {
      this.componentContent.update(param)
    }
  }
    
  // 弹窗主体
  abstract dialogBuilder(): WrappedBuilder<T[]>

  isShowing(): boolean {
    if (this.componentContent) {
      return true
    }
    return false
  }
  // 关闭弹窗
  dismiss(): Promise<void> {
    return new Promise((resolve) => {
      if (this.componentContent) {
        this.promptAction?.closeCustomDialog(this.componentContent).then(() => {
          this.release()
          resolve()
        }).catch(() => {
          this.release()
          resolve()
        })
      } else {
        resolve()
      }
    })
  }
  
  private release() {
    this.componentContent?.dispose()
    this.componentContent = undefined;
    this.promptAction = undefined;
  }
}

实现一个温馨提示弹窗TipsDialog

// 自定义弹窗中需要的参数和回调
class TipsDialogParam extends BaseDialogParam {
  msg: string = ''
  onsubmit: () => void = () => {
  }
}

// 弹窗主体实现
@Builder
function TipsBuilder(param: TipsDialogParam) {
  Column() {
    Text('温馨提示').fontSize(26).fontWeight(700)
    Text(param.msg).alignSelf(ItemAlign.Start)
    Row({space: 15}) {
      Button('关闭')
        .layoutWeight(1)
        .onClick(() => {
        param.dismissAction()
      })
      Button('确认')
        .layoutWeight(1)
        .onClick(() => {
        param.onsubmit()
      })
    }.width('100%')
  }
  .width('80%')
  .height('30%')
  .backgroundColor(Color.White)
  .borderRadius(20)
  .padding(25)
  .justifyContent(FlexAlign.SpaceBetween)

}

export class TipsDialog extends BaseDialog<TipsDialogParam> {
  // 打开弹窗
  open(msg: string) {
    const param = new TipsDialogParam()
    param.msg = msg
    param.onsubmit = () => {
      promptAction.showToast({message: '点击了确认按钮'})
    }
    this.show(param)
  }
  // 实现BaseDialog中的抽象方法
  dialogBuilder(): WrappedBuilder<TipsDialogParam[]> {
    return wrapBuilder(TipsBuilder)
  }
}

使用TipsDialog

Button('弹窗').onClick(() => {
  const tipsDialog = new TipsDialog()
  tipsDialog.open('提示内容')
})