)
在 HarmonyOS 中使用CustomDialogController实现复杂交互的核心在于通过CustomDialog装饰器定义弹窗 UI利用CustomDialogController控制其显示与隐藏并通过属性传递和事件回调实现父子组件间的数据与交互解耦。以下是实现复杂交互弹窗的完整方案与实战代码一、 定义自定义弹窗组件 (CustomDialog)在弹窗组件内部定义好需要接收的数据Props和回调函数Events并在按钮点击时通过controller.close()关闭弹窗并触发回调。// 1. 定义弹窗参数接口 export interface CommonDialogOptions { title?: string; content?: string; cancelText?: string; confirmText?: string; onCancel?: () void; onConfirm?: () void; } // 2. 使用 CustomDialog 装饰器定义弹窗 UI CustomDialog export struct CommonDialog { controller: CustomDialogController; options: CommonDialogOptions; build() { Column() { if (this.options.title) { Text(this.options.title) .fontSize(18) .fontWeight(FontWeight.Bold) .margin({ top: 20, bottom: 10 }) } if (this.options.content) { Text(this.options.content) .fontSize(14) .fontColor(#666666) .margin({ bottom: 20 }) } // 按钮组交互 Row() { Button(this.options.cancelText || 取消) .flex(1) .height(44) .backgroundColor(#F5F5F5) .onClick(() { this.controller.close(); // 关闭弹窗 this.options.onCancel?.(); // 触发取消回调 }) Button(this.options.confirmText || 确认) .flex(1) .height(44) .backgroundColor(#007DFF) .margin({ left: 12 }) .onClick(() { this.controller.close(); // 关闭弹窗 this.options.onConfirm?.(); // 触发确认回调 }) } .width(100%) .margin({ bottom: 20 }) } .width(100%) .padding({ left: 20, right: 20 }) .backgroundColor(#FFFFFF) .borderRadius(12) } }二、 封装弹窗控制器 (DialogController)为了避免在每个页面都重复创建CustomDialogController推荐封装一个静态工具类来统一管理和调用弹窗。import { CommonDialog, CommonDialogOptions } from ./CommonDialog; export class DialogController { private static dialogController: CustomDialogController | null null; static showCommonDialog(options: CommonDialogOptions) { // 先关闭之前的弹窗避免重复弹出 this.dismiss(); this.dialogController new CustomDialogController({ builder: CommonDialog({ options: options }), alignment: DialogAlignment.Center, customStyle: true, cornerRadius: 12, maskColor: 0x33000000, cancelable: true, // 点击蒙层可关闭 onDismiss: () { this.dialogController null; // 弹窗关闭后释放引用避免内存泄漏 } }); this.dialogController.open(); } static dismiss() { if (this.dialogController) { this.dialogController.close(); this.dialogController null; } } }三、 页面使用示例在业务页面中只需一行代码即可唤起带有复杂交互的弹窗并通过回调处理业务逻辑。import { DialogController } from ../common/DialogController; Entry Component struct Index { build() { Column() { Button(显示自定义交互弹窗) .margin(10) .onClick(() { DialogController.showCommonDialog({ title: 提示, content: 确定要执行此操作吗, onConfirm: () { console.log(用户点击了确认); }, onCancel: () { console.log(用户点击了取消); } }); }) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } }四、 嵌套弹窗在自定义弹窗中再次弹窗在复杂的业务流中经常需要在第一个弹窗内触发第二个弹窗例如点击“确认”后弹出“操作成功”提示。实现原理在第一个CustomDialog组件内部定义第二个弹窗的CustomDialogController。当点击按钮时调用第二个控制器的open()方法。CustomDialog struct FirstDialog { controller?: CustomDialogController; // 定义第二个弹窗的控制器 dialogControllerTwo: CustomDialogController | null new CustomDialogController({ builder: SecondDialog(), alignment: DialogAlignment.Bottom, offset: { dx: 0, dy: -25 } }); build() { Column() { Text(这是第一个弹窗) Button(点我打开第二个弹窗) .margin(20) .onClick(() { if (this.dialogControllerTwo ! null) { this.dialogControllerTwo.open(); } }) } } }注若需要传入多个其他弹窗的 Controller当前自定义弹窗的 controller 定义需放在其他传入的 controller 后面。五、 样式与动画深度定制系统默认的弹窗样式往往难以满足品牌化需求通过CustomDialogControllerOptions可以实现高度定制。自定义外观样式通过设置customStyle: true开启完全自定义样式配合backgroundColor、cornerRadius、borderWidth、borderColor甚至shadow阴影属性打造精致的弹窗 UI。出入场动画定制使用openAnimation和closeAnimation属性可以自定义弹窗弹出的动画时长duration、速度曲线curve以及延迟delay实现丝滑的过渡效果。1、 自定义外观样式代码通过设置customStyle: true您可以完全接管弹窗的样式渲染并配合backgroundColor、cornerRadius、borderWidth、borderColor以及shadow等属性打造精致的 UI。dialogController: CustomDialogController | null new CustomDialogController({ builder: CustomDialogExample(), autoCancel: true, alignment: DialogAlignment.Center, offset: { dx: 0, dy: -20 }, customStyle: false, backgroundColor: 0xd9ffffff, cornerRadius: 20, width: 80%, height: 100px, borderWidth: 1, borderStyle: BorderStyle.Dashed, // 使用borderStyle属性需要和borderWidth属性一起使用 borderColor: Color.Blue, // 使用borderColor属性需要和borderWidth属性一起使用 shadow: ({ radius: 20, color: Color.Grey, offsetX: 50, offsetY: 0 }), });2、 出入场动画定制代码通过openAnimation属性您可以控制弹窗出现动画的持续时间、速度曲线、延迟等参数实现丝滑的过渡效果。dialogController: CustomDialogController | null new CustomDialogController({ builder: CustomDialogExample(), openAnimation: { duration: 1200, curve: Curve.Friction, delay: 500, playMode: PlayMode.Alternate, onFinish: () { hilog.info(DOMAIN, testTag, play end); } }, autoCancel: true, alignment: DialogAlignment.Bottom, offset: { dx: 0, dy: -20 }, gridCount: 4, customStyle: false, backgroundColor: 0xd9ffffff, cornerRadius: 10, });六、 页面级弹窗防遮挡新页面在电商或资讯类应用中常遇到一个痛点在页面 A 打开自定义弹窗此时如果跳转到页面 B弹窗会依然高高盖在所有新页面之上遮挡新页面的内容。根因揭秘CustomDialog和promptAction.showDialog()默认挂载在应用的 Root根节点显示层级高于所有的 Page 页面。解决方案如果希望弹窗随所属页面走页面跳转后弹窗自动跟随或关闭不遮挡新页必须使用页面级弹出框。方案 1使用bindSheet组件并将mode设置为SheetMode.EMBEDDED。此时弹层挂载在当前 Page/NavDestination 节点随页面入栈/出栈。方案 2在 API 15 及以上版本配置CustomDialogController的levelMode为LevelMode.EMBEDDED使其成为页面级弹窗。方案一使用 NavDestinationMode.DIALOG推荐这是目前处理页面级弹窗最优雅的方式。通过将NavDestination设置为弹窗模式弹窗会作为路由栈中的一个页面存在。每次 push 一个 Dialog 页面它就会出现在当前弹窗之上其层级顺序完全由路由栈控制天然实现了页面级绑定。// 1. 定义弹窗页面 export struct DialogPage1 { Provide(NavPathStack) pageStack: NavPathStack new NavPathStack(); build() { NavDestination() { Stack({ alignContent: Alignment.Center }) { Column() { Text(这是一个页面级弹窗) .fontSize(20) .margin({ bottom: 100 }) Button(关闭) .width(30%) .onClick(() { this.pageStack.pop(); // 出栈关闭弹窗 }) } .backgroundColor(Color.White) .borderRadius(10) .height(30%) .width(80%) } .height(100%) .width(100%) } .backgroundColor(rgba(0,0,0,0.5)) // 半透明蒙层 .hideTitleBar(true) .mode(NavDestinationMode.DIALOG) // 核心设置为弹窗模式 } } // 2. 在主页面中通过路由栈唤起 Button(打开页面级弹窗) .onClick(() { this.pageStack.pushPath({ name: DialogPage1 }); })方案二使用 openCustomDialog 全局弹窗 焦点转移对于使用openCustomDialog实现的全局自定义弹窗其所在的窗口取决于自身锚点的 UI 上下文。如果希望弹窗不被子窗口遮挡或者希望弹窗在特定的窗口中打开可以通过转移焦点来控制弹窗的层级。// 获取主窗口和子窗口 ID let subWindowID: number window.findWindow(ResizeSubWindow).getWindowProperties().id; let windowStage AppStorage.get(windowStage) as window.WindowStage; let mainWindowID: number windowStage.getMainWindowSync().getWindowProperties().id; // 将焦点从主窗口转移到子窗口使弹窗在子窗口中打开从而不被遮挡 let promise window.shiftAppWindowFocus(mainWindowID, subWindowID); promise.then(() { // 焦点转移成功后再打开全局弹窗 OMDialog.instance.open(); });方案三使用 DialogHub 三方库企业级推荐在实际开发中为了彻底解决弹窗与 UI 的耦合问题并实现弹窗随页面生命周期自动管理推荐使用华为官方推出的DialogHub解决方案。核心优势页面级生命周期绑定通过 UIObserver 实时监听应用内的路由变化。当路由发生变化页面切换或导航时DialogHub 会自动检查并隐藏旧页面的弹窗页面销毁时自动清理资源。UI 解耦支持在全局监听里创建弹窗通过链式调用的方式绑定目标组件并弹出无需在组件内部声明 Controller。// 1. 初始化 DialogHub.init(this.getUIContext()); // 2. 链式调用创建并显示弹窗自动跟随页面生命周期 DialogHub.getToast() .setContent(wrapBuilder(TextToastBuilder), new TextToastParams(提示内容)) .setAnimation({ dialogAnimation: AnimationType.UP_DOWN }) .setConfig({ dialogBehavior: { isModal: true } }) .build() .show();七、 弹窗生命周期管理从 API 19 开始自定义弹窗提供了完整的生命周期回调函数方便开发者在弹窗状态变化时执行特定逻辑如埋点上报、暂停视频播放等。触发时序依次为onWillAppear弹出框显示动效前触发。onDidAppear弹出框完全弹出后触发。onWillDisappear弹出框退出动效前触发。onDidDisappear弹出框完全消失后触发。1. 基础自定义弹窗CustomDialog在CustomDialogController中直接配置生命周期回调dialogController: CustomDialogController new CustomDialogController({ builder: CustomDialogExample(), // 生命周期回调 onWillAppear: () { console.info(弹窗即将显示准备暂停视频...); }, onDidAppear: () { console.info(弹窗已完全显示上报曝光埋点); }, onWillDisappear: () { console.info(弹窗即将关闭保存表单状态...); }, onDidDisappear: () { console.info(弹窗已完全消失恢复视频播放释放资源); } });2. 固定样式弹窗如 AlertDialog / ActionSheet通过UIContext获取PromptAction对象后在调用接口时传入生命周期参数API 19let promptAction this.getUIContext().getPromptAction(); promptAction.showDialog({ title: 提示, message: 确定要删除吗, buttons: [{ text: 确定 }, { text: 取消 }] }, { onWillAppear: () { /* 动效前 */ }, onDidAppear: () { /* 弹出后 */ }, onWillDisappear: () { /* 退出前 */ }, onDidDisappear: () { /* 消失后 */ } });3. 半模态/全模态页面bindSheet / bindContentCover直接在组件的属性链上配置生命周期回调.bindSheet($$this.isShowSheet, this.sheetBuilder(), { onWillAppear: () { /* 半模态显示动效前 */ }, onAppear: () { /* 半模态显示动效后 */ }, onWillDisappear: () { /* 半模态回退动效前 */ }, onDisappear: () { /* 半模态回退动效后 */ } })八、 模态与非模态交互模态弹窗默认isModal: true。弹窗带有蒙层不可与蒙层下方的控件进行交互不支持点击和手势向下透传。非模态弹窗isModal: false。弹窗周围的蒙层区可以透传事件。适用于需要用户在查看弹窗信息的同时依然能与底层页面进行交互的场景。1. 基础自定义弹窗CustomDialog在CustomDialogController中通过isModal属性进行配置dialogController: CustomDialogController new CustomDialogController({ builder: CustomDialogExample(), isModal: false, // 设置为非模态弹窗允许手势向下透传 autoCancel: true, alignment: DialogAlignment.Center });2. 气泡提示弹窗Popup通过bindPopup的mask属性来控制模态与非模态状态当mask为true或颜色值时气泡为模态窗口。当mask为false时气泡为非模态窗口。Button(显示非模态气泡) .bindPopup(this.showPopup, { message: 这是一个非模态提示, mask: false // 设置为非模态窗口 })3. 全局自定义弹窗openCustomDialog在使用UIContext的PromptAction打开自定义弹窗时同样可以通过isModal进行配置let promptAction this.getUIContext().getPromptAction(); promptAction.openCustomDialog(contentNode, { isModal: false, // 设置为非模态弹窗 alignment: DialogAlignment.Center });