【HarmonyOS实战开发】鸿蒙自定义 Dialog 的 6 种方式

59 篇文章 0 订阅
53 篇文章 0 订阅

前言

之前已经有多篇文章介绍过我封装的开源组件库 XTHUD,该组件库的 V3 版本中,针对在特殊场景中调用显示 HUD 的诸多问题,做了一次较大的更新,即之前的 HUD 是通过构造 CustomDialog 的方式实现的,但是在 V3 版本中新增的 XTPromptHUD 工具类,其 HUD 是通过 ComponentContent 实现的,ComponentContent 是 API12 新增的特性,可以支持在非 UI 组件环境中构造 UI,十分适合弹窗类的 UI 解耦开发。

在完善 HUD 这个组件库的开发过程中,针对鸿蒙开发中自定义 Dialog 的多种方式有了一些接触和尝试,下面的内容就是相关知识点的简单总结。

1. 自定义弹窗 (CustomDialog)

1.1. 简单的使用示例

自定义弹窗的最常规方式就是通过 CustomDialogController 类去控制显示自定义的弹窗或者浮层。
针对该方式的用法,可以参考官方的 API 文档:

●HarmonyOS Next API12 - 自定义弹窗 (CustomDialog)
●OpenHarmony API11 - 自定义弹窗 (CustomDialog)
●OpenHarmony API11 - 自定义弹窗 (CustomDialog) 开发范式

在我的 XTHUD 组件库中,核心实现如下,为便于展示自定义 Dialog 的使用方式,源码做了删减:

// @CustomDialog装饰器用于装饰自定义弹框,此装饰器内进行自定义内容(也就是弹框内容)
@CustomDialog
struct XTToastCustomDialogView {
  // 这里可以不用初始化,但是必须声明
  // @CustomDialog component should have a property of the CustomDialogController type.
  controller: CustomDialogController
  // 显示文本,可动态响应更新
  @Prop text: string = ''
  // 动态响应配置
  @Prop options: XTHUDToastOptions = defaultToastOptions

  build() {
    Column () {
      if (this.options?.iconSrc) {
        Image(this.options?.iconSrc)
          .objectFit(ImageFit.Contain)
          .size(this.options?.iconSize ?? defaultToastOptions.iconSize)
          .fillColor(this.options?.tintColor ?? defaultToastOptions.tintColor)
          .margin(this.options?.iconMargin ?? defaultToastOptions.iconMargin)
      }
      Text(this.text)
        .fontSize(this.options?.fontSize ?? defaultToastOptions.fontSize)
        .fontColor(this.options?.textColor ?? defaultToastOptions.textColor)
        .padding({
          top: this.options?.iconSrc ? 0 : this.options?.textPadding?.top ?? defaultToastOptions.textPadding.top,
          left: this.options?.textPadding?.left ?? defaultToastOptions.textPadding.left,
          right: this.options?.textPadding?.right ?? defaultToastOptions.textPadding.right,
          bottom: this.options?.textPadding?.bottom ?? defaultToastOptions.textPadding.bottom
        })
        .textAlign(TextAlign.Center)
    }
    .backgroundColor(this.options?.backgroundColor ?? defaultToastOptions.backgroundColor)
    .borderRadius(this.options?.borderRadius ?? defaultToastOptions.borderRadius)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .constraintSize({
      minWidth: this.options?.minWidth ?? defaultToastOptions.minWidth,
      maxWidth: this.options?.maxWidth ?? defaultToastOptions.maxWidth
    })
  }
}

@Component
export struct XTHUDToast {
  /// 弹窗控制器
  private _dialogController: CustomDialogController | null = null
  /// toast视图构造器
  @Builder private toastCustomDialogView() {
    XTToastCustomDialogView({
      text: this._currentText,
      options: this._currentOptions,
    })
  }

  showToastDialog(): void {
    // 打开
    this._dialogController?.open()
  }
  hide(): void {
    this._dialogController?.close()
  }

  // 组件挂载
  aboutToAppear() {
    // 后初始化,避免options更新无效
    this._dialogController = new CustomDialogController({
      builder: () => {
        this.toastCustomDialogView()
      },
      alignment: this._currentOptions?.alignment ?? defaultToastOptions.alignment,
      // 是否可以点击背景关闭
      autoCancel: this._currentOptions?.closeOnClickOutside ?? defaultToastOptions.closeOnClickOutside,
      // 偏移量
      offset: this._currentOptions?.offset ?? defaultToastOptions.offset,
      // 自定义样式,默认就是白色圆角弹窗背景
      customStyle: true,
      maskColor: this._currentOptions?.maskColor ?? defaultToastOptions.maskColor,
      openAnimation: this._currentOptions?.openAnimation ?? defaultToastOptions.openAnimation,
      closeAnimation: this._currentOptions?.closeAnimation ?? defaultToastOptions.closeAnimation,
      // 默认true,是否全屏展示,false只有弹窗区域UI
      isModal: this._currentOptions?.isModal ?? defaultToastOptions.isModal,
    })
  }

  build() {
  }
}

1.2. 自定义 Dialog 不显示的问题探究

在 鸿蒙ArkUI自定义组件的构造原理探索 一文中,我使用了一种非常规的组件挂载方式,即只接 new XTHUDToast()去操作 HUD 的展示,通过 XTEasyHUD 管理类去控制,优势就是灵活,可以在任意地方无需初始化一句代码执行 HUD 的显示和隐藏操作。
但是问题也是有很多的,在我的 Demo 仓库 issues 中可以看到对应的的使用反馈:
在这里插入图片描述

核心问题就是,在非 UI 组件环境中使用,HUD 可能会无法显示,本质原因应该是上下文环境丢失或者上下文获取异常。
其实这个问题在官方文档中也有说明:

在Stage模型中,WindowStage/Window可以通过loadContent接口加载页面并创建UI的实例,并将页面内容渲染到关联的窗口中,所以UI实例和窗口是一一关联的。一些全局的UI接口是和具体UI实例的执行上下文相关的,在当前接口调用时,通过追溯调用链跟踪到UI的上下文,来确定具体的UI实例。若在非UI页面中或者一些异步回调中调用这类接口,可能无法跟踪到当前UI的上下文,导致接口执行失败。

再次翻阅 arkui/ace_engine 源码,可以看到 CustomDialog 的承载控制器 CustomDialogController 的源码实现部分:
在这里插入图片描述

其基类 NativeCustomDialogController 是 C++ 实现的:
在这里插入图片描述

在 Dialog 的打开操作中,可以看到上下文溯源相关的操作 auto pipelineContext = PipelineContext::GetCurrentContext():
在这里插入图片描述

具体更深入的源码部分我就没再深读了,有兴趣的可以研究下:
在这里插入图片描述

2. promptAction.openCustomDialog

系统的 promptAction 似乎并不受上下文环境影响,实测可在 EntryAbility 中直接执行显示:

promptAction.showToast({
  message: 'Hello World'
})

promptAction 核心源码也是 C++ 实现的:
在这里插入图片描述

2.1. openCustomDialog 简单示例

API11 之后,新增了 promptAction.openCustomDialog 方法,可以利用 promptAction 调用显示自定义的 Dialog:

@Entry
@Component
struct Index {
  private customDialogComponentId: number = 0

  @Builder customDialogComponent() {
    Column() {
      Text('弹窗').fontSize(30)
      Row({ space: 50 }) {
        Button("确认").onClick(() => {
          promptAction.closeCustomDialog(this.customDialogComponentId)
        })
        Button("取消").onClick(() => {
          promptAction.closeCustomDialog(this.customDialogComponentId)
        })
      }
    }.height(200).padding(5).justifyContent(FlexAlign.SpaceBetween)
  }

  build() {
    Row() {
      Column({ space: 20 }) {
        Text('组件内弹窗')
          .fontSize(30)
          .onClick(() => {
            promptAction.openCustomDialog({
              builder: () => {
                this.customDialogComponent()
              },
              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()
                }
              }
            }).then((dialogId: number) => {
              this.customDialogComponentId = dialogId
            })
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

2.2. 脱离 UI 组件环境的问题

如果我们想进一步脱离 UI 组件环境,可以将 builder 替换为全局 @Builder 函数,但是很可惜,直接这么使用,会报错崩溃:
在这里插入图片描述

查阅 API 文档,可以看到这句说明:
在这里插入图片描述

如果是全局builder需要在组件内部创建一个builder,在内部builder中调用全局builder。

其实官方给出的示例也是这么写的:

@Builder
customDialogComponent() {
  customDialogBuilder()
}

也就是说,这种方式,我们同样无法完全脱离 UI 组件构建环境,本质上和 CustomDialog 一样。
其完整实现为:

import promptAction from '@ohos.promptAction'

let customDialogId: number = 0

@Builder
  function customDialogBuilder() {
    Column() {
      Text('Custom dialog Message').fontSize(10)
      Row() {
        Button("确认").onClick(() => {
          promptAction.closeCustomDialog(customDialogId)
        })
        Blank().width(50)
        Button("取消").onClick(() => {
          promptAction.closeCustomDialog(customDialogId)
        })
      }
    }
  }

@Entry
  @Component
  struct Index {
    @State message: string = 'Hello World'

    @Builder
    customDialogComponent() {
      customDialogBuilder()
    }

    build() {
      Row() {
        Column() {
          Text(this.message)
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
            .onClick(() => {
              promptAction.openCustomDialog({
                builder: () => {
                  this.customDialogComponent()
                },
                showInSubWindow: false,
                offset: { dx: 5, dy: 5 },
                backgroundColor: 0xd9ffffff,
                cornerRadius: 20,
                width: '80%',
                height: 200,
                borderWidth: 1,
                borderStyle: BorderStyle.Dashed, //使用borderStyle属性,需要和borderWidth属性一起使用
                borderColor: Color.Blue, //使用borderColor属性,需要和borderWidth属性一起使用
                shadow: ({
                  radius: 20,
                  color: Color.Grey,
                  offsetX: 50,
                  offsetY: 0
                }),
              }).then((dialogId: number) => {
                customDialogId = dialogId
              })
            })
        }
        .width('100%')
      }
      .height('100%')
    }
  }

3. UIContext.getPromptAction().openCustomDialog 和 ComponentContent

在 UIContext 的文档中,可以看到 API12 新增了 openCustomDialog 这个方法,其入参之一是一个 ComponentContent 对象:

ComponentContent表示组件内容的实体封装,ComponentContent对象支持在非UI组件中创建与传递,便于开发者对弹窗类组件进行解耦封装。

看描述,这十分满足我们的需求,实际上,API12 中新增了很多自定义组件相关的底层能力,可以使得自定义组件的自由度大幅提升:
在这里插入图片描述

3.1. 简单示例

通过 wrapBuilder 函数,可以封装一个全局 @Builder 函数:

import { BusinessError } from '@ohos.base';
import { ComponentContent } from "@ohos.arkui.node";

class Params {
  text: string = ""
  constructor(text: string) {
    this.text = text;
  }
}

@Builder
function buildText(params: Params) {
  Column() {
    Text(params.text)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .margin({bottom: 36})
  }.backgroundColor('#FFF0F0F0')
}

@Entry
@Component
struct Index {
  @State message: string = "hello"

  build() {
    Row() {
      Column() {
        Button("click me")
            .onClick(() => {
                let uiContext = this.getUIContext();
                let promptAction = uiContext.getPromptAction();
                let contentNode = new ComponentContent(uiContext, wrapBuilder(buildText), new Params(this.message));
                try {
                  promptAction.openCustomDialog(contentNode);
                } catch (error) {
                  let message = (error as BusinessError).message;
                  let code = (error as BusinessError).code;
                  console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`);
                };
            })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
}

3.2. UIContext 的获取方式

ComponentContent 实例化核心参数之一就是 UIContext。
注意使用不同的方式获取的 UIContext 可能会导致 Dialog 显示在不同的层级,如果获取到错误的上下文信息,可能会导致 Dialog 无法显示。

1.在组件内部,可以直接通过 this.getUIContext()获取 UI 上下文:

this.getUIContext()

2.在 EntryAbility 中,可以在 onWindowStageCreate中执行 loadContent后,通过获取主 window 的方式,利用 window 去获取 UIContext:

onWindowStageCreate(windowStage: window.WindowStage): void {
  windowStage.loadContent('pages/Index', (err) => {
    let uiContext = windowStage.getMainWindowSync().getUIContext()
  });
}

3.在未知环境中,还可以 getContext函数去间接获取 UIContext,getContext() 入参是组件对象,但是可选,如果不传,就会默认根据当前环境去自动获取,但是这种方式并不安全,实测在某些环境下,getContext()返回值为undefined,会导致后续获取执行失败:

let windowClass = await window.getLastWindow(getContext())
let uiContext = windowClass.getUIContext()

3.3. 更新显示的问题

可以直接通过 updateCustomDialog 函数去更新 Dialog 的显示,但是这种操作其实是整个 Dialog 维度的,可能会重绘自定义 Dialog 的整体组件元素,如果是 ProgressHUD 这种每次更新的只是一个数值进度,如果都会重绘 HUD 整体,可能会有性能问题。
其实,还有一个更新方式,ComponentContent 有一个 update 方法,它更新的只是 ComponentContent 构造时的 WrappedBuilder 的参数部分。
在我的 XTHUD 组件库中,核心实现如下,为便于展示,源码做了删减修改:

// 显示 dialog
function _showDialogNode<T extends Object>(
  nodeBuilder: WrappedBuilder<[T]>, 
  nodeArgs: T, 
  defaultOptions: XTHUDCustomDefaultOptions, 
  currentOptions?: XTHUDReactiveBaseOptions
): void {
  // hud计数器
  this._HUDCount ++

  // 避免重复创建
  if (this._HUDCount > 1) {
    // 更新 node 参数
    this._dialogNode?.update(nodeArgs)
    return
  }
  // 单次显示时,执行上下文相关配置信息,不重复更新,避免出问题
  this._promptAction = this._uiContext.getPromptAction()
  this._dialogNode = new ComponentContent(
    this._uiContext,
    nodeBuilder,
    nodeArgs
  )
  // node构造参数
  const dialogOptions: promptAction.BaseDialogOptions = {
    alignment: currentOptions?.alignment ?? defaultOptions.alignment,
    // 偏移量
    offset: currentOptions?.offset ?? defaultOptions.offset,
    // 默认true,是否全屏展示,false只有弹窗区域UI
    isModal: currentOptions?.isModal ?? defaultOptions.isModal,
    // 是否可以点击背景关闭
    autoCancel: currentOptions?.closeOnClickOutside ?? defaultOptions.closeOnClickOutside,
    maskColor: currentOptions?.maskColor ?? defaultOptions.maskColor
  }
  // 打开
  this._promptAction?.openCustomDialog(this._dialogNode, dialogOptions)
}
function _updateDialogNode<T extends Object>(
  nodeArgs: T
): void {
  // 更新 node 参数
  this._dialogNode?.update(nodeArgs)
}

function _hide(
  onCompletion?: XTHUDCallback
): void {
  this._promptAction?.closeCustomDialog(this._dialogNode)
}

4. @ohos.arkui.advanced.Dialog (弹出框)

在 ArkUI 的高级组件中,也有 Dialog 相关的部分:@ohos.arkui.advanced.Dialog (弹出框)
但其本质还是 CustomDialogController,只是提供了一些 @CustomDialog 的组件封装实现。

4.1. LoadingDialog

LoadingDialog 是官方实现的 LoadingHUD,其样式相对固定,符合官方 UX 范式。
在这里插入图片描述

import { LoadingDialog } from '@ohos.arkui.advanced.Dialog'

@Entry
@Component
struct Index {
  dialogControllerProgress: CustomDialogController = new CustomDialogController({
    builder: LoadingDialog({
      content: '文本文本文本文本文本...',
    }),
  })

  build() {
    Row() {
      Stack() {
        Column() {
          Button("进度条弹出框")
            .width(96)
            .height(40)
            .onClick(() => {
              this.dialogControllerProgress.open()
            })
        }.margin({ bottom: 300 })
      }.align(Alignment.Bottom)
        .width('100%').height('100%')
    }
    .backgroundImageSize({ width: '100%', height: '100%' })
      .height('100%')
  }
}

4.2. CustomContentDialog

CustomContentDialog 是对应的自定义 Dialog,其 API 设计更符合弹窗范式。
在这里插入图片描述

import { CustomContentDialog } from '@ohos.arkui.advanced.Dialog'

@Entry
@Component
struct Index {
  dialogController: CustomDialogController = new CustomDialogController({
    builder: CustomContentDialog({
      primaryTitle: '标题',
      secondaryTitle: '辅助文本',
      contentBuilder: () => {
        this.buildContent();
      },
      buttons: [{ value: '按钮1', buttonStyle: ButtonStyleMode.TEXTUAL, action: () => {
        console.info('Callback when the button is clicked')
      } }, { value: '按钮2', buttonStyle: ButtonStyleMode.TEXTUAL, role: ButtonRole.ERROR }],
    }),
  });

  build() {
    Column() {
      Button("支持自定义内容弹出框")
        .onClick(() => {
          this.dialogController.open()
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  buildContent(): void {
    Column() {
      Text('内容区')
    }
  }
}

5. 通过自定义 window 构建 Dialog

系统的 promptAction ��底层实现,就可以看到 Subwindow 类似的字样,大体可以猜测其实现机制之一就是自定义 window:
在这里插入图片描述

在 iOS 开发中,部分自定义弹窗或浮层,就是通过自定义 window 实现的,这种自定义的形式显示层级更高,但是带来的问题也更多,很多时候会导致横竖屏显示方向错误,或者和其他系统级的 window 显示冲突,所以使用时必须特别谨慎。
鸿蒙系统中,自定义 window 有两种方式,一个是 createWindow ,一个是 createSubWindow,

5.1. createWindow

可以设置一个 Dailog 类型的 window,如果设置 parentId,那就等效于 createSubWindow。
注意 window 的尺寸是 px 单位值,需要使用vp2px()做转换。

let config: window.Configuration = {
  name: "test",
  windowType: window.WindowType.TYPE_DIALOG,
  // parentId: windowStage.getMainWindowSync().getWindowProperties().id,
  ctx: this.context
};
window.createWindow(config, (err, data) => {
  let windowClass = data;
  // window 尺寸
  windowClass.resize(vp2px(300), vp2px(300))
  // window 内容
  windowClass.setUIContent('pages/Index')
  // windowClass.loadContent('pages/Index', null).then(() => {
  //   console.log('Succeeded in loading the')
  // })
  // 显示 window
  windowClass.showWindow(() => {
    console.info('Succeeded show')
  })
});

/// 3 秒后销毁 window
setTimeout(() => {
  window.findWindow('test').destroyWindow()
}, 3000)

5.2. createSubWindow

如果是基于 window 自定义 Dialog,相对更合适的方式是创建子 window:

windowStage.createSubWindow('test', (err, data) => {
  // window 尺寸
  data.resize(vp2px(300), vp2px(300)).then(() => {
  })
  // window 位置
  data.moveWindowTo(0, 0).then(() => {
  })
  // window 内容
  data.setUIContent('pages/Index', () => {
  })
  // 显示 window
  data.showWindow()
})

/// 3 秒后销毁 window
setTimeout(() => {
  // window.getLastWindow(this.context, (err, windowClass) => {
  //   windowClass.destroyWindow()
  // })
  window.findWindow('test').destroyWindow()
}, 3000)

5.3. 实际使用

中心仓中有很多 Dialog 组件库都是基于自定义 window 的方式实现的,例如 @abner/dialog:
在这里插入图片描述

但就像前面说到的,利用自定义 window 去做 Dialog,其自由度很高,可以做到很多,但是使用时还是要注意处理,避免未知的冲突问题,相对来说,个人还是更倾向于使用 ComponentContent 或者 FrameNode 的方式去构建自定义 Dialog,毕竟 API 相对更加简单,也更加可控。

6. Native 方式

这是一套 C API,不仅限于自定义 Dialog,而是整个 ArkUI 框架都提供了对应的 C API,可以针对某些特殊的开发场景,具体文档如下:

●ArkUI_NativeModule
●native_dialog.h

写在最后

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。

这份鸿蒙(HarmonyOS NEXT)文档包含了鸿蒙开发必掌握的核心知识要点,内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony南向开发、鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。

希望这一份鸿蒙学习文档能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!

获取这份完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档

鸿蒙(HarmonyOS NEXT)最新5.0学习路线

在这里插入图片描述

有了路线图,怎么能没有学习文档呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。

获取以上完整版高清学习路线,请点击→纯血版5.0全套鸿蒙HarmonyOS学习文档

《鸿蒙 (OpenHarmony)开发入门教学视频》

在这里插入图片描述

《鸿蒙生态应用开发V3.0白皮书》

在这里插入图片描述

《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

在这里插入图片描述

《鸿蒙开发基础》

●ArkTS语言
●安装DevEco Studio
●运用你的第一个ArkTS应用
●ArkUI声明式UI开发
.……
在这里插入图片描述

《鸿蒙开发进阶》

●Stage模型入门
●网络管理
●数据管理
●电话服务
●分布式应用开发
●通知与窗口管理
●多媒体技术
●安全技能
●任务管理
●WebGL
●国际化开发
●应用测试
●DFX面向未来设计
●鸿蒙系统移植和裁剪定制
……
在这里插入图片描述

《鸿蒙进阶实战》

●ArkTS实践
●UIAbility应用
●网络案例
……
在这里插入图片描述

获取以上完整鸿蒙HarmonyOS学习文档,请点击→纯血版全套鸿蒙HarmonyOS学习文档

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值