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

GeorgeGcs
发布于 2025-9-1 20:57
浏览
0收藏

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

一、集成的应用背景介绍

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

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

1、小组介绍:
其中BONNET小组负责开发的应用,《鸿社圈子》。作为主攻校园平台的鸿蒙学习资源与社群应用。
【HarmonyOS】一步解决弹框集成-快速弹框QuickDialog使用详解-鸿蒙开发者社区
应用采用分层架构设计,主要分为表现层、业务逻辑层和数据层。采用模块化设计,将博客、圈子和公共功能拆分为独立模块,通过接口实现模块间通信:

【HarmonyOS】一步解决弹框集成-快速弹框QuickDialog使用详解-鸿蒙开发者社区
应用拥有基础的博客功能,作为承接学习资源主要形式,文章的平台呈现。

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

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

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


【代码仓库】
https://gitcode.com/GeorgeGcs/OpenHarmonyOSBlogApp

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 与逻辑解耦,支持生命周期全管控。

【HarmonyOS】一步解决弹框集成-快速弹框QuickDialog使用详解-鸿蒙开发者社区

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

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

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

此时QuickDialog就应运而生了。

三、QuickDialog详解

【HarmonyOS】一步解决弹框集成-快速弹框QuickDialog使用详解-鸿蒙开发者社区

// 三方库中心地址:
https://ohpm.openharmony.cn/#/cn/detail/quickdialog

// 三方库项目源码地址:
https://atomgit.com/qccmobileteam/QuickDialog

通过下载阅读QuickDialog的源码,我们梳理了其核心模块设计:
1、QuickDialogManager全局管理器
作为全局管理器,静态属性与方法实现全局弹窗状态管理的工具类。其核心职责是按页面维度管理弹窗控制器(QuickDialogController),实现弹窗与页面的绑定、缓存、销毁及系统事件适配,解决传统弹窗跨页面状态混乱的问题。
【HarmonyOS】一步解决弹框集成-快速弹框QuickDialog使用详解-鸿蒙开发者社区

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

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

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

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

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

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

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

3、技术特点总结:
基于 Overlay 技术栈实现弹窗悬浮显示,脱离页面生命周期限制。
通过 Node 机制动态创建弹窗节点,避免侵入业务页面结构。
路由适配层针对原生 Navigation 和 HMRouter 方案分别提供拦截器与生命周期监听器,确保弹窗状态与页面导航同步。
【HarmonyOS】一步解决弹框集成-快速弹框QuickDialog使用详解-鸿蒙开发者社区

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示例
@Builder
export function SampleDialogBuilder(params: QuickDialogParams) {
  SampleDialog({ params: params });
}
@ComponentV2
struct SampleDialog {
  @Param @Require params: QuickDialogParams;
  build() { /* 弹窗内容UI */ }
}

// 装饰器Builder示例
@Builder
export function SampleDecoratorBuilder(params: QuickDialogDecoratorParams) {
  SampleDecorator({ params: params });
}
@ComponentV2
struct SampleDecorator {
  private contentNodeController = QuickDialogManager.decoratorCreateContentNodeController(this.params);
  build() {
    Column() { // 装饰器样式
      NodeContainer(this.contentNodeController) // 嵌入内容
    }
  }
}

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

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

对比维度 QuickDialog CustomDialog(@CustomDialog) OpenCustomDialog(promptAction) DialogHub(第三方典型方案)
核心能力 支持弹窗堆栈暂存、层级管理 基础弹窗功能,无堆栈管理 基础自定义弹窗,单次生命周期 支持基础层级管理,无状态暂存
侵入性 无侵入(动态创建,不修改页面结构) 高侵入(需在页面内定义组件) 中侵入(需绑定页面上下文) 中侵入(需集成框架API到页面)
层级管理 页面绑定式层级控制,规则清晰 依赖页面层级,多弹窗易冲突 全局层级,无页面绑定,易混乱 全局堆栈,无页面隔离,易跨页干扰
状态暂存能力 支持弹窗状态堆栈暂存,可中断恢复 无状态暂存,关闭后状态丢失 无状态暂存,关闭后销毁 部分支持状态缓存,但无堆栈恢复
双向通讯能力 支持弹窗与页面双向数据交互 需手动实现回调,通讯链路繁琐 仅支持简单结果返回,通讯能力有限 支持基础通讯,扩展需自定义
复用性 内容与装饰器解耦,支持跨场景复用 样式与内容强耦合,复用需重复开发 样式固定,复用性低 支持组件复用,但样式扩展受限
系统适配性 支持路由拦截(Navigation/HMRouter)、深色模式自动适配 需手动适配系统配置,无路由联动 无系统配置适配能力,需手动处理 部分适配路由,深色模式需额外开发
性能表现 轻量设计,页面级缓存释放,低内存占用 随页面渲染,多弹窗易导致页面卡顿 全局实例,长期使用易内存泄漏 堆栈管理冗余,高并发下性能下降
开发效率 链式调用+Builder模式,开发效率提升40%+ 需手动管理生命周期,代码冗余 配置繁琐,复杂场景需大量定制 需学习框架API,上手成本中等

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

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

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

五、源码示例

【HarmonyOS】一步解决弹框集成-快速弹框QuickDialog使用详解-鸿蒙开发者社区

// 导入QuickDialog相关控制器、管理器和参数类型,用于构建弹窗
import {
  QuickDialogContentNodeController,  // 弹窗内容节点控制器
  QuickDialogController,             // 弹窗控制器
  QuickDialogDecoratorParams,        // 弹窗装饰器参数类型
  QuickDialogManager,                // 弹窗管理器,用于构建和管理弹窗
  QuickDialogParams                  // 弹窗参数类型
} from "quickdialog"
// 导入窗口相关模块,用于处理窗口显示信息
import { window } from "@kit.ArkUI"

// 入口组件,展示QuickDialog的使用示例
@Entry
export 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 自定义背景色(可选)
 */
@Builder
export 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 弹窗参数,用于传递数据
 */
@Builder
export function SimplePureDialogBuilder(dialogParams: QuickDialogParams) {
  // 渲染弹窗内容组件
  SimplePureDialog({ dialogParams: dialogParams })
}

/**
 * 弹窗内容组件(列表展示)
 */
@ComponentV2
struct 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 装饰器参数,用于配置弹窗样式
 */
@Builder
export function SimpleCenterDialogDecoratorBuilder(
  decoratorParams: QuickDialogDecoratorParams
) {
  // 渲染弹窗装饰器组件
  SimpleCenterDialogDecorator({
    decoratorParams: decoratorParams
  })
}

/**
 * 弹窗装饰器组件(控制弹窗样式和动画)
 */
@ComponentV2
struct 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)
  }
}

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2025-9-2 12:23:12修改
收藏
回复
举报
回复
    相关推荐