首页 > 其他分享 >鸿蒙(HarmonyOS)实战开发篇——基于ArkUI现有能力实现自定义弹窗封装方案

鸿蒙(HarmonyOS)实战开发篇——基于ArkUI现有能力实现自定义弹窗封装方案

时间:2024-09-27 21:23:37浏览次数:3  
标签:... 自定义 dialogOptions public HarmonyOS AppDialog ArkUI options 弹窗

推荐看点

场景描述

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

步骤二:封装弹窗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();

标签:...,自定义,dialogOptions,public,HarmonyOS,AppDialog,ArkUI,options,弹窗
From: https://blog.csdn.net/lc748258/article/details/142595126

相关文章

  • Ant-design-vue Table 自定义列斑马纹效果
    业务需求在使用ant-design-vue的Table组件的时候,在某个业务模块的内因其承载的功能比较多,各个条件间界定不明显导致感官上十分的模糊,所以需要增加类似斑马纹的填充区分。table组件自带只支持行的斑马纹而我们需要的是列的斑马纹。table组件本身是不支持的所以只能通过其他方......
  • Ant-design-vue Table 自定义列斑马纹效果
    业务需求在使用ant-design-vue的Table组件的时候,在某个业务模块的内因其承载的功能比较多,各个条件间界定不明显导致感官上十分的模糊,所以需要增加类似斑马纹的填充区分。table组件自带只支持行的斑马纹而我们需要的是列的斑马纹。table组件本身是不支持的所以只能通过其他方......
  • Element UI 自定义Layout前端页面布局
    1.layout下新建front文件夹index.vue中内容<template><divclass="frontLayout"><el-container><el-header><divclass="navBar"><divclass="navBarLeft">......
  • JVM自定义类的加载器
    自定义类的加载器咱们书接上回继续说说自定义类类加载器自定义类加载器有什么用?通过类加载器可以实现非常精妙的插件机制。例如:著名的OSGI组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。......
  • HarmonyOS NEXT-CoreVision Kit-FaceDetector-实现人脸识别,获取人脸数据
    效果演示图,右边的是人脸数据,可用来比对人脸注意这里只有真机才能测试是否成功,测试机型pce-w30实现这个效果很简洁:打开相册、选取图片、打开文件、创建imageSource、创建PixelMap、喂给faceDetector拿到结果在这里我简单封装了两个工具类方便后续使用,分别是:照片选择类、......
  • HarmonyOS开发之WaterFlow组件
    在HarmonyOS应用开发中,瀑布流布局因其灵活性和美观性而广受欢迎。HarmonyOSNEXT提供了强大的WaterFlow组件,可以帮助开发者轻松实现瀑布流布局,并支持多种自定义布局和性能优化特性。本文将通过两个具体场景,详细介绍如何使用WaterFlow组件实现页面滑动加载和吸顶效果。场景一:瀑......
  • uniapp [全端兼容] - 详细实现拍照或相册选取图片后插入水印功能,手机拍照或相册上传图
    前言网上的教程乱七八糟且兼容性太差,本文提供优质示例。在uni-app全平台兼容(H5网页网站、支付宝/微信小程序、安卓App、苹果App、nvue)开发中,详解手机从相册选取上传图像后加入水印功能,手机拍摄照相后也可以加入水印,Uniapp给图片添加水印,获取上传或拍摄的图片信息后,为......
  • 鸿蒙(HarmonyOS)--声明式UI、自定义组件
    目录1.基础语法概述2.声明式UI描述2.1创建组件2.1.1无参数2.1.2有参数2.2配置属性2.3配置事件 2.4配置子组件3.自定义组件3.1创建自定义组件3.1.1基本使用3.1.2组件属性、方法3.1.3通用样式事件 3.2页面和自定义组件生命周期3.2.1自定义组件的创建......
  • PbootCMS默认面包屑导航样式修改及自定义的设置方法
    在使用PBootCMS建站时,如果需要对系统默认的面包屑标签进行样式修改,可以通过调整相应的参数来实现。以下是具体的步骤和示例代码:修改面包屑标签的样式自定义分隔符修改首页文本添加首页图标添加分割图标示例代码假设你需要修改面包屑标签的分隔符、首页文本以及图标,可以按......
  • 自定义 Git
    我们可以对Git做一些配置。‍配置别名有没有经常敲错命令?比如gitstatus​?status​这个单词真心不好记。如果敲gitst​就表示gitstatus​那就简单多了,当然这种偷懒的办法我们是极力赞成的。我们只需要敲一行命令,告诉Git,以后st​就表示status​:$gitconfig--......