HarmonyOS 全局弹窗实现与页面解耦

📑往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)

✒️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

✒️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

✒️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

✒️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

✒️ 记录一场鸿蒙开发岗位面试经历~

✒️ 持续更新中……


概述

弹窗是应用开发需要实现的基础功能,通常用来展示用户当前需要或用户必须关注的信息或操作,可用于广告、中奖、警告、软件更新等与用户交互响应的操作。在应用开发中,经常需要实现自定义UI和功能要求,系统弹窗往往无法满足需求,此时就需要使用到自定义弹窗。

自定义弹窗选型

合理选择不同的系统能力实现弹窗,有利于提升应用开发效率,实现更好的功能需求,因此了解自定义弹窗的选型和差异非常重要。目前在HarmonyOS中实现自定义弹窗有以下方式:

  • @CustomDialog弹窗
  • PromptAction弹窗
  • UIContext.getPromptAction弹窗
  • bindsheet
  • UIContext.OverlayManager
  • Navigation.dialog
  • window.createWindow/createWindowWithOption

不同弹窗在使用上存在其局限性,详情可以参考如下表格中的能力支持情况:

表1 弹窗能力选型

场景描述ArkUI自定义弹窗Navigation应用子窗口
@CustomDialogpromptActionbindsheetUIContext PromptAction
页面解耦×××
路由解耦
事件分发到页面×××
切换页面弹窗不消失××
弹窗侧滑拦截/响应
弹窗蒙层
弹窗样式自定义(背景、圆角等)
自定义弹窗显示和退出动画×
键盘避让模式选择×

下面根据常见开发场景,选用对应弹窗的实现方案。

常见场景问题解决方案

了解了自定义弹窗的对比和使用区别后,接下来,我们通过场景下几个常见的问题,来介绍如何选择合适的自定义弹窗。

  • 与页面解耦的全局弹窗
  • 拦截物理返回按钮、手势滑动关闭弹窗
  • 切换页面弹窗不消失
  • 自定义弹窗显示和退出动画

与页面解耦的全局弹窗

在应用开发的过程中,开发者要实现在不关联页面的情况下进行弹窗,此时需要对弹窗进行封装。

典型场景

  • 登录提示弹窗
  • 全局的广告弹窗
  • 网络请求与其他操作行为的提示弹窗
  • 请求或操作异常时的警告弹窗

实现思路

可以使用UIContext.getPromptAction.openCustomDialog的方式,创建并弹出对应的自定义弹窗,支持弹窗和页面解耦。

示例代码

  1. 封装自定义弹窗类PromptActionClass,自定义弹窗的打开方法openDialog()、关闭方法closeDialog()。
import { BusinessError } from '@kit.BasicServicesKit';
import { ComponentContent, promptAction } from '@kit.ArkUI';
import { UIContext } from '@ohos.arkui.UIContext';
import { hilog } from '@kit.PerformanceAnalysisKit';

export class PromptActionClass {
  static ctx: UIContext;
  static contentNode: ComponentContent<Object>;
  static options: promptAction.BaseDialogOptions;

  static setContext(context: UIContext) {
    PromptActionClass.ctx = context;
  }

  static setContentNode(node: ComponentContent<Object>) {
    PromptActionClass.contentNode = node;
  }

  static setOptions(options: promptAction.BaseDialogOptions) {
    PromptActionClass.options = options;
  }

  static openDialog() {
    if (PromptActionClass.contentNode !== null) {
      PromptActionClass.ctx.getPromptAction()
        .openCustomDialog(PromptActionClass.contentNode, PromptActionClass.options)
        .then(() => {
          hilog.info(0x0000, 'testTag', 'OpenCustomDialog complete.');
        })
        .catch((error: BusinessError) => {
          let message = (error as BusinessError).message;
          let code = (error as BusinessError).code;
          hilog.error(0x0000, 'testTag', `OpenCustomDialog args error code is ${code}, message is ${message}`);
        })
    }
  }

  static closeDialog() {
    if (PromptActionClass.contentNode !== null) {
      PromptActionClass.ctx.getPromptAction()
        .closeCustomDialog(PromptActionClass.contentNode)
        .then(() => {
          hilog.info(0x0000, 'testTag', 'CloseCustomDialog complete.');
        })
        .catch((error: BusinessError) => {
          let message = (error as BusinessError).message;
          let code = (error as BusinessError).code;
          hilog.error(0x0000, 'testTag', `CloseCustomDialog args error code is ${code}, message is ${message}`);
        })
    }
  }
}
  1. 在需要使用弹窗的页面中引入弹窗组件,调用openDialog()和closeDialog()自定义方法,实现打开和关闭弹窗的功能。
import { PromptActionClass } from '../uitls/PromptActionClass';
import { ComponentContent } from '@kit.ArkUI';

class Params {
  text: string = '';

  constructor(text: string) {
    this.text = text;
  }
}

@Builder
function buildText(params: Params) {
  Column() {
    Row() {
      Text('Title')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .height(56)
    .justifyContent(FlexAlign.Center)

    Text(params.text)
      .fontSize(14)

    Button('CONFIRM')
      .fontSize(16)
      .fontColor('#0A59F7')
      .backgroundColor(Color.White)
      .onClick(() => {
        PromptActionClass.closeDialog(); // Close customDialog.
      })
      .width('100%')
      .margin({
        top: 8,
        bottom: 16
      })
  }
  .padding({
    left: 24,
    right: 24
  })
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
  .backgroundColor(Color.White)
  .borderRadius(32)
  .margin({ left: 16, right: 16 })
}

@Component
export struct GlobalDialogDecoupledFromThePage {
  @State message: string = 'This is a dialog content.';
  private ctx: UIContext = this.getUIContext();
  private contentNode: ComponentContent<Object> =
    new ComponentContent(this.ctx, wrapBuilder(buildText), new Params(this.message));

  aboutToAppear(): void {
    PromptActionClass.setContext(this.ctx);
    PromptActionClass.setContentNode(this.contentNode);
    PromptActionClass.setOptions({ alignment: DialogAlignment.Center, maskColor: 'rgba(0, 0, 0, 0.2)' });
  }

  aboutToDisappear(): void {
    if (PromptActionClass.contentNode !== null) {
      PromptActionClass.contentNode.dispose(); // release contentNode.
    }
  }

  build() {
    NavDestination() {
      Column() {
        Row() {
          Button('OPEN')
            .fontSize(16)
            .width('100%')
            .borderRadius(20)
            .margin({ bottom: 16 })
            .backgroundColor('#0A59F7')
            .onClick(() => {
              PromptActionClass.openDialog(); // Open customDialog.
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .height('100%')
      .padding({
        left: 16,
        right: 16
      })
      .justifyContent(FlexAlign.End)
    }
  }
}

效果演示

拦截物理返回按钮、手势滑动关闭弹窗

用户只能通过按钮关闭弹窗,不允许使用物理返回按钮、手势滑动关闭弹窗。

典型场景

  • 密码输入框,输入密码之前不允许关闭弹窗
  • 展示隐私协议弹窗时,用户必须点击同意才能继续使用应用

实现思路

方式一 基于UIContext.getPromptAction弹窗,使用 弹窗的选项 对象中的onWillDismiss交互式关闭回调函数,支持物理拦截返回。当用户执行点击遮障层关闭、左滑/右滑、三键back、键盘ESC关闭交互操作时,如果注册该回调函数,则不会立刻关闭弹窗。在回调函数中可以通过 DismissReason )得到关闭弹窗的操作类型,从而根据原因选择是否能关闭弹窗。

示例代码

import { PromptAction } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Component
export struct InterceptReturn01 {
  private context: UIContext = this.getUIContext();
  private promptAction: PromptAction = this.getUIContext().getPromptAction();
  private customDialogComponentId: number = 0;
  @Consume('NavPathStack') pageStack: NavPathStack;

  @Builder
  customDialogComponent() {
    Column() {
      Row() {
        Text('Title')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .height(56)
      .justifyContent(FlexAlign.Center)

      Text('This is a dialog content.')
        .fontSize(14)

      Button('CONFIRM')
        .fontSize(16)
        .fontColor('#0A59F7')
        .backgroundColor(Color.White)
        .onClick(() => {
          this.context.getPromptAction().closeCustomDialog(this.customDialogComponentId);
        })
        .width('100%')
        .margin({
          top: 8,
          bottom: 16
        })
    }
    .padding({
      left: 24,
      right: 24
    })
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor(Color.White)
    .borderRadius(32)
    .margin({
      left: 16,
      right: 16
    })
  }

  build() {
    NavDestination() {
      Column() {
        Row() {
          Button('OPEN')
            .fontSize(16)
            .width('100%')
            .borderRadius(20)
            .margin({ bottom: 16 })
            .backgroundColor('#0A59F7')
            .onClick(() => {
              this.promptAction.openCustomDialog({
                builder: () => {
                  this.customDialogComponent()
                },
                alignment: DialogAlignment.Center,
                maskColor: 'rgba(0, 0, 0, 0.2)',
                onWillDismiss: (dismissDialogAction: DismissDialogAction) => {
                  hilog.info(0xFF00, 'testTag', JSON.stringify(dismissDialogAction.reason));
                }
              }).then((dialogId: number) => {
                this.customDialogComponentId = dialogId
              })
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .height('100%')
      .padding({
        left: 16,
        right: 16
      })
      .justifyContent(FlexAlign.End)
    }
  }
}

效果演示

方式二 可以基于Navigation自定义弹窗实现,使用NavDestination的回调函数onBackPressed,当与Navigation绑定的页面栈中存在内容时,此回调生效。当点击物理返回按钮或使用手势滑动时,触发该回调。返回值为true时,表示重写返回键逻辑,即可实现拦截。

示例代码

@Component
export struct InterceptReturn02 {
  @Consume('NavPathStack') pageStack: NavPathStack;

  build() {
    NavDestination() {
      Column() {
        Row() {
          Button('OPEN')
            .fontSize(16)
            .width('100%')
            .borderRadius(20)
            .margin({ bottom: 16 })
            .backgroundColor('#0A59F7')
            .onClick(() => {
              this.pageStack.pushPathByName('DialogPage1', '');
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .height('100%')
      .padding({
        left: 16,
        right: 16
      })
      .justifyContent(FlexAlign.End)
    }
  }
}

@Component
export struct DialogPage1 {
  @Consume('NavPathStack') pageStack: NavPathStack;

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.Center }) {
        Column() {
          Row() {
            Text('Title')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .height(56)
          .justifyContent(FlexAlign.Center)

          Text('This is a dialog content.')
            .fontSize(14)

          Button('CONFIRM')
            .fontSize(16)
            .fontColor('#0A59F7')
            .backgroundColor(Color.White)
            .onClick(() => {
              this.pageStack.pop();
            })
            .width('100%')
            .margin({
              top: 8,
              bottom: 16
            })
        }
        .padding({
          left: 24,
          right: 24
        })
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .backgroundColor(Color.White)
        .borderRadius(32)
        .margin({
          left: 16,
          right: 16
        })
      }
      .height('100%')
      .width('100%')
    }
    .backgroundColor('rgba(0, 0, 0, 0.2)')
    .hideTitleBar(true)
    .mode(NavDestinationMode.DIALOG)
    .onBackPressed((): boolean => {
      return true;
    })
    .borderColor(Color.White)
  }
}

效果演示

切换页面弹窗不消失

点击弹窗中的按钮或链接打开新页面,返回后自定义弹窗还在原页面上展示。

典型场景

用户首次进入应用需要进行权限配置,弹出弹窗后,点击跳转到隐私详情页面,返回后弹窗还在显示。

实现思路

NavDestinationMode.DIALOG弹窗存在于路由栈中,可以实现切换页面弹窗不消失。

示例代码

@Component
export struct CustomDialogNotDisappear {
  @Consume('NavPathStack') pageStack: NavPathStack;

  build() {
    NavDestination() {
      Column() {
        Row() {
          Button('OPEN')
            .fontSize(16)
            .width('100%')
            .borderRadius(20)
            .margin({ bottom: 16 })
            .backgroundColor('#0A59F7')
            .onClick(() => {
              this.pageStack.pushPathByName('DialogPage', '');
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .height('100%')
      .padding({
        left: 16,
        right: 16
      })
      .justifyContent(FlexAlign.End)
    }
  }
}

@Component
export struct DialogPage {
  @Consume('NavPathStack') pageStack: NavPathStack;

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.Center }) {
        Column() {
          Row() {
            Text('Title')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .height(56)
          .justifyContent(FlexAlign.Center)

          Text('This is a dialog content.')
            .fontSize(14)

          Button('CONFIRM')
            .fontSize(16)
            .fontColor('#0A59F7')
            .backgroundColor(Color.White)
            .onClick(() => {
              this.pageStack.pushPathByName('PageOne', 'PageOne Param');
            })
            .width('100%')
            .margin({
              top: 8,
              bottom: 16
            })
        }
        .padding({
          left: 24,
          right: 24
        })
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .backgroundColor(Color.White)
        .borderRadius(32)
        .margin({
          left: 16,
          right: 16
        })
      }
      .height('100%')
      .width('100%')
    }
    .backgroundColor('rgba(0,0,0,0.2)')
    .hideTitleBar(true)
    .mode(NavDestinationMode.DIALOG)
  }
}

@Component
export struct PageOne {
  @Consume('NavPathStack') pageStack: NavPathStack;

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.Center }) {
        Column() {
          Row() {
            Image($r('app.media.Back'))
              .width(40)
              .height(40)
              .margin({ right: 8 })
              .onClick(() => {
                this.pageStack.pop();
              })

            Text('Back')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .height(56)
          .justifyContent(FlexAlign.Start)
        }
        .backgroundColor('#F1F3F5')
        .padding({
          left: 16,
          right: 16
        })
        .height('100%')
        .width('100%')
      }
      .height('100%')
      .width('100%')
    }
    .backgroundColor('#F1F3F5')
    .hideTitleBar(true)
  }
}

效果演示

自定义弹窗显示和退出动画

典型场景

在应用开发中,系统弹窗的显示和退出动画往往不满足需求,若要实现自定义弹窗出入动画,可以使用以下方式,例如:1)渐隐渐显的方式弹出,2)从左往右弹出,从右往左收回,3)从下往上的抽屉式弹出、关闭时从上往下收回。我们以渐隐渐显的方式为例,来介绍自定义弹窗的显示和退出动画。

实现思路

可以基于UIContext.getPromptAction弹窗实现,通过CustomDialogOptions自定义弹窗的内容,BaseDialogOptions弹窗选项transition参数可以设置弹窗显示和退出的过渡效果。

示例代码

import { PromptAction } from '@kit.ArkUI';

@Component
export struct CustomDialogDisplayAndExitAnimations {
  private promptAction: PromptAction = this.getUIContext().getPromptAction();
  private customDialogComponentId: number = 0;
  @Consume('NavPathStack') pageStack: NavPathStack;

  @Builder
  customDialogComponent() {
    Column() {
      Row() {
        Text('Title')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .height(56)
      .justifyContent(FlexAlign.Center)

      Text('This is a custom dialog box with different entrance and exit animations.')
        .fontSize(14)
        .width('100%')

      Button('CONFIRM')
        .fontSize(16)
        .fontColor('#0A59F7')
        .backgroundColor(Color.White)
        .onClick(() => {
          this.promptAction.closeCustomDialog(this.customDialogComponentId);
        })
        .width('100%')
        .margin({
          top: 8,
          bottom: 16
        })
    }
    .padding({
      left: 24,
      right: 24
    })
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor(Color.White)
    .borderRadius(32)
  }

  build() {
    NavDestination() {
      Column() {
        Row() {
          Button('OPEN')
            .fontSize(16)
            .width('100%')
            .borderRadius(20)
            .margin({ bottom: 16 })
            .backgroundColor('#0A59F7')
            .onClick(() => {
              this.promptAction.openCustomDialog({
                builder: () => {
                  this.customDialogComponent();
                },
                alignment: DialogAlignment.Center,
                maskColor: 'rgba(0, 0, 0, 0.2)',
                // Set two animations, corresponding to the pop-up window display and hidden animation respectively.
                transition: TransitionEffect.asymmetric(
                  TransitionEffect.OPACITY
                    .animation({ duration: 1000 })
                  ,
                  TransitionEffect.OPACITY
                    .animation({ delay: 500, duration: 1000 })
                )
              }).then((dialogId: number) => {
                this.customDialogComponentId = dialogId;
              })
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .height('100%')
      .padding({
        left: 16,
        right: 16
      })
      .justifyContent(FlexAlign.End)
    }
  }
}

效果演示

总结

本文从自定义弹窗选型对比、使用场景的角度,主要介绍了以下弹窗的使用区别:

  • UIContext.getPromptAction弹窗:适合全局自定义弹窗,不依赖UI组件的场景
  • Navigation.Dialog弹窗:适合Navigation路由形态、透明页面、切换页面弹窗不消失的场景

同时对于开发者在弹窗使用中经常遇到的问题,给出详细的解决方案,帮助开发者快速选择自定义弹窗的实现方式,提升开发效率。

<think>好的,我现在需要解决用户的问题:如何在鸿蒙系统中将自定义弹窗封装为全局可用的组件。首先,我得回忆一下鸿蒙开发的相关知识,特别是ArkUI框架。用户提到了引用中的几个资料,里面提到了封装弹窗UI组件、动态路由案例,以及如何通过动画和遮罩实现弹窗关闭。我需要结合这些信息来构建一个全局可用的弹窗组件。 首先,封装组件通常需要创建一个可复用的自定义组件。全局调用的话,可能需要将这个组件挂载到全局上下文或者使用单例模式,这样在任何页面都可以调用。根据引用[3],弹窗的结构包括模态遮罩和内容部分,并且通过动画控制显示和隐藏。另外,引用[1]提到步骤二封装弹窗UI组件,可能需要将弹窗的结构独立出来,并暴露接口给外部调用。 接下来,考虑如何实现全局调用。可能需要一个全局的管理类,负责实例化弹窗组件,并在需要显示的时候将其添加到当前的页面中。例如,在引用[2]中,开发者需要解耦弹窗和UI组件,所以可能需要一个中间层来管理弹窗的显示和隐藏。 然后,代码结构方面,应该定义一个CustomDialog类,继承自某个基础组件,比如@Component。内部结构包括Stack布局,包含遮罩层和内容区域。遮罩层的点击事件会触发关闭动画,这部分在引用[3]中有示例代码,使用animateTo来修改透明度和位置。 另外,全局调用可能需要将弹窗挂载到应用的生命周期中,或者使用Ability的上下文。例如,通过一个静态方法,在调用时获取当前页面的窗口,并将弹窗添加到窗口的根布局中。引用[1]中的动态路由案例可能涉及如何动态添加组件到页面,这可以借鉴到弹窗全局调用中。 最后,还需要考虑样式的自定义,比如位置、动画参数等,可以通过参数传入。例如,弹窗的位置可能在底部、中间或其他位置,根据不同的样式调整translateY等属性。引用[3]中的代码通过判断Alignment.Bottom来调整位置,这可以作为样式的配置项。 总结步骤:1. 创建自定义弹窗组件,包含遮罩和内容区域,支持动画。2. 封装全局管理类,负责实例化和显示弹窗。3. 在需要的地方调用全局方法,传入参数显示弹窗。4. 处理弹窗的关闭逻辑和动画效果。同时,需要注意解耦,使得弹窗组件不依赖于具体页面,方便复用。</think>### HarmonyOS自定义弹窗全局封装实现方法 在鸿蒙系统中,通过ArkUI组件化能力封装全局弹窗需遵循以下步骤,实现效果代码示例如下: #### 一、基础弹窗组件封装 ```typescript @Component export struct CustomDialog { @State opacityNum: number = 0 @State translateY: string = '100%' private dialogOptions?: DialogOptions build() { Stack() { // 模态遮罩层 Column() .width('100%') .height('100%') .backgroundColor('#66000000') .opacity(this.opacityNum) .onClick(() => this.closeAnimation()) // 弹窗内容容器 Column() { // 自定义内容插槽 if (this.dialogOptions?.builder) { this.dialogOptions.builder() } } .width('100%') .backgroundColor(Color.White) .translate({ y: this.translateY }) } } // 关闭动画逻辑 private closeAnimation() { animateTo({ duration: 200, curve: Curve.Friction, onFinish: () => { this.dialogOptions?.instance?.close() } }, () => { this.opacityNum = 0 this.translateY = "100%" }) } } ``` #### 二、全局服务层封装 ```typescript export class DialogService { private static instance: DialogService static getInstance() { if (!DialogService.instance) { DialogService.instance = new DialogService() } return DialogService.instance } show(options: DialogOptions) { const node = new CustomDialogNode(options) // 获取当前窗口根节点并挂载 getWindow(this.context).rootView.addChild(node) } } class CustomDialogNode extends CustomDialog { constructor(options: DialogOptions) { super() this.dialogOptions = options this.openAnimation() } private openAnimation() { animateTo({ duration: 300 }, () => { this.opacityNum = 1 this.translateY = '0%' }) } } ``` #### 三、全局调用示例 ```typescript // 在任意页面调用 DialogService.getInstance().show({ styles: { align: Alignment.Bottom }, builder: () => { return Column() { Text('自定义内容').fontSize(20) Button('关闭').onClick(() => DialogService.close()) } } }) ``` #### 关键技术要点: 1. **组件分层架构**:通过分离UI组件服务层,实现业务逻辑视图解耦[^2] 2. **动画同步控制**:使用animateTo实现遮罩透明度内容位移动画同步,提升视觉流畅度[^3] 3. **动态挂载机制**:通过getWindow获取当前窗口上下文,动态挂载弹窗节点[^1] 4. **样式参数化**:支持通过DialogOptions配置弹窗位置、动画参数等样式属性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值