当前位置:   article > 正文

基于ArkUI现有能力实现自定义弹窗封装方案_arkui-x 弹窗封装

arkui-x 弹窗封装

场景描述

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

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

场景一:在公共逻辑中触发弹窗
登录提示弹窗、全屏广告弹窗、网络请求与其他操作行为的提示、异常弹窗

场景二:侧滑手势拦截
隐私弹窗的拦截,退出登录时的确认弹窗

场景三:切换页面弹窗不消失
隐私弹窗和二级页面中的半模态弹窗

场景四:自定义弹出、关闭动画
从下往上的抽屉式弹出、关闭时从上往下收回

场景五:透明、模态、半模态背景
应用实现自定义的背景颜色

方案描述

1. 使用Navigation.Dialog

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

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

当前限制:

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

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

演示效果:

对于少量弹窗的实现,可以直接使用Navigation来进行路由跳转,参考基于Navigation的路由管理 - - 3MS知识管理社区 (huawei.com)

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

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

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

  1. export class AppRouter
  2.   private static instance = new AppRouter(); 
  3.   private pathStack: NavPathStack = new NavPathStack();  // 初始化路由栈 
  4.  
  5.   public static getInstance(): AppRouter { 
  6.     return AppRouter.instance; 
  7.   } 
  8.  
  9.   public getPathStack(): NavPathStack { 
  10.     return this.pathStack; 
  11.   } 
  12.   ... 
  13. }

在根页面中注册NavPathStack

  1. @Entry 
  2. @Component 
  3. struct Index { 
  4.   build() { 
  5.     Navigation(AppRouter.getInstance().getPathStack()) { 
  6.       ... 
  7.     } 
  8.   } 
  9. }

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

  1. @Builder 
  2. PageMap(name: string) { 
  3.   if (name === CommonConstants.DEFAULT_DIALOG) { 
  4.     DefaultDialog() 
  5.   } 
  6.   ... 
  7.  
  8. Navigation(AppRouter.getInstance().getPathStack()) { 
  9.   ... 
  10. }.navDestination(this.PageMap)

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

步骤二:封装弹窗UI组件

定义弹窗选项类AppDialogOption

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

  1. export class AppDialogStyle { 
  2.   transparent: boolean = false 
  3.   background: string = 'rgba(0,0,0,0.5)' 
  4.   radius: Length = 5 
  5.   align: Alignment = Alignment.Center 
  6. }

创建自定义弹窗组件DefaultDialog

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

  1. @Component 
  2. export struct DefaultDialog { 
  3.   private dialogOptions?: AppDialogOption; 
  4.  
  5.   build() { 
  6.     NavDestination() { 
  7.       Stack() { 
  8.         Column() { 
  9.           // 模态遮罩 
  10.         } 
  11.  
  12.         Column() { 
  13.           // 弹窗内容 
  14.         } 
  15.       } 
  16.       .width("100%") 
  17.       .height("100%") 
  18.     } 
  19.     .mode(NavDestinationMode.DIALOG)  // 页面类型为dialog 
  20.   } 
  21. }

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

  1. ... 
  2. Stack() { 
  3.   Column() { 
  4.     // 模态遮罩 
  5.   } 
  6.   .backgroundColor(this.dialogOptions?.styles?.transparent ? Color.Transparent : this.dialogOptions?.styles?.background) // 背景颜色 
  7.  
  8.   Column() { 
  9.     // 弹窗内容 
  10.   } 
  11. }
  12. 通过Stack.alignContent设置弹窗定位
  13. Stack({ 
  14.   alignContent: this.dialogOptions?.styles?.align 
  15. }) { 
  16.   Column() { 
  17.     // 模态遮罩 
  18.   } 
  19.  
  20.   Column() { 
  21.     // 弹窗内容 
  22.   } 
  23. }

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

提供链式调用的Api

  1. export class AppDialog { 
  2.   static indexArr: number[] = []; 
  3.   private stackIndex: number = 0
  4.   private options?: AppDialogOption; 
  5.  
  6.   public static buildWithOptions(options?: AppDialogOption): AppDialog { 
  7.     let instance: AppDialog = new AppDialog(); 
  8.     // 获取并保存弹窗的路由栈序号 
  9.     let index: number = AppRouter.getInstance().getPathStack().size() - 1
  10.     AppDialog.indexArr.push(index); 
  11.     instance.stackIndex = index
  12.     instance.options = options
  13.     options!.instance = instance; 
  14.     return instance; 
  15.   } 
  16.  
  17.   public static build(builder: WrappedBuilder<Object[]>): AppDialog { 
  18.     let options: AppDialogOption = new AppDialogOption(); 
  19.     options.view = builder; 
  20.     return AppDialog.buildWithOptions(options); 
  21.   } 
  22.  
  23.   public static toast(msg: string): AppDialog { 
  24.     let options: AppDialogOption = new AppDialogOption(); 
  25.     options.view = AppDialog.toastBuilder; 
  26.     options.buildParams = msg; 
  27.     return AppDialog.buildWithOptions(options); 
  28.   } 
  29.  
  30.   public static closeAll(): void { 
  31.     AppRouter.getInstance().getPathStack().removeByName(CommonConstants.DEFAULT_DIALOG); 
  32.   } 
  33.  
  34.   public static closeLast(params?: Object): void { 
  35.     let lastIndex = AppDialog.indexArr.pop() 
  36.     if (!lastIndex) { 
  37.       AppDialog.closeAll(); 
  38.     } else if (lastIndex && AppRouter.getInstance().getPathStack().size() > lastIndex) { 
  39.       AppRouter.getInstance().getPathStack().popToIndex(lastIndex, params); 
  40.     } 
  41.   } 
  42.  
  43.   public open(): AppDialog { 
  44.     AppRouter.getInstance() 
  45.       .getPathStack() 
  46.       .pushPathByName(CommonConstants.DEFAULT_DIALOG, this.options, this.options!.onPop!, true); 
  47.     return this; 
  48.   } 
  49.  
  50.   public close(params?: Object): void { 
  51.     if (AppRouter.getInstance().getPathStack().size() > this.stackIndex) { 
  52.       AppRouter.getInstance().getPathStack().popToIndex(this.stackIndex, params); 
  53.     } 
  54.   } 
  55.  
  56.   public buildParams(buildParams: Object): AppDialog { 
  57.     this.options!.buildParams = buildParams; 
  58.     return this; 
  59.   } 
  60.  
  61.   public params(params: Object): AppDialog { 
  62.     this.options!.params = params; 
  63.     return this; 
  64.   } 
  65.  
  66.   public onBackPressed(callback: () => boolean): AppDialog {...} 
  67.  
  68.   public onPop(callback: (data: PopInfo) => void): AppDialog {...} 
  69.  
  70.   public animation(animation: TransitionEffect): AppDialog {...} 
  71.  
  72.   public autoClose(time: number): AppDialog {...} 
  73.  
  74.   public align(align: Alignment): AppDialog {...} 
  75.  
  76.   public transparent(transparent: boolean): AppDialog {...} 
  77. }

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

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

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

  1. @Component 
  2. export struct DefaultDialog
  3.   private dialogOptions?: AppDialogOption
  4.  
  5.   build() { 
  6.     NavDestination() { 
  7.       ... 
  8.     } 
  9.     .onReady((ctx: NavDestinationContext) =>
  10.       console.log("onReady"
  11.       this.dialogOptions = ctx.pathInfo.param as AppDialogOption
  12.     }) 
  13.   } 
  14. }

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

  1. onPop = (data: PopInfo) =>
  2.   console.log("onPop"
  3.   // 更新状态变量 
  4.   this.params[index] = JSON.stringify(data.result) 
  5.  
  6. navPathStack.pushPathByName(CommonConstants.DEFAULT_DIALOG, this.options, this.options!.onPop!, true)

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

navPathStack.popToIndex(this.stackIndex, params);

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

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

  1. ... 
  2. Stack() { 
  3.   Column() { 
  4.     // 模态遮罩 
  5.   } 
  6.   .transition(  // 转场动画 
  7.     TransitionEffect.OPACITY.animation({ 
  8.       duration: 300
  9.       curve: Curve.Friction 
  10.     }) 
  11.   ) 
  12.  
  13.   Column() { 
  14.     // 弹窗内容 
  15.   } 
  16.   .transition(  // 转场动画 
  17.     this.dialogOptions?.animation ? 
  18.       this.dialogOptions?.animation
  19.     TransitionEffect.scale({ x: 0, y: 0 }).animation({ 
  20.       duration: 300
  21.       curve: Curve.Friction 
  22.     }) 
  23.   ) 
  24. }

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

  1. ... 
  2. Stack() { 
  3.   Column() { 
  4.     // 模态遮罩 
  5.   } 
  6.   .opacity(this.opacityNum) 
  7.   .onClick(() =>
  8.     animateTo({ 
  9.       duration: 200
  10.       curve: Curve.Friction, 
  11.       onFinish: () =>
  12.         this.dialogOptions?.instance?.close(); 
  13.       } 
  14.     }, () =>
  15.       this.opacityNum = 0  // 修改模态遮罩的透明度 
  16.       if (this.dialogOptions?.styles?.align === Alignment.Bottom) { 
  17.         this.translateY = "100%" 
  18.       } 
  19.     }) 
  20.   }) 
  21.  
  22.   Column() { 
  23.     // 弹窗内容 
  24.   } 
  25.   .translate({ x: 0, y: this.translateY }) 
  26. }

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

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

  1. Stack() { 
  2.   Column() { 
  3.     // 模态遮罩 
  4.   } 
  5.  
  6.   Column() { 
  7.     // 弹窗内容 
  8.     this.dialogOptions?.view?.builder(this.dialogOptions); 
  9.   } 
  10. }

定义弹窗内容组件

  1. @Builder 
  2. export function DialogViewBuilder(dialogOptions: AppDialogOption) { 
  3.   DialogView({ options: dialogOptions }) 
  4.  
  5. @Component 
  6. struct DialogView { 
  7.   private options?: dialogOptions
  8.  
  9.   build() { 
  10.     Column() { 
  11.     } 
  12.     ... 
  13.   } 
  14. }

步骤六:侧滑手势拦截

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

  1. @Component 
  2. export struct DefaultDialog
  3.   private dialogOptions?: AppDialogOption
  4.  
  5.   build() { 
  6.     NavDestination() { 
  7.       ... 
  8.     } 
  9.     .onBackPressed((): boolean =>
  10.       // true为拦截 
  11.       if (this.dialogOptions?.onBackPressed) { 
  12.         return this.dialogOptions?.onBackPressed() 
  13.       } else
  14.         return false
  15.       } 
  16.     }) 
  17.   } 
  18. }

使用效果:

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

  1. export class AppService { 
  2.   buzz(): void { 
  3.     setTimeout(() =>
  4.       AppDialog 
  5.         .toast("登录成功"
  6.         .onBackPressed(() => true
  7.         .autoClose(2000
  8.         .transparent(true
  9.         .open(); 
  10.     }, 1000)  // 模拟业务接口调用耗时 
  11.   } 
  12.  
  13. AppDialog.toastBuilder = wrapBuilder(ToastViewBuilder) 
  14.  
  15. @Builder 
  16. export function ToastViewBuilder(dialogOptions: AppDialogOption) { 
  17.   ToastView({ msg: dialogOptions.buildParams as string }) 
  18.  
  19. @Component 
  20. struct ToastView { 
  21.   private msg?: string
  22.   build() { 
  23.     Column() { 
  24.       Text(this.msg) 
  25.         .fontSize(14
  26.         .fontColor(Color.White) 
  27.         .padding(10
  28.     } 
  29.     .backgroundColor("rgba(0,0,0,0.8)"
  30.     .justifyContent(FlexAlign.Center) 
  31.     .borderRadius(12
  32.     .width(100
  33.   } 
  34. }

关闭弹窗

  1. // 全局使用 
  2. AppDialog.closeLast(); 
  3. AppDialog.closeAll(); 
  4.  
  5. // 弹窗页面中使用 
  6. this.dialogOptions?.instance?.close();
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/985400
推荐阅读
相关标签
  

闽ICP备14008679号