HarmonyOS弹窗+bindSheet半模态+浮层通用解决方案覆盖全业务场景

前言

从api9开始开发鸿蒙的大佬们想必一开始被弹窗折腾得有点难受,使用起来各种不顺手且不方便还有各种限制, 尤其是面对复杂且多样化的需求设计场景下太难受了,还好ComponentContent在api12的版本出现了

ComponentContent的出现能解决什么问题?

以CustomDialog弹窗为例,如果要使用弹窗CustomDialogController必须定义在页面中,通过CustomDialogController控制弹窗显示隐藏,这样就至少带来2个问题:

1:哪里需要弹窗就需要在哪里声明CustomDialogController,不能直接在其他普通类(非@Component)直接使用弹窗,这样就导致把弹窗的部分逻辑强制耦合在页面上,业务越复杂,耦合度越高
2:在实际业务场景下弹窗不方便改成普通页面或者普通组件

而通过ComponentContent实现弹窗的方案只需要一个uiContext即可在任意地方控制弹窗显示,而且还能快速的改造成其他页面、组件、甚至是overlay(浮层)和bindSheet(半模态)且入侵性极低

基于ComponentContent实现的弹窗有多方便?

显示弹窗的核心代码就三行(实例化Dialog,设置视图,显示弹窗),以前做Android开发的应该对这个写法很熟悉

text 复制代码
import { BaseDialog, ComponentParam } from '@zhongrui/easy_dialog'

@Entry
@Component
struct Page {
  build() {
    Column() {
      Button('显示弹窗').onClick(() => {
        //只要有uiContext,此处显示弹窗的代码可以写到主线程的任意地方
        const dialog = new BaseDialog(this.getUIContext())
        //传入弹窗视图和参数
        dialog.setContentView(wrapBuilder(dialogView), new ComponentParam('自定义参数'))
        dialog.show()
      })
      
      //弹窗的视图以组件的形式在页面中直接使用
      DialogView({ param: new ComponentParam('自定义参数') })
    }
    .height('100%')
    .width('100%')
  }
}

@Builder
function dialogView(param: ComponentParam<string>) {
  DialogView({ param: param })
}

@ComponentV2
struct DialogView {
  @Param param: ComponentParam<string> = new ComponentParam('')

  build() {
    Button('销毁弹窗').onClick(() => {
      this.param.dismiss()
    })
  }
}

基于ComponentContent实现的弹窗如何快速改造成页面、组件、甚至是overlay(浮层)和bindSheet(半模态)

text 复制代码
//弹窗的视图以组件的形式在页面中直接使用,其他地方可以不做任何修改编译不会报错
DialogView({ param: new ComponentParam('自定义参数') })
text 复制代码
//改成overlay浮层
const dialog = new BaseOverlay(this.getUIContext())
dialog.setContentView(wrapBuilder(dialogView), new ComponentParam('自定义参数'))
dialog.show()
text 复制代码
//改成bindSheet半模态
const dialog = new BaseSheet(this.getUIContext(),'页面组件id')
dialog.setContentView(wrapBuilder(dialogView), new ComponentParam('自定义参数'))
dialog.show()

没错,只需要在实例化的时候,改个名字就行了

下载安装

ohpm install @zhongrui/easy_dialog

easy_dialog开源库中心仓(api详细使用说明)
源码地址

效果图

当出现以下场景时使用easy_dialog轻松应对

  • 场景1:不同页面显示同一个弹窗时,弹窗上面某些埋点上报需要上报不同页面来源(页面传递参数给弹窗视图)

  • 场景2:在弹窗的视图上执行某个操作之后,需要将某些数据同步给页面(弹窗视图和页面通信)

  • 场景3:页面满足某个状态时,需要将某些数据同步给弹窗视图(页面和弹窗视图通信)

  • 场景4:页面出现多个运营活动弹窗,产品要求出现多个弹窗的场景,上一个弹窗消失时,下一个弹窗才能出现,不能同时显示

  • 场景5:页面,弹窗,半模态之间切换

    领导:这个登录页面你改成弹窗实现吧

    我:修改这个大概需要1个小时(将页面改成弹窗用easy_dialog实际上10分钟不到就改完了)

    领导:这个弹窗能不能滑上去,我看其他app有这个效果,能不能改成这样的效果?

    我:我瞅瞅,噢~这个半模态效果啊,可以改的

    领导:我听其他开发说这是bindSheet,你再花1小时研究下怎么实现的

    我:好的好的(用2秒钟把BaseDialog改成了BaseSheet,然后摸鱼59分钟后去给领导演示效果)

实现上述场景的需求,核心是解耦的条件下进行传参和通信,只要解决这两个问题那么该方案就能覆盖99%以上的业务场景

场景1:不同页面显示同一个弹窗时,弹窗上面某些埋点上报需要上报不同页面来源

text 复制代码
const dialog = new BaseDialog(this.getUIContext())
dialog.setContentView(wrapBuilder(dialogView), new ComponentParam('自定义参数'))
//通过Tag给Dialog设置页面来源
dialog.addTag('pageSource','xxx')
dialog.show()

@Builder
function dialogView(param: ComponentParam<string>) {
  DialogView({ param: param })
}
text 复制代码
@ComponentV2
struct DialogView {
  @Param param: ComponentParam<string> = new ComponentParam('')

  aboutToAppear(): void {
    //获取页面来源
    const pageSource = this.param.getTag('pageSource')
  }

  build() {
    Text()
  }
}

场景2:在弹窗的视图上执行某个操作之后,需要将某些数据同步给页面

text 复制代码
@Entry
@Component
struct Page {
  build() {
    Column() {
      Button('显示弹窗').onClick(() => {
        const dialog = new BaseDialog(this.getUIContext())
        dialog.setContentView(wrapBuilder(dialogView), new ComponentParam('自定义参数'))
        //注册事件,接收弹窗视图发送的事件
        dialog.addEvent<string>('key', (data) => {
          //获取事件传的参数
          const result = data
          return '返回值'
        })
        dialog.show()
      })
    }
    .height('100%')
    .width('100%')
  }
}

@Builder
function dialogView(param: ComponentParam<string>) {
  DialogView({ param: param })
}
text 复制代码
@ComponentV2
struct DialogView {
  @Param param: ComponentParam<string> = new ComponentParam('')

  build() {
    Text('xxx操作').onClick(() => {
      //给Dialog发送事件且获取结果
      const result = this.param.sendEvent('key', '自定义数据')
    })
  }
}

场景3:页面满足某个状态时,需要将某些数据同步给弹窗视图(页面和弹窗视图通信)

text 复制代码
import { BaseDialog, ComponentParam } from '@zhongrui/easy_dialog'

@Entry
@Component
struct Page {
  build() {
    Column() {
      Button('显示弹窗').onClick(() => {
        const dialog = new BaseDialog(this.getUIContext())
        dialog.setContentView(wrapBuilder(dialogView), new ComponentParam('自定义参数'))
        dialog.show()

        setTimeout(() => {
          //模拟触发某个状态
          //给弹窗视图发送事件并获取返回值
          const result = dialog.sendEvent('key', '自定义数据')
        }, 2000)
      })
    }
    .height('100%')
    .width('100%')
  }
}

@Builder
function dialogView(param: ComponentParam<string>) {
  DialogView({ param: param })
}
text 复制代码
@ComponentV2
struct DialogView {
  @Param param: ComponentParam<string> = new ComponentParam('')

  aboutToAppear(): void {
    //注册事件,接收dialog发送的事件
    this.param.addEvent<string>('key', (data) => {
      //获取事件传的参数
      const result = data
      return '返回值'
    })
  }

  build() {
  }
}

场景4:页面出现多个运营活动弹窗,产品要求出现多个弹窗的场景,上一个弹窗消失时,下一个弹窗才能出现,不能同时显示

text 复制代码
import { BaseDialog, ComponentParam, DialogManager } from '@zhongrui/easy_dialog'

@Entry
@Component
struct Page {
  build() {
    Column() {
      Button('显示弹窗').onClick(() => {
        const dialog1 = new BaseDialog(this.getUIContext())
        dialog1.setContentView(wrapBuilder(dialogView), new ComponentParam('第1个弹窗'))

        const dialog2 = new BaseDialog(this.getUIContext())
        dialog2.setContentView(wrapBuilder(dialogView), new ComponentParam('第2个弹窗'))

        const dialog3 = new BaseDialog(this.getUIContext())
        dialog3.setContentView(wrapBuilder(dialogView), new ComponentParam('第3个弹窗'))

        DialogManager.get('def').show(dialog1)
        DialogManager.get('def').show(dialog2)
        DialogManager.get('def').show(dialog3)
        //或者
        DialogManager.get('def').addDialog(dialog1)
        DialogManager.get('def').addDialog(dialog2)
        DialogManager.get('def').addDialog(dialog3)
        DialogManager.get('def').show()
      })
    }
    .height('100%')
    .width('100%')
  }
}

@Builder
function dialogView(param: ComponentParam<string>) {
  DialogView({ param: param })
}
text 复制代码
@ComponentV2
struct DialogView {
  @Param param: ComponentParam<string> = new ComponentParam('')

  build() {
    Button('取消弹窗').onClick(() => {
      this.param.dismissAndShowNext()
    })
  }
}

实际项目用法

1:新建一个普通类比如LoginDialog,继承BaseDialog或BaseOverlay或BaseSheet

2:创建一个静态方法对外提供,哪个页面需要显示弹窗,调用静态方法即可,页面不用关心弹窗的创建逻辑以及弹窗内部交互逻辑

3:@Builder装饰的function方法定义在LoginDialog文件里面

4:遇到复杂的业务场景,部分业务逻辑在LoginDialog类中单独闭环与外部页面完全解耦

页面

text 复制代码
import { LoginDialog, LoginResult } from '../test/LoginDialog'

@Entry
@Component
struct Page {
  build() {
    Column() {
      Button('显示登录弹窗').onClick(() => {
        LoginDialog.showDialog(this.getUIContext(), '156xxx8888', (result: LoginResult) => {
          //获取登录结果
        })
      })
    }
    .height('100%')
    .width('100%')
  }
}

LoginDialog类

text 复制代码
import { BaseDialog, ComponentParam } from '@zhongrui/easy_dialog';
import { LoginDialogView } from './LoginDialogView';

export interface LoginParam {
  phone: string
}

export interface LoginResult {
  phone: string
  userName: string
  pwd: string
}

@Builder
function dialogView(param: ComponentParam<LoginParam>) {
  LoginDialogView({ param: param })
}

export class LoginDialog extends BaseDialog {
  public static showDialog(uiContext: UIContext, phone: string,
    loginSuccess: (data: LoginResult) => void): LoginDialog {
    //构造业务参数
    const data: ComponentParam<LoginParam> = new ComponentParam<LoginParam>({ phone: phone })
    //创建弹窗
    const dialog = new LoginDialog(uiContext)
    //设置视图
    dialog.setContentView(wrapBuilder(dialogView), data)
    //设置弹窗参数promptAction.BaseDialogOptions(和官网文档一致)
    //maskRect,alignment,offset,isModal,showInSubWindow,onWillDismiss,autoCancel
    //maskColor,transition,dialogTransition等
    dialog.setOption({})
    //设置tag
    dialog.addTag('key', '自定义参数')
    //注册登录成功事件
    dialog.addEvent<LoginResult>('success', (data: LoginResult | undefined) => {
      if (!data) {
        return
      }
      loginSuccess?.(data)
    })
    //显示弹窗
    dialog.show()
    return dialog
  }

  //可以定义其他方法根据实际场景处理一些额外的业务逻辑
  public otherMethod() {
    this.addTag('key', 'value')
  }
}

LoginDialog视图

text 复制代码
import { ComponentParam } from '@zhongrui/easy_dialog'
import { LoginParam, LoginResult } from './LoginDialog'

@ComponentV2
export struct LoginDialogView {
  @Param param: ComponentParam<LoginParam> = new ComponentParam<LoginParam>({ phone: '' })
  @Local phone: string = ''

  aboutToAppear(): void {
    this.phone = this.param.data.phone
    //可以获取其他参数
    this.param.getTag('key')
  }

  build() {
    Column() {
      Text("手机号:" + this.phone)
      Button('登录').onClick(() => {
        //登录成功
        const result: LoginResult = {
          phone: this.phone,
          userName: 'Harmony',
          pwd: 'OpenHarmony'
        }
        //发送登录成功事件
        this.param.sendEvent('success', result)
        //隐藏弹窗
        this.param.dismiss()
      })
    }
  }
}

进阶用法

公共部分

typescript 复制代码
//除了构造函数能设置业务参数,也可以通过setData设置,支持各种类型
const data: ComponentParam<string> = new ComponentParam('自定义参数')
data.setData('xxx')

//Dialog(自定义弹窗)
let dialog: AbsDialog = new BaseDialog(this.getUIContext())
//Overlay(浮层)
dialog = new BaseOverlay(this.getUIContext())
//bindSheet(半模态弹窗)
dialog = new BaseSheet(this.getUIContext(), 'contentId')

//Dialog(自定义弹窗)设置绑定的组件id
//虽然不设置也可以,但是后续Navigation跳转的页面都会被弹窗覆盖,根据实际场景调用即可
dialog.setComponentId('contentId')

//(和官方文档一致)设置实例化ComponentContent的参数,所以setBuildOption必须在setContentView之前调用
const buildOption: BuildOptions = {}
dialog.setBuildOption(buildOption)

//设置弹窗视图
dialog.setContentView(wrapBuilder(dialogView), data)

//注册事件,接收视图发送的事件,获取事件参数的同时,支持设置返回值
dialog.addEvent<number>('key', (result: number | undefined) => {
  return 123
})
dialog.addEvent<string>('key', (result: string | undefined) => {
})
//删除单个事件
dialog.deleteEvent('key')

//给组件视图发送事件(不是dialog.addEvent注册的事件)并获取事件的返回值
const result1 = dialog.sendEvent('key')
const result2 = dialog.sendEvent('key', '自定义类型参数')

//清除Dialog注册的所有事件
dialog.clearDialogRegisterEvent()

//清除视图注册的所有事件
dialog.clearComponentRegisterEvent()

//清除Dialog注册的事件+当前组件注册的事件
dialog.clearAllEvent()

//设置tag,在dialog和组件视图中都可以通过getTag可以获取
dialog.setTag('xxx')

//添加tag,支持各种类型,在dialog和组件视图中都可以通过getTag(key)可以获取
dialog.addTag('key', 'value')

//获取tag,不传key获取setTag('xxx')的值,传key获取addTag('key', 'value')的值
//在dialog和组件视图中都可以获取
dialog.getTag('key')

//删除tag
dialog.deleteTag('key')

//清除tag,true:清除所有tag,false:只清除addTag('key', 'value')的值,不清除setTag('xxx')的值
dialog.clearTag(true)

//显示弹窗
dialog.show()

//隐藏弹窗,下次直接调用show方法即可再次显示
dialog.hide()

//销毁弹窗,下次显示必须重新实例化
dialog.dismiss()

//隐藏弹窗/浮层/sheet,显示下一个(用于有序弹窗/浮层/sheet)
dialog.hideAndShowNext()

//隐藏并销毁弹窗/浮层/sheet,显示下一个(用于有序弹窗/浮层/sheet)
dialog.dismissAndShowNext()

//判断弹窗是否显示
dialog.isShowing()

//判断弹窗是否隐藏
dialog.isHide()

//判断弹窗是否销毁
dialog.isDismiss()

//(和官方文档一致)用于更新WrappedBuilder对象封装的builder函数参数,与constructor传入的参数类型保持一致
const newData: ComponentParam<string> = new ComponentParam('自定义参数')
dialog.update(newData)

//触发ComponentContent中的自定义组件的复用
dialog.reuse()

//触发ComponentContent中自定义组件的回收
dialog.recycle()

//传递系统环境变化事件,触发节点的全量更新
dialog.updateConfiguration()

组件视图

txt 复制代码
import { ComponentParam } from '@zhongrui/easy_dialog'

@ComponentV2
export struct DialogView {
  //固定写法
  @Param param: ComponentParam<string> = new ComponentParam('')
  @Local title: string = ''

  aboutToAppear(): void {
    //获取参数
    this.title = this.param.data
    
    //注册接收Dialog发送的事件
    this.param.addEvent<string>('key', (result: string | undefined) => {
      return 'xxx'
    })
    this.param.addEvent<number>('key', () => {
    })
    
    //删除注册的事件
    this.param.deleteEvent('key')
    
    //给Dialog发送事件并获取返回值
    const result1 = this.param.sendEvent('key')
    const result2 = this.param.sendEvent('key', 'xxx')

    //隐藏弹窗/浮层/sheet
    this.param.hide()
    
    //隐藏并销毁弹窗/浮层/sheet
    this.param.dismiss()
    
    //隐藏弹窗/浮层/sheet,显示下一个(用于有序弹窗/浮层/sheet)
    this.param.hideAndShowNext()
    
    //隐藏并销毁弹窗/浮层/sheet,显示下一个(用于有序弹窗/浮层/sheet)
    this.param.dismissAndShowNext()

    //设置tag,在dialog和组件视图中都可以通过getTag可以获取
    this.param.setTag('xxx')

    //添加tag,支持各种类型,在dialog和组件视图中都可以通过getTag(key)可以获取
    this.param.addTag('key', 'value')

    //获取tag,不传key获取setTag('xxx')的值,传key获取addTag('key', 'value')的值
    //在dialog和组件视图中都可以获取
    this.param.getTag('key')

    //删除tag
    this.param.deleteTag('key')

    //清除tag,true:清除所有tag,false:只清除addTag('key', 'value')的值,不清除setTag('xxx')的值
    this.param.clearTag(true)

    //清除Dialog注册的事件
    this.param.clearDialogRegisterEvent()
    
    //清除当前组件注册的事件
    this.param.clearComponentRegisterEvent()
    
    //清除Dialog注册的事件+当前组件注册的事件
    this.param.clearAllEvent()

  }

  build() {
    Text(this.title)
  }
}

差异部分

Dialog(自定义弹窗)

typescript 复制代码
const data: ComponentParam<string> = new ComponentParam('自定义参数')
data.setData('xxx')

const dialog = new BaseDialog(this.getUIContext())
dialog.setContentView(wrapBuilder(dialogView), data)

//(和官方文档一致)设置弹窗参数
const options: promptAction.BaseDialogOptions = {}
dialog.setOption(options)

//(和官方文档一致)在弹窗显示的情况下可以更新弹窗参数
const dialogOption: promptAction.BaseDialogOptions = {}
dialog.updateDialog(dialogOption)

dialog.show()

Overlay(浮层)

typescript 复制代码
const data: ComponentParam<string> = new ComponentParam('自定义参数')
data.setData('xxx')

const dialog = new BaseOverlay(this.getUIContext())
dialog.setContentView(wrapBuilder(dialogView), data)

//(和官方文档一致)设置OverlayManager参数
const options: OverlayManagerOptions = {}
dialog.setOverlayManagerOption(options)

//新增视图节点在OverlayManager上的层级位置,当index ≥ 0时,越大
dialog.setIndex(1)

dialog.show()

bindSheet(半模态弹窗)

typescript 复制代码
const data: ComponentParam<string> = new ComponentParam('自定义参数')
data.setData('xxx')

const dialog = new BaseSheet(this.getUIContext(), 'contentId')
dialog.setContentView(wrapBuilder(dialogView), data)

//(和官方文档一致)设置半模态参数
const options: SheetOptions = {}
dialog.setSheetOption(options)

dialog.show()

历史文章

HarmonyOS NEXT多环境+多渠道+自定义路径输出+自定义名称一键打app和hap包

HarmonyOS NEXT一行代码实现任意处弹窗

HarmonyOS NEXT数据列表加载更多(无需监听列表滑到最底部)

HarmonyOS NEXT下拉刷新+上拉加载(纵向横向都支持)(v1+v2装饰器)

HarmonyOS NEXT图片压缩(支持fd,uri,网络图片,沙箱路径,base64,ArrayBuffer)

相关推荐
好奇龙猫2 小时前
【人工智能学习-AI-MIT公开课13.- 学习:遗传算法】
android·人工智能·学习
TO_ZRG2 小时前
Unity打包安卓、iOS知识点
android·unity·android studio
周杰伦fans2 小时前
AndroidStudioJava国内镜像地址gradle
android·java·android-studio
艾莉丝努力练剑2 小时前
【Linux进程控制(一)】进程创建是呼吸,进程终止是死亡,进程等待是重生:进程控制三部曲
android·java·linux·运维·服务器·人工智能·安全
2501_924064112 小时前
2026年移动应用渗透测试流程方案及iOS与Android框架对比
android·ios
用户69371750013842 小时前
谷歌官方推荐:Android 性能优化全攻略——从工具到实战,两周提升 App 评分
android·android studio·android jetpack
顾林海2 小时前
Android Profiler实战宝典:揪出CPU耗时元凶与内存泄露小偷
android·面试·性能优化
城东米粉儿2 小时前
Android刷新与绘制机制详解 笔记
android
李艺为2 小时前
Android 16安兔兔分辨率作假显示(非修改TextView方案)
android
裴云飞2 小时前
Compose原理二之GapBuffer
android·架构