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

546 篇文章 5 订阅
435 篇文章 3 订阅

场景描述

自定义弹窗是应用开发需要实现的基础功能,包括但不限于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();

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

之前总有很多小伙伴向我反馈说,不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以这里为大家准备了一份实用的鸿蒙(HarmonyOS NEXT)学习路线与学习文档用来跟着学习是非常有必要的。

针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

①全方位,更合理的学习路径
路线图包括ArkTS基础语法、鸿蒙应用APP开发、鸿蒙能力集APP开发、次开发多端部署开发、物联网开发等九大模块,六大实战项目贯穿始终,由浅入深,层层递进,深入理解鸿蒙开发原理!

②多层次,更多的鸿蒙原生应用
路线图将包含完全基于鸿蒙内核开发的应用,比如一次开发多端部署、自由流转、元服务、端云一体化等,多方位的学习内容让学生能够高效掌握鸿蒙开发,少走弯路,真正理解并应用鸿蒙的核心技术和理念。

③实战化,更贴合企业需求的技术点
学习路线图中的每一个技术点都能够紧贴企业需求,经过多次真实实践,每一个知识点、每一个项目,都是码牛课堂鸿蒙研发团队精心打磨和深度解析的成果,注重对学生的细致教学,每一步都确保学生能够真正理解和掌握。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:https://qr21.cn/FV7h05

如何快速入门:

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

开发基础知识:https://qr21.cn/FV7h05

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

基于ArkTS 开发:https://qr21.cn/FV7h05

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

鸿蒙开发面试真题(含参考答案):https://qr21.cn/FV7h05

大厂鸿蒙面试题::https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值