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

场景描述

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

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

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

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

场景二:侧滑手势拦截

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

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

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

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

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

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

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

方案描述

1. 使用Navigation.Dialog

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

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

当前限制:

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

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

演示效果:

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

其他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)

进阶用法:可以参考动态路由案例实现动态路由, 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

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

@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();

鸿蒙全栈开发全新学习指南

为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线【包含了大厂APP实战项目开发】

本路线共分为四个阶段

第一阶段:鸿蒙初中级开发必备技能

在这里插入图片描述

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:gitee.com/MNxiaona/733GH

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:gitee.com/MNxiaona/733GH

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HarmonyOS是一种基于微内核架构的分布式操作系统,而设计模式是在软件开发中用来解决特定问题的重要工具。HarmonyOS 2.3引入了一些常见的设计模式,以便开发人员能够更好地利用它们来构建高效、可靠和易于维护的应用程序。 1. 单例模式:HarmonyOS 2.3中的单例模式用于确保某个类的实例在全局范围内只能存在一个。这在多个模块需要共享数据或状态时非常有用。 2. 观察者模式:观察者模式用于在对象之间建立一种订阅-发布的关系。在HarmonyOS 2.3中,这种模式可以用于建立应用程序组件之间的通信和数据传递机制。 3. 适配器模式:适配器模式用于兼容不同接口之间的差异。在HarmonyOS 2.3中,它可以用于确保不同设备和组件之间可以无缝地进行通信。 4. 建造者模式:建造者模式用于创建对象的复杂结构。在HarmonyOS 2.3中,该模式可以用于快速构建复杂的用户界面,同时保持代码的可读性和可维护性。 5. 桥接模式:桥接模式用于将抽象部分与其实现部分解耦。在HarmonyOS 2.3中,这种模式可以用于实现应用程序组件与底层硬件或服务之间的解耦,提高代码的灵活性和可重用性。 通过使用这些设计模式,HarmonyOS 2.3为开发人员提供了更加灵活和高效的开发工具,使他们能够更好地构建出符合用户需求的应用程序。同时,这些设计模式还能够提高代码的可维护性和可扩展性,为长期的应用程序开发提供了更好的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值