在 HarmonyOS 中,使用 CustomDialogController 实现复杂交互的核心在于:通过 @CustomDialog 装饰器定义弹窗 UI,利用 CustomDialogController 控制其显示与隐藏,并通过属性传递和事件回调实现父子组件间的数据与交互解耦。
以下是实现复杂交互弹窗的完整方案与实战代码:
一、 定义自定义弹窗组件 (@CustomDialog)
在弹窗组件内部,定义好需要接收的数据(Props)和回调函数(Events),并在按钮点击时通过 controller.close() 关闭弹窗并触发回调。
TypeScript
// 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,推荐封装一个静态工具类来统一管理和调用弹窗。
TypeScript
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;
}
}
}
三、 页面使用示例
在业务页面中,只需一行代码即可唤起带有复杂交互的弹窗,并通过回调处理业务逻辑。
TypeScript
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() 方法。
TypeScript
@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。
TypeScript
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 属性,您可以控制弹窗出现动画的持续时间、速度曲线、延迟等参数,实现丝滑的过渡效果。
TypeScript
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 页面,它就会出现在当前弹窗之上,其层级顺序完全由路由栈控制,天然实现了页面级绑定。
TypeScript
// 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 上下文。如果希望弹窗不被子窗口遮挡,或者希望弹窗在特定的窗口中打开,可以通过转移焦点来控制弹窗的层级。
TypeScript
// 获取主窗口和子窗口 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。
TypeScript
// 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 中直接配置生命周期回调:
TypeScript
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample(),
// 生命周期回调
onWillAppear: () => {
console.info('弹窗即将显示,准备暂停视频...');
},
onDidAppear: () => {
console.info('弹窗已完全显示,上报曝光埋点');
},
onWillDisappear: () => {
console.info('弹窗即将关闭,保存表单状态...');
},
onDidDisappear: () => {
console.info('弹窗已完全消失,恢复视频播放,释放资源');
}
});
2. 固定样式弹窗(如 AlertDialog / ActionSheet)
通过 UIContext 获取 PromptAction 对象后,在调用接口时传入生命周期参数(API 19+):
TypeScript
let promptAction = this.getUIContext().getPromptAction();
promptAction.showDialog({
title: '提示',
message: '确定要删除吗?',
buttons: [{ text: '确定' }, { text: '取消' }]
}, {
onWillAppear: () => { /* 动效前 */ },
onDidAppear: () => { /* 弹出后 */ },
onWillDisappear: () => { /* 退出前 */ },
onDidDisappear: () => { /* 消失后 */ }
});
3. 半模态/全模态页面(bindSheet / bindContentCover)
直接在组件的属性链上配置生命周期回调:
TypeScript
.bindSheet($$this.isShowSheet, this.sheetBuilder(), {
onWillAppear: () => { /* 半模态显示动效前 */ },
onAppear: () => { /* 半模态显示动效后 */ },
onWillDisappear: () => { /* 半模态回退动效前 */ },
onDisappear: () => { /* 半模态回退动效后 */ }
})
八、 模态与非模态交互
- 模态弹窗(默认) :
isModal: true。弹窗带有蒙层,不可与蒙层下方的控件进行交互(不支持点击和手势向下透传)。 - 非模态弹窗 :
isModal: false。弹窗周围的蒙层区可以透传事件。适用于需要用户在查看弹窗信息的同时,依然能与底层页面进行交互的场景。
1. 基础自定义弹窗(CustomDialog)
在 CustomDialogController 中通过 isModal 属性进行配置:
TypeScript
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample(),
isModal: false, // 设置为非模态弹窗,允许手势向下透传
autoCancel: true,
alignment: DialogAlignment.Center
});
2. 气泡提示弹窗(Popup)
通过 bindPopup 的 mask 属性来控制模态与非模态状态:
- 当
mask为true或颜色值时,气泡为模态窗口。 - 当
mask为false时,气泡为非模态窗口。
TypeScript
Button('显示非模态气泡')
.bindPopup(this.showPopup, {
message: '这是一个非模态提示',
mask: false // 设置为非模态窗口
})
3. 全局自定义弹窗(openCustomDialog)
在使用 UIContext 的 PromptAction 打开自定义弹窗时,同样可以通过 isModal 进行配置:
TypeScript
let promptAction = this.getUIContext().getPromptAction();
promptAction.openCustomDialog(contentNode, {
isModal: false, // 设置为非模态弹窗
alignment: DialogAlignment.Center
});