写点什么

【HarmonyOS】一步解决弹框集成 - 快速弹框 QuickDialog 使用详解

作者:GeorgeGcs
  • 2025-09-01
    四川
  • 本文字数:8347 字

    阅读完需:约 27 分钟

【HarmonyOS】一步解决弹框集成-快速弹框QuickDialog使用详解

【HarmonyOS】一步解决弹框集成-快速弹框 QuickDialog 使用详解

一、集成的应用背景介绍

最近比较忙,除了工作节奏调整,有重点项目需要跟。业务时间,也因为参加了 25 年创新大赛,我们网友,组成了鸿蒙超新星研发团队,经过两个月的人员加入和磨合,现已分为三个元服务小组,两个应用小组,正式参加了比赛。


团队多来自全国各地的校园开发者,例如上海交大的博士同学。当然为保证项目贴近行业技术前沿,也邀请了来自大厂的开发者加入,帮忙进行项目框架的搭建和前沿鸿蒙技术的调研。


1、小组介绍:其中 BONNET 小组负责开发的应用,《鸿社圈子》。作为主攻校园平台的鸿蒙学习资源与社群应用。



应用采用分层架构设计,主要分为表现层、业务逻辑层和数据层。采用模块化设计,将博客、圈子和公共功能拆分为独立模块,通过接口实现模块间通信:



应用拥有基础的博客功能,作为承接学习资源主要形式,文章的平台呈现。


应用中较为亮点的是,拥有类似朋友圈一样发言效果的圈子广场功能,不同于朋友圈,所有用户都可在广场中发言。


为了控制发言频率,在接入 AI 安全审核的基础上,我们通过将发言与成长体系的金币进行绑定,N 个金币发言一次,而金币又通过签到、发文章、评论等社区行为获得 X。


2、项目地址:旨在让更多的学生开发者参与到鸿蒙开发,我们决定将项目开源,作为开源鸿蒙项目让大家了解项目细节,互相学习和进步,我们的应用地址如下:



【代码仓库】https://gitcode.com/invite/link/06f16bb6a56d4c4484d5
复制代码


3、团队开发代码规范参考:因为是多人项目开发,所以针对鸿蒙团队开发的代码规范,我们沟通总结后,梳理了内部的代码规范文档如下:



https://developer.huawei.com/consumer/cn/blog/topic/03180782323061035
复制代码


该规范文章,主要梳理和规避了常见的一些前端转鸿蒙的书写习惯问题,和应用开发书写代码的行业共识等。

二、为何要用到 QuickDialog?

介绍完应用背景,接下来就是本篇文章的主角:QuickDialog。


在了解 QuickDialog 之前,我们需要了解目前鸿蒙里的弹框方案有哪些?


答案是:OpenCustomDialog、CustomDialog 与 DialogHub 三种。


1、官方迭代过程为:



CustomDialog => OpenCustomDialog => DialogHub
复制代码


CustomDialog 作为基础版本,依赖 UI 层的 CustomDialogController 实现弹框控制,存在强耦合局限。


OpenCustomDialog 通过 ComponentContent 节点将弹框实例托管于上下文,突破 UI 层依赖,支持纯逻辑调用。


DialogHub 基于 ArkUI 浮层机制,通过 OverlayManager 实现弹框节点的动态增删,彻底实现 UI 与逻辑解耦,支持生命周期全管控。



迭代过程表明,弹框的调用越来越便捷,与 UI 解耦,最终达到在纯逻辑中使用自定义弹出,弹框内容更新和生命周期可控,写法简洁。


弹框最重要的场景,自定义 View 与 UI 解耦的解决方案,目前共有三种方式,使用浮层(DialogHub 底层原理),使用 OpenCustomDialog,使用 subWindow。模板代码太多,使用起来也不方便。


但是这些弹框方案局限性在于,仅支持单次弹出与关闭,无法暂存弹窗堆栈状态,难以管理弹窗模态与层级互斥关系,限制了自定义自由度。


此时 QuickDialog 就应运而生了。

三、QuickDialog 详解


// 三方库中心地址:https://ohpm.openharmony.cn/#/cn/detail/quickdialog
// 三方库项目源码地址:https://atomgit.com/qccmobileteam/QuickDialog
复制代码


通过下载阅读 QuickDialog 的源码,我们梳理了其核心模块设计:1、QuickDialogManager 全局管理器作为全局管理器,静态属性与方法实现全局弹窗状态管理的工具类。其核心职责是按页面维度管理弹窗控制器(QuickDialogController),实现弹窗与页面的绑定、缓存、销毁及系统事件适配,解决传统弹窗跨页面状态混乱的问题。



通过 currentNaviDestinationId 和 dialogControllerMap 按页面 ID 隔离弹窗,避免不同页面弹窗状态混乱,解决传统弹窗跨页面生命周期失控问题。


采用静态类而非单例,通过静态属性维护全局状态,简化调用链路(无需实例化即可使用),降低接入成本。


弹窗控制器缓存与页面导航绑定逻辑通过静态方法暴露,无需修改业务页面结构,仅需在路由拦截中注入适配代码,符合 “非侵入式” 设计理念。


基于 HashSet 存储弹窗控制器,dismissLastShowingDialog() 可按显示状态优先关闭最新弹窗,间接实现堆栈式层级管理。


2、QuickDialogBuilder 内容与装饰器在 QuickDialogManager 中,装饰器与内容的关联通过 decoratorCreateContentNodeController 方法实现,这是装饰器嵌入内容的 “桥梁”。


弹窗构建器(QuickDialogBuilder)在配置时,会将内容参数(with 方法配置)和内容 Builder 封装到 QuickDialogDecoratorParams 中,传递给装饰器。


QuickDialogManager.decoratorCreateContentNodeController(decoratorParams) 获取内容节点控制器,再通过 NodeContainer 组件将内容嵌入装饰器的样式容器中。


3、技术特点总结:基于 Overlay 技术栈实现弹窗悬浮显示,脱离页面生命周期限制。通过 Node 机制动态创建弹窗节点,避免侵入业务页面结构。路由适配层针对原生 Navigation 和 HMRouter 方案分别提供拦截器与生命周期监听器,确保弹窗状态与页面导航同步。



4、接入方式:通过 ohpm 安装组件:ohpm install quickdialog:


{  "name": "entry",  "version": "1.0.0",  "description": "Please describe the basic information.",  "main": "",  "author": "",  "license": "",  "dependencies": {    "quickdialog": "^1.0.0"  },}
复制代码


在 AppAbilityStage 中配置深色模式适配,重写 onConfigurationUpdate 方法触发弹窗样式刷新。



import { AbilityStage, Configuration } from '@kit.AbilityKit';import { QuickDialogManager } from 'quickdialog';
export class AppAbilityStage extends AbilityStage {
onConfigurationUpdate(newConfig: Configuration): void { QuickDialogManager.onConfigurationUpdate(newConfig) }}
复制代码


5、使用步骤:基于 Builder 模式构建弹窗,通过 with() 传递内容参数,decorateWith() 配置装饰器样式,onEvent() 绑定交互事件,最终调用 show() 显示弹窗。示例代码如下:



// 创建控制器const dialogController = QuickDialogManager.newBuilder( this.getUIContext(), wrapBuilder(SampleDialogBuilder), // 内容Builder wrapBuilder(SampleDecoratorBuilder) // 装饰器Builder(可选)) .with('title', '示例弹窗') // 内容参数 .onEvent<string>('confirm', (ctrl, param) => { ctrl.dismiss(); // 事件回调处理 }) .decorateWith('bgColor', '#fff') // 装饰器参数 .create();dialogController.show(); // 显示弹窗
复制代码


弹窗内容通过 @ComponentV2 声明组件,装饰器需包含 NodeContainer 嵌入内容节点。在 QuickDialog 框架中,弹窗控件 wrapperBuilder 与弹窗装饰器 wrapperBuilder 是实现 “内容与样式分离” 核心设计的两个关键概念,分别对应弹窗的 “业务内容层” 和 “样式装饰层”。


前者用于构建弹窗的核心业务内容,即用户实际交互的主体部分(如表单、列表、文本信息、操作按钮等)。


后者用于构建弹窗的通用样式容器,即包裹内容的装饰性结构(如边框、背景、标题栏、关闭按钮、阴影、动画等)。


示例如下:


// 内容Builder示例@Builderexport function SampleDialogBuilder(params: QuickDialogParams) {  SampleDialog({ params: params });}@ComponentV2struct SampleDialog {  @Param @Require params: QuickDialogParams;  build() { /* 弹窗内容UI */ }}
// 装饰器Builder示例@Builderexport function SampleDecoratorBuilder(params: QuickDialogDecoratorParams) { SampleDecorator({ params: params });}@ComponentV2struct SampleDecorator { private contentNodeController = QuickDialogManager.decoratorCreateContentNodeController(this.params); build() { Column() { // 装饰器样式 NodeContainer(this.contentNodeController) // 嵌入内容 } }}
复制代码


针对原生 Navigation 方案,在 PageStack 拦截器与 NavDestination 中配置返回键处理。HMRouter 方案需添加全局生命周期监听器 QuickDialogHMLifecycle,确保弹窗随页面导航正确显隐。

四、应用 QuickDialog 集成与其他弹框方案数据对比


从应用集成后的数据对比来看,QuickDialog 在核心能力上实现了对传统弹窗方案的全面升级。

相比 CustomDialog 的高侵入性和功能局限、OpenCustomDialog 的单次生命周期限制,以及 DialogHub 的层级管理不足,QuickDialog 通过“无侵入动态创建”“页面级堆栈暂存”“内容与装饰器解耦”三大核心设计,解决了复杂弹窗场景中的层级混乱、状态丢失、复用困难等痛点。


其在系统适配性(路由联动、深色模式)和开发效率上的优势,使其更适合鸿蒙应用中多弹窗交互、状态持久化、高复用性的复杂场景,能显著降低开发维护成本并提升用户体验。

五、源码示例


// 导入QuickDialog相关控制器、管理器和参数类型,用于构建弹窗import {  QuickDialogContentNodeController,  // 弹窗内容节点控制器  QuickDialogController,             // 弹窗控制器  QuickDialogDecoratorParams,        // 弹窗装饰器参数类型  QuickDialogManager,                // 弹窗管理器,用于构建和管理弹窗  QuickDialogParams                  // 弹窗参数类型} from "quickdialog"// 导入窗口相关模块,用于处理窗口显示信息import { window } from "@kit.ArkUI"
// 入口组件,展示QuickDialog的使用示例@Entryexport struct QuickDialogDemoPage { build() { Column() { // 渲染一个按钮,点击后展示弹窗 SimpleBtn( '简单装饰器使用 - 中心弹窗示例', // 按钮文本 () => { // 点击事件回调 // 使用QuickDialogManager构建弹窗 QuickDialogManager.newBuilder( this.getUIContext(), // 获取UI上下文 wrapBuilder(SimplePureDialogBuilder), // 弹窗内容构建器 wrapBuilder(SimpleCenterDialogDecoratorBuilder) // 弹窗装饰器构建器 ) // 设置装饰器参数:水平边距 .decorateWith('testHorizontalMargin', 20) // 设置装饰器参数:边框圆角 .decorateWith('testBorderRadius', 8) // 设置弹窗内容参数 .with('testParam', '测试入参数222') // 构建弹窗并显示 .build() .show() } ) } .width("100%") // 占满父容器宽度 .height("100%") // 占满父容器高度 .justifyContent(FlexAlign.Center) // 子元素垂直居中 }}
/** * 自定义按钮组件 * @param content 按钮文本内容 * @param onClickEvent 点击事件回调 * @param customColor 自定义背景色(可选) */@Builderexport function SimpleBtn( content: ResourceStr, onClickEvent?: () => void, customColor?: ResourceColor) { Text(content) .fontSize(14) // 字体大小 .fontColor(Color.White) // 字体颜色 .textAlign(TextAlign.Center) // 文本居中 .width('calc(100% - 24vp)') // 宽度:父容器宽度减去24vp .height(40) // 高度40vp .margin({ left: 12, right: 12, top: 5, bottom: 5 }) // 边距 .backgroundColor(customColor ?? Color.Blue) // 背景色,默认蓝色 .borderRadius(4) // 边框圆角 .onClick(() => { // 点击事件 if (onClickEvent) { onClickEvent() } })}
/** * 弹窗内容构建器 * @param dialogParams 弹窗参数,用于传递数据 */@Builderexport function SimplePureDialogBuilder(dialogParams: QuickDialogParams) { // 渲染弹窗内容组件 SimplePureDialog({ dialogParams: dialogParams })}
/** * 弹窗内容组件(列表展示) */@ComponentV2struct SimplePureDialog { // 接收弹窗参数(必传) @Param @Require dialogParams: QuickDialogParams
private testText: string = '无参数' // 测试文本,默认"无参数" @Local fakeDataList: string[] = [] // 本地模拟数据列表 // 本地状态:底部安全区域高度(避免被导航栏遮挡) @Local bottomAvoidHeight: number = DisplayUtils.provideBottomAvoidHeight()
/** * 组件即将显示时调用 * 初始化数据,从弹窗参数中获取传递的值 */ aboutToAppear(): void { // 从弹窗参数中获取testParam并赋值 if (typeof this.dialogParams.data['testParam'] == 'string') { this.testText = this.dialogParams.data['testParam'] }
// 生成模拟数据列表(20条) const fakeDataList: string[] = [] for (let index = 0; index < 20; index++) { fakeDataList.push(this.testText) } this.fakeDataList = fakeDataList }
build() { // 列表展示模拟数据 List() { ForEach( this.fakeDataList, // 数据源 (value: string, index: number) => { // 迭代渲染每个列表项 ListItem() { Text(`${value} -- ${index}`) // 显示文本和索引 .fontColor(Color.Black) // 字体颜色 .padding(10) // 内边距 } } ) } .divider({ // 列表分隔线 strokeWidth: 0.5, // 线宽 color: Color.Gray // 颜色 }) .contentEndOffset(this.bottomAvoidHeight) // 列表底部偏移(避开导航栏) .scrollBar(BarState.Off) // 隐藏滚动条 .width('100%') // 宽度占满 .height('100%') // 高度占满 }}
/** * 弹窗装饰器构建器 * @param decoratorParams 装饰器参数,用于配置弹窗样式 */@Builderexport function SimpleCenterDialogDecoratorBuilder( decoratorParams: QuickDialogDecoratorParams) { // 渲染弹窗装饰器组件 SimpleCenterDialogDecorator({ decoratorParams: decoratorParams })}
/** * 弹窗装饰器组件(控制弹窗样式和动画) */@ComponentV2struct SimpleCenterDialogDecorator { // 接收装饰器参数(必传) @Param @Require decoratorParams: QuickDialogDecoratorParams // 弹窗内容节点控制器(管理弹窗内容) private contentNodeController: QuickDialogContentNodeController | undefined = undefined // 弹窗控制器(管理弹窗显示/隐藏) private dialogController: QuickDialogController | undefined = undefined @Local testHorizontalMargin: number = 0 // 水平边距(从装饰器参数获取) @Local testBorderRadius: number = 0 // 边框圆角(从装饰器参数获取)
private readonly ANIMATE_DURATION = 300 // 动画持续时间(毫秒) private readonly START_SCALE = 0.2 // 动画起始缩放比例 private readonly END_SCALE = 1 // 动画结束缩放比例 @Local testScale: number = this.START_SCALE // 缩放比例(控制动画)
/** * 组件即将显示时调用 * 初始化控制器和参数,设置动画和拦截器 */ aboutToAppear(): void { // 创建内容节点控制器 this.contentNodeController = QuickDialogManager.decoratorCreateContentNodeController(this.decoratorParams) // 获取弹窗控制器 this.dialogController = this.decoratorParams.contentParams.controller
// 从装饰器参数中获取圆角值 if (typeof this.decoratorParams.decoratorData['testBorderRadius'] == 'number') { this.testBorderRadius = this.decoratorParams.decoratorData['testBorderRadius'] } // 从装饰器参数中获取水平边距 if (typeof this.decoratorParams.decoratorData['testHorizontalMargin'] == 'number') { this.testHorizontalMargin = this.decoratorParams.decoratorData['testHorizontalMargin'] }
// 初始化显示动画 this.animateShowOrDismiss(true)
// 设置显示/隐藏拦截器(控制动画时机) this.decoratorParams.decoratorShowDismissInterceptor = { // 显示拦截:执行显示动画后再触发后续操作 onShowIntercept: (afterAction) => { this.animateShowOrDismiss(true, afterAction) }, // 隐藏拦截:执行隐藏动画后再触发后续操作 onDismissIntercept: (afterAction) => { this.animateShowOrDismiss(false, afterAction) } } }
/** * 执行显示/隐藏动画 * @param show 是否显示(true:显示动画;false:隐藏动画) * @param afterAction 动画结束后执行的回调 */ animateShowOrDismiss(show: boolean, afterAction?: () => void) { setTimeout(() => { // 执行属性动画 this.getUIContext().animateTo({ duration: this.ANIMATE_DURATION, // 动画时长 curve: Curve.EaseInOut, // 动画曲线(缓入缓出) onFinish: () => { // 动画结束回调 if (afterAction) { afterAction() } } }, () => { // 动画执行的属性变化:缩放比例 this.testScale = show ? this.END_SCALE : this.START_SCALE }) }) }
build() { Column() { // 弹窗内容容器(通过节点控制器关联内容) NodeContainer(this.contentNodeController) .onClick(() => { // 点击内容区域不做处理(避免触发外部关闭) }) // 宽度:父容器宽度减去两倍水平边距 .width(`calc(100% - ${this.testHorizontalMargin * 2}vp)`) .height(300) // 固定高度300vp .scale({ x: this.testScale, y: this.testScale }) // 应用缩放动画 .backgroundColor(Color.Yellow) // 背景色:黄色 .borderRadius(this.testBorderRadius) // 应用圆角 } .alignItems(HorizontalAlign.Center) // 水平居中 .justifyContent(FlexAlign.Center) // 垂直居中 .onClick(() => { // 点击外部区域关闭弹窗 this.dialogController?.dismiss() }) .backgroundColor(Color.Red) // 外部背景色:红色 .width('100%') // 占满父容器宽度 .height('100%') // 占满父容器高度 }}
/** * 显示工具类 * 处理窗口相关信息(如安全区域高度、窗口尺寸等) */export class DisplayUtils { private static bottomAvoidHeight: number = 0 // 底部安全区域高度(避开导航栏) private static mainWindow: window.Window | undefined = undefined // 主窗口实例
/** * 注入主窗口实例并计算底部安全区域高度 * @param mainWindow 主窗口实例 */ static injectWindowClass(mainWindow: window.Window) { DisplayUtils.mainWindow = mainWindow // 获取导航栏安全区域 let navigationIndicatorAvoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) if (navigationIndicatorAvoidArea && navigationIndicatorAvoidArea.bottomRect) { // 转换为vp单位并设置底部安全高度 DisplayUtils.injectBottomAvoidHeight(px2vp(navigationIndicatorAvoidArea.bottomRect.height)) } }
/** * 设置底部安全区域高度 * @param height 高度(vp) */ static injectBottomAvoidHeight(height: number) { DisplayUtils.bottomAvoidHeight = height }
/** * 提供底部安全区域高度 * @returns 底部安全高度(vp) */ static provideBottomAvoidHeight() { return DisplayUtils.bottomAvoidHeight }
/** * 提供窗口宽度 * @returns 窗口宽度(vp) */ static provideWindowWidth() { return px2vp(DisplayUtils.mainWindow?.getWindowProperties()?.windowRect?.width ?? 0) }
/** * 提供窗口高度 * @returns 窗口高度(vp) */ static provideWindowHeight() { return px2vp(DisplayUtils.mainWindow?.getWindowProperties()?.windowRect?.height ?? 0) }}
复制代码


发布于: 刚刚阅读数: 4
用户头像

GeorgeGcs

关注

路漫漫其修远兮,吾将上下而求索。 2024-12-24 加入

鸿蒙创作先锋,华为HDE专家,鸿蒙讲师,作者。 目前任职鸿蒙应用架构师。历经腾讯,宝马,研究所,金融。 待过私企,外企,央企。 深耕大应用开发领域十年。 OpenHarmony,HarmonyOS,Flutter,H5,Android,IOS。

评论

发布
暂无评论
【HarmonyOS】一步解决弹框集成-快速弹框QuickDialog使用详解_GeorgeGcs_InfoQ写作社区