鸿蒙应用示例:基于 promptAction 封装全局弹窗工具类

源码下载:
随着 HarmonyOS 的不断发展和完善,开发者们在构建应用时有了更多选择和灵活性。其中,promptAction 是一个非常有用的 API,允许开发者创建全局的弹窗,从而增强应用的用户体验。然而,在使用 promptAction 时,可能会遇到一些挑战,如如何正确地封装弹窗逻辑,确保弹窗的稳定性和易用性。
本文将详细介绍如何在 HarmonyOS 应用开发中,使用 promptAction API 封装一个全局弹窗工具类,并探讨一些常见问题及其解决方案。
起因
在开发过程中,我们发现使用 promptAction 创建全局弹窗时存在几个关键问题:
1. @Builder 函数中的参数要求:在 @Builder 函数中,必须要有参数,即使不使用也会导致应用崩溃。这种情况下,即使是使用 try-catch 也不能捕获异常。
2. 参数类型限制:对于 new ComponentContent(...) 的第三个参数,必须是 Object 或者类接口,而不能是基本数据类型(如 string)。如果传入基本数据类型,虽然编译时不会报错,但在运行时会引发错误 OpenCustomDialog args error code is 401。
工具类设计思路
为了解决上述问题,我们设计了一套工具类,以简化弹窗的创建、显示和关闭过程。
1. 封装弹窗操作:MyPromptActionUtil 类封装了弹窗的基本操作,包括创建、显示和关闭。
2. 生成唯一标识:MyPromptInfo 类用于生成每个弹窗的唯一标识符 dialogID,便于后续的事件监听和管理。
3. 配置弹窗属性:通过 MyPromptActionUtil 类的链式调用,可以轻松配置弹窗的各种属性,如对齐方式、是否允许侧滑关闭等。
使用示例
【工具类】src/main/ets/utils/MyPromptActionUtil.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | import { ComponentContent, PromptAction } from '@kit.ArkUI' ; import { BusinessError } from '@kit.BasicServicesKit' ; // MyPromptInfo 类用于生成唯一的 dialogID export class MyPromptInfo { public dialogID: string constructor() { this .dialogID = this .generateRandomString(10) } // 生成指定长度的随机字符串 generateRandomString(length: number): string { const characters = 'abcdefghijklmnopqrstuvwxyz0123456789' ; let result = '' ; for ( let i = 0; i < length; i++) { const randomIndex = Math.floor(Math.random() * characters.length); result += characters.charAt(randomIndex); } return result; } } // MyPromptActionUtil 类用于封装弹窗操作 export class MyPromptActionUtil<T extends MyPromptInfo> { private uiContext: UIContext; private promptAction: PromptAction; private contentNode: ComponentContent<T> | undefined; private wrapBuilder: WrappedBuilder<[T]>; private t: T; private isModal: boolean = true ; private alignment: DialogAlignment = DialogAlignment.Center; private isSwipeBackEnabled: boolean = true ; private isMaskTapToCloseEnabled: boolean = true ; public dialogID: string constructor(uiContext: UIContext, wrapBuilder: WrappedBuilder<[T]>, t: T) { this .uiContext = uiContext; this .promptAction = uiContext.getPromptAction(); this .wrapBuilder = wrapBuilder; this .t = t; this .dialogID = t.dialogID } setSwipeBackEnabled(isSwipeBackEnabled: boolean) { this .isSwipeBackEnabled = isSwipeBackEnabled; return this ; } setMaskTapToCloseEnabled(isMaskTapToCloseEnabled: boolean) { this .isMaskTapToCloseEnabled = isMaskTapToCloseEnabled return this ; } setAlignment(alignment: DialogAlignment) { this .alignment = alignment; return this ; } setModal(isModal: boolean) { this .isModal = isModal; return this ; } onDidAppear(callback: () => void) { this .onDidAppearCallback = callback; return this ; } onDidDisappear(callback: () => void) { this .onDidDisappearCallback = callback; return this ; } onWillAppear(callback: () => void) { this .onWillAppearCallback = callback; return this ; } onWillDisappear(callback: () => void) { this .onWillDisappearCallback = callback; return this ; } private onDidAppearCallback?: () => void; private onDidDisappearCallback?: () => void; private onWillAppearCallback?: () => void; private onWillDisappearCallback?: () => void; closeCustomDialog() { if ( this .contentNode) { this .promptAction.closeCustomDialog( this .contentNode); } return this ; } // 显示自定义弹窗 showCustomDialog() { try { if (! this .contentNode) { this .contentNode = new ComponentContent( this .uiContext, this .wrapBuilder, this .t); } this .promptAction.openCustomDialog( this .contentNode, { // 打开自定义弹窗 alignment: this .alignment, isModal: this .isModal, showInSubWindow: false , maskRect: { x: 0, y: 0, width: '100%' , height: '100%' }, onWillDismiss: (dismissDialogAction: DismissDialogAction) => { //弹窗响应 console.info( "reason" + JSON.stringify(dismissDialogAction.reason)) console.log( "dialog onWillDismiss" ) if (dismissDialogAction.reason == 0 && this .isSwipeBackEnabled) { //手势返回时,关闭弹窗。 this .promptAction.closeCustomDialog( this .contentNode) } if (dismissDialogAction.reason == 1 && this .isMaskTapToCloseEnabled) { this .promptAction.closeCustomDialog( this .contentNode) } }, onDidAppear: this .onDidAppearCallback ? this .onDidAppearCallback : () => { }, onDidDisappear: this .onDidDisappearCallback ? this .onDidDisappearCallback : () => { }, onWillAppear: this .onWillAppearCallback ? this .onWillAppearCallback : () => { }, onWillDisappear: this .onWillDisappearCallback ? this .onWillDisappearCallback : () => { }, }); } catch (error) { // 错误处理 let message = (error as BusinessError).message; let code = (error as BusinessError).code; console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`); } return this ; } } |
【使用示例】src/main/ets/dialog/MyDialog_1.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | @Component export struct MyDialog_1 { @Prop dialogID: string @State title: string = '加载中...' build() { Stack() { Column() { LoadingProgress() .color(Color.White).width(100).height(100) Text( this .title) .fontSize(18).fontColor(0xffffff).margin({ top: 8 }) .visibility( this .title ? Visibility.Visible : Visibility.None) } } .onClick(() => { getContext( this ).eventHub.emit( this .dialogID, "关闭弹窗" ) }) .width(180) .height(180) .backgroundColor(0x88000000) .borderRadius(10) .shadow({ radius: 10, color: Color.Gray, offsetX: 3, offsetY: 3 }) } } |
src/main/ets/dialog/MyDialog_2.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | @Component export struct MyDialog_2 { @Prop dialogID: string @Prop message: string aboutToAppear(): void { setTimeout(() => { console.info(` this .dialogID:${ this .dialogID}`) getContext( this ).eventHub.emit( this .dialogID, "关闭弹窗" ) }, 2000) } build() { Column() { Text( this .message) .fontSize( '36lpx' ) .fontColor(Color.White) .backgroundColor( "#B2000000" ) .borderRadius(8) .constraintSize({ maxWidth: '80%' }) .padding({ bottom: '28lpx' , left: '60lpx' , right: '60lpx' , top: '28lpx' }) } } } |
src/main/ets/pages/Page23.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | import { MyPromptActionUtil, MyPromptInfo } from '../utils/MyPromptActionUtil' // MyDialog_1 和 MyDialog_2 类用于定义弹窗内容 import { MyDialog_1 } from '../dialog/MyDialog_1' import { MyDialog_2 } from '../dialog/MyDialog_2' /*【注意事项】必须有参数,没参数在运行时会闪退,报错内容 Error message:is not callable SourceCode: (parent ? parent : this).observeComponentCreation2((elmtId, isInitialRender) => { */ // @Builder // function test_1() { // MyDialog_1() // } @Builder function test_1(data: MyPromptInfo) { MyDialog_1({ dialogID: data.dialogID }) } @Builder function test_2(data: MyDialog_2_Info) { MyDialog_2({ dialogID: data.dialogID, message: data.message }) } class MyDialog_2_Info extends MyPromptInfo { public message: string = "" constructor(message: string) { super () this .message = message } } @Entry @Component struct Page23 { myDialog_1: MyPromptActionUtil<MyPromptInfo> | undefined myDialog_2: MyPromptActionUtil<MyDialog_2_Info> | undefined build() { Column() { Button( '显示Loading' ).onClick(() => { this .showLoadingDialog() }) Button( '显示Toast' ).onClick(() => { this .showToast(`随机数:${ this .getRandomInt(1, 100)}`) }) } .height( '100%' ) .width( '100%' ) } getRandomInt(min: number, max: number): number { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } // 显示 Loading 弹窗 showLoadingDialog() { if (! this .myDialog_1) { this .myDialog_1 = new MyPromptActionUtil<MyPromptInfo>( this .getUIContext(), wrapBuilder(test_1), new MyPromptInfo()) .setModal( true ) //true:存在黑色半透明蒙层,false:没有蒙层 .setSwipeBackEnabled( true ) //true:侧滑允许关闭弹窗 .setMaskTapToCloseEnabled( true ) //true:点击半透明蒙层可关闭弹窗【注:如果setModal(false),那么就没有蒙层,所以点击对话框外也没有响应事件,也就是这里设置了也没效果,并且事件会穿透】 .setAlignment(DialogAlignment.Center) //对齐方式 .onDidAppear(() => { console.info( '当对话框完全呈现出来,即完成打开动画后' ) }) .onDidDisappear(() => { console.info( '当对话框完全消失,即关闭动画结束后' ) }) .onWillAppear(() => { console.info( '在对话框的打开动画开始之前调用的回调函数' ) getContext( this ).eventHub.on( this .myDialog_1?.dialogID, (data: string) => { //监听结果 if (data == '关闭弹窗' ) { this .myDialog_1?.closeCustomDialog() } }) }) .onWillDisappear(() => { console.info( '在对话框的关闭动画开始之前调用的回调函数' ) getContext( this ).eventHub.off( this .myDialog_1?.dialogID) }) } this .myDialog_1.showCustomDialog() } // 显示 Toast 弹窗 showToast(toastInfo: string) { getContext( this ).eventHub.off( this .myDialog_2?.dialogID) this .myDialog_2?.closeCustomDialog() //如果之前有的toast对话框,并且正在显示,则先关闭toast提示 this .myDialog_2 = new MyPromptActionUtil<MyDialog_2_Info>( this .getUIContext(), wrapBuilder(test_2), new MyDialog_2_Info(toastInfo)) .setModal( false ) //true:存在黑色半透明蒙层,false:没有蒙层 .setSwipeBackEnabled( false ) //true:侧滑允许关闭弹窗 .setMaskTapToCloseEnabled( true ) //true:点击半透明蒙层可关闭弹窗【注:如果setModal(false),那么就没有蒙层,所以点击对话框外也没有响应事件,也就是这里设置了也没效果,并且事件会穿透】 .setAlignment(DialogAlignment.Center) .onWillAppear(() => { console.info( '在对话框的打开动画开始之前调用的回调函数' ) getContext( this ).eventHub.on( this .myDialog_2?.dialogID, (data: string) => { //监听结果 if (data == '关闭弹窗' ) { this .myDialog_2?.closeCustomDialog() } }) }) .onWillDisappear(() => { console.info( '在对话框的关闭动画开始之前调用的回调函数' ) getContext( this ).eventHub.off( this .myDialog_2?.dialogID) }) .showCustomDialog() } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了