【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), // 內容BuilderwrapBuilder(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通過“無侵入動態創建”“頁面級堆棧暫存”“內容與裝飾器解耦”三大核心設計,解決了復雜彈窗場景中的層級混亂、狀態丟失、復用困難等痛點。
其在系統適配性(路由聯動、深色模式)和開發效率上的優勢,使其更適合鴻蒙應用中多彈窗交互、狀態持久化、高復用性的復雜場景,能顯著降低開發維護成本并提升用戶體驗。
五、源碼示例
// 導入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: QuickDialogParamsprivate 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)}
}