【鸿蒙实战开发】基于ArkUI现有能力实现自定义弹窗封装方案

场景描述

自定义弹窗是应用开发需要实现的基础功能,包括但不限于HarmonyOS开发者文档中定义的模态、半模态、Toast等形式,封装一个好用且和UI组件解耦的弹窗组件是开发者的高频诉求

自定义弹窗通常的使用场景有:

场景一:在公共逻辑中触发弹窗

登录提示弹窗、全屏广告弹窗、网络请求与其他操作行为的提示、异常弹窗

场景二:侧滑手势拦截

隐私弹窗的拦截,退出登录时的确认弹窗

场景三:切换页面弹窗不消失

隐私弹窗和二级页面中的半模态弹窗

场景四:自定义弹出、关闭动画

从下往上的抽屉式弹出、关闭时从上往下收回

场景五:透明、模态、半模态背景

应用实现自定义的背景颜色

方案描述

1. 使用Navigation.Dialog

基于Navigation.Dialog的透明页面特性,可以用于实现弹窗效果

而且Navigation.Dialog存在于路由栈中,天然可以实现切换页面弹窗不消失

当前限制:

弹窗组件中的动效建议开发者自行实现

Navigation.Dialog自身无颜色,需要开发者自行实现模态遮罩,以及手势事件。

演示效果:

对于少量弹窗的实现,可以直接使用Navigation来进行路由跳转,参考 Navigation常见场景及解决方案

其他Navigation的使用也可参考上述文章

步骤一:封装路由工具类,并注册自定义弹窗组件

定义路由工具类AppRouter,并创建路由栈NavPathStack

text
复制代码
export class AppRouter {   private static instance = new AppRouter();   private pathStack: NavPathStack = new NavPathStack();  // 初始化路由栈   public static getInstance(): AppRouter {     return AppRouter.instance;   }   public getPathStack(): NavPathStack {     return this.pathStack;   }   ... }

在根页面中注册NavPathStack

text
复制代码
@Entry @Component struct Index {   build() {     Navigation(AppRouter.getInstance().getPathStack()) {       ...     }   } }

在.navDestination注册封装的自定义弹窗组件DefaultDialog

text
复制代码
@Builder PageMap(name: string) {   if (name === CommonConstants.DEFAULT_DIALOG) {     DefaultDialog()   }   ... } Navigation(AppRouter.getInstance().getPathStack()) {   ... }.navDestination(this.PageMap)

进阶用法:可以参考动态路由案例实现动态路由, HarmonyOS NEXT应用开发案例集 - Gitee.com

步骤二:封装弹窗UI 组件

定义弹窗选项类AppDialogOption

text
复制代码
export class AppDialogOption {   view?: WrappedBuilder<Object[]> // 自定义弹窗内容组件   buildParams?: Object  // 自定义弹窗内容参数   params?: Object  // 打开时传递参数   autoClose?: number  // 自动关闭时间   onPop?: (data: PopInfo) => void  // 接收上一个弹窗关闭时的参数回调   onBackPressed?: () => boolean  // 侧滑返回拦截   styles?: AppDialogStyle = new AppDialogStyle()  // 弹窗样式   animation?: TransitionEffect  // 弹窗动画   instance?: AppDialog  // 弹窗操作对象 } 定义弹窗样式类AppDialogStyle export class AppDialogStyle {   transparent: boolean = false   background: string = 'rgba(0,0,0,0.5)'   radius: Length = 5   align: Alignment = Alignment.Center }

创建自定义弹窗组件DefaultDialog

通过Stack布局及2个Column容器实现模态遮罩和自定义弹窗内容,通过NavDestinationMode定义页面类型

text
复制代码
@Component export struct DefaultDialog {   private dialogOptions?: AppDialogOption;   build() {     NavDestination() {       Stack() {         Column() {           // 模态遮罩         }         Column() {           // 弹窗内容         }       }       .width("100%")       .height("100%")     }     .mode(NavDestinationMode.DIALOG)  // 页面类型为dialog   } }

通过.backgroundColor设置模态遮罩的背景颜色

text
复制代码
... Stack() {   Column() {     // 模态遮罩   }   .backgroundColor(this.dialogOptions?.styles?.transparent ? Color.Transparent : this.dialogOptions?.styles?.background) // 背景颜色   Column() {     // 弹窗内容   } }

通过Stack.alignContent设置弹窗定位

text
复制代码
Stack({   alignContent: this.dialogOptions?.styles?.align }) {   Column() {     // 模态遮罩   }   Column() {     // 弹窗内容   } }

步骤三:封装弹窗控制器,与UI 组件解耦

提供链式调用的Api

text
复制代码
export class AppDialog {   static indexArr: number[] = [];   private stackIndex: number = 0;   private options?: AppDialogOption;   public static buildWithOptions(options?: AppDialogOption): AppDialog {     let instance: AppDialog = new AppDialog();     // 获取并保存弹窗的路由栈序号     let index: number = AppRouter.getInstance().getPathStack().size() - 1;     AppDialog.indexArr.push(index);     instance.stackIndex = index;     instance.options = options;     options!.instance = instance;     return instance;   }   public static build(builder: WrappedBuilder<Object[]>): AppDialog {     let options: AppDialogOption = new AppDialogOption();     options.view = builder;     return AppDialog.buildWithOptions(options);   }   public static toast(msg: string): AppDialog {     let options: AppDialogOption = new AppDialogOption();     options.view = AppDialog.toastBuilder;     options.buildParams = msg;     return AppDialog.buildWithOptions(options);   }   public static closeAll(): void {     AppRouter.getInstance().getPathStack().removeByName(CommonConstants.DEFAULT_DIALOG);   }   public static closeLast(params?: Object): void {     let lastIndex = AppDialog.indexArr.pop()     if (!lastIndex) {       AppDialog.closeAll();     } else if (lastIndex && AppRouter.getInstance().getPathStack().size() > lastIndex) {       AppRouter.getInstance().getPathStack().popToIndex(lastIndex, params);     }   }   public open(): AppDialog {     AppRouter.getInstance()       .getPathStack()       .pushPathByName(CommonConstants.DEFAULT_DIALOG, this.options, this.options!.onPop!, true);     return this;   }   public close(params?: Object): void {     if (AppRouter.getInstance().getPathStack().size() > this.stackIndex) {       AppRouter.getInstance().getPathStack().popToIndex(this.stackIndex, params);     }   }   public buildParams(buildParams: Object): AppDialog {     this.options!.buildParams = buildParams;     return this;   }   public params(params: Object): AppDialog {     this.options!.params = params;     return this;   }   public onBackPressed(callback: () => boolean): AppDialog {...}   public onPop(callback: (data: PopInfo) => void): AppDialog {...}   public animation(animation: TransitionEffect): AppDialog {...}   public autoClose(time: number): AppDialog {...}   public align(align: Alignment): AppDialog {...}   public transparent(transparent: boolean): AppDialog {...} }

步骤四:页面与弹窗,弹窗与弹窗之间传递参数

通过路由跳转NavPathStack.pushPathByName传递参数

在弹窗组件的.onReady事件中获取路由跳转参数。

text
复制代码
@Component export struct DefaultDialog {   private dialogOptions?: AppDialogOption;   build() {     NavDestination() {       ...     }     .onReady((ctx: NavDestinationContext) => {       console.log("onReady")       this.dialogOptions = ctx.pathInfo.param as AppDialogOption;     })   } }

使用NavPathStack中的onPop回调来接收上一个弹窗返回的参数。

text
复制代码
onPop = (data: PopInfo) => {   console.log("onPop")   // 更新状态变量   this.params[index] = JSON.stringify(data.result) } navPathStack.pushPathByName(CommonConstants.DEFAULT_DIALOG, this.options, this.options!.onPop!, true)

上一个弹窗在关闭时传入参数

text
复制代码
navPathStack.popToIndex(this.stackIndex, params);

步骤五:实现弹窗自定义动画

通过.transition属性分别实现背景和内容的转场动画

text
复制代码
... Stack() {   Column() {     // 模态遮罩   }   .transition(  // 转场动画     TransitionEffect.OPACITY.animation({       duration: 300,       curve: Curve.Friction     })   )   Column() {     // 弹窗内容   }   .transition(  // 转场动画     this.dialogOptions?.animation ?       this.dialogOptions?.animation :     TransitionEffect.scale({ x: 0, y: 0 }).animation({       duration: 300,       curve: Curve.Friction     })   ) }

通过监听模态遮罩的点击事件实现关闭动画

text
复制代码
... Stack() {   Column() {     // 模态遮罩   }   .opacity(this.opacityNum)   .onClick(() => {     animateTo({       duration: 200,       curve: Curve.Friction,       onFinish: () => {         this.dialogOptions?.instance?.close();       }     }, () => {       this.opacityNum = 0  // 修改模态遮罩的透明度       if (this.dialogOptions?.styles?.align === Alignment.Bottom) {         this.translateY = "100%"       }     })   })   Column() {     // 弹窗内容   }   .translate({ x: 0, y: this.translateY }) }

步骤五:实现自定义弹窗内容

在弹窗内容的Column容器中传入WrappedBuilder来实现动态的自定义弹窗内容。

text
复制代码
Stack() {   Column() {     // 模态遮罩   }   Column() {     // 弹窗内容     this.dialogOptions?.view?.builder(this.dialogOptions);   } }

定义弹窗内容组件

text
复制代码
@Builder export function DialogViewBuilder(dialogOptions: AppDialogOption) {   DialogView({ options: dialogOptions }) } @Component struct DialogView {   private options?: dialogOptions ;   build() {     Column() {     }     ...   } }

步骤六:侧滑手势拦截

在弹窗组件的.onBackPressed事件中进行拦截

text
复制代码
@Component export struct DefaultDialog {   private dialogOptions?: AppDialogOption;   build() {     NavDestination() {       ...     }     .onBackPressed((): boolean => {       // true为拦截       if (this.dialogOptions?.onBackPressed) {         return this.dialogOptions?.onBackPressed()       } else {         return false;       }     })   } }

使用效果:

使用弹窗控制器即可在非UI业务逻辑中打开弹窗

text
复制代码
export class AppService {   buzz(): void {     setTimeout(() => {       AppDialog         .toast("登录成功")         .onBackPressed(() => true)         .autoClose(2000)         .transparent(true)         .open();     }, 1000)  // 模拟业务接口调用耗时   } } AppDialog.toastBuilder = wrapBuilder(ToastViewBuilder) @Builder export function ToastViewBuilder(dialogOptions: AppDialogOption) {   ToastView({ msg: dialogOptions.buildParams as string }) } @Component struct ToastView {   private msg?: string;   build() {     Column() {       Text(this.msg)         .fontSize(14)         .fontColor(Color.White)         .padding(10)     }     .backgroundColor("rgba(0,0,0,0.8)")     .justifyContent(FlexAlign.Center)     .borderRadius(12)     .width(100)   } }

关闭弹窗

text
复制代码
// 全局使用 AppDialog.closeLast(); AppDialog.closeAll(); // 弹窗页面中使用 this.dialogOptions?.instance?.close();

写在最后

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力;
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识;
  • 想要获取更多完整鸿蒙最新学习知识点,可关注B站:码牛课堂鸿蒙开发;
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
OH小贾
下载 APP