HarmonyOS 开发实践——基于ArkUI现有能力实现自定义弹窗封装方案

场景描述

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

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

  • 场景一:在公共逻辑中触发弹窗
    登录提示弹窗、全屏广告弹窗、网络请求与其他操作行为的提示、异常弹窗
  • 场景二:侧滑手势拦截
    隐私弹窗的拦截,退出登录时的确认弹窗
  • 场景三:切换页面弹窗不消失
    隐私弹窗和二级页面中的半模态弹窗
  • 场景四:自定义弹出、关闭动画
    从下往上的抽屉式弹出、关闭时从上往下收回
  • 场景五:透明、模态、半模态背景
    应用实现自定义的背景颜色

方案描述

1. 使用Navigation.Dialog

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

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

当前限制:

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

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

演示效果:

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

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

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

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

@Entry 
@Component 
struct Index
  build() { 
    Navigation(AppRouter.getInstance().getPathStack()) { 
      ... 
    } 
  } 
}

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

@Builder 
PageMap(name: string) { 
  if (name === CommonConstants.DEFAULT_DIALOG) { 
    DefaultDialog() 
  } 
  ... 

 
Navigation(AppRouter.getInstance().getPathStack()) { 
  ... 
}.navDestination(this.PageMap)

步骤二:封装弹窗UI组件

定义弹窗选项类AppDialogOption

@Builder 
PageMap(name: string) { 
  if (name === CommonConstants.DEFAULT_DIALOG) { 
    DefaultDialog() 
  } 
  ... 

 
Navigation(AppRouter.getInstance().getPathStack()) { 
  ... 
}.navDestination(this.PageMap)

定义弹窗样式类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定义页面类型

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

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

... 
Stack() { 
  Column() { 
    // 模态遮罩 
  } 
  .backgroundColor(this.dialogOptions?.styles?.transparent ? Color.Transparent : this.dialogOptions?.styles?.background) // 背景颜色 
 
  Column() { 
    // 弹窗内容 
  } 
}
通过Stack.alignContent设置弹窗定位
Stack({ 
  alignContent: this.dialogOptions?.styles?.align 
}) { 
  Column() { 
    // 模态遮罩 
  } 
 
  Column() { 
    // 弹窗内容 
  } 
}

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

提供链式调用的Api

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): 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事件中获取路由跳转参数。

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

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

onPop = (data: PopInfo) =>
  console.log("onPop"
  // 更新状态变量 
  this.params[index] = JSON.stringify(data.result

 
navPathStack.pushPathByName(CommonConstants.DEFAULT_DIALOG, this.options, this.options!.onPop!, true)

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

navPathStack.popToIndex(this.stackIndex, params);

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

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

... 
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 
    }) 
  ) 
}

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

... 
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来实现动态的自定义弹窗内容。

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

定义弹窗内容组件

@Builder 
export function DialogViewBuilder(dialogOptions: AppDialogOption) { 
  DialogView({ options: dialogOptions }) 

 
@Component 
struct DialogView
  private options?: dialogOptions
 
  build() { 
    Column() { 
    } 
    ... 
  } 
}

步骤七:侧滑手势拦截

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

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

使用效果:

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

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
  } 
}

关闭弹窗

// 全局使用 
AppDialog.closeLast(); 
AppDialog.closeAll(); 
 
// 弹窗页面中使用 
this.dialogOptions?.instance?.close();
阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.1024c.cn/archives/21951,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?