HarmonyOS鸿蒙Next中如何实现在非UI组件里弹出弹窗

HarmonyOS鸿蒙Next中如何实现在非UI组件里弹出弹窗 请问如何在非UI组件里弹出弹窗?

现在的设计是CustomDialogController里需要定义在组件里,而有些场景是在非UI的逻辑里进行了一系列的判断后再决定是否弹出弹窗,比如拦截特定的接口错误并弹出弹窗,此时想要弹窗显示在当前展示的页面上(页面不固定),目前没想到比较简易的实现方法,期望像Android的Dialog一样,拿到当前页面的上下文就可以在当前页面上弹出弹窗

4 回复

步骤一:封装路由工具类,并注册自定义弹窗组件

定义路由工具类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();

更多关于HarmonyOS鸿蒙Next中如何实现在非UI组件里弹出弹窗的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


要么自己封装个全局弹窗,要么借用三方库来实现全局弹窗

在HarmonyOS鸿蒙Next中,若要在非UI组件中弹出弹窗,可以使用AbilityContext提供的showDialog方法。AbilityContext是Ability的生命周期上下文,通过它可以调用系统弹窗功能。具体步骤如下:

  1. 获取AbilityContext:在Ability中,可以直接使用this.context来获取AbilityContext

  2. 创建弹窗内容:使用CommonDialog或自定义Component作为弹窗内容。

  3. 调用showDialog方法:通过AbilityContextshowDialog方法显示弹窗。

示例代码如下:

import common from '@ohos.app.ability.common';

let context = this.context as common.UIAbilityContext; // 获取AbilityContext
let dialogController = new common.CommonDialogController(); // 创建弹窗控制器

let dialogComponent = { // 定义弹窗内容
  builder: (context) => {
    return <Text>这是一个弹窗</Text>;
  }
};

dialogController.showDialog(dialogComponent); // 显示弹窗

在非UI组件中,可以通过依赖注入或其他方式获取AbilityContext,然后按照上述步骤弹出弹窗。注意,弹窗的显示依赖于UI线程,确保在UI线程中调用showDialog方法。

在HarmonyOS鸿蒙Next中,虽然非UI组件不能直接操作UI,但可以通过事件机制实现弹窗。具体步骤如下:

  1. 定义事件:在非UI组件中定义一个事件,用于触发弹窗操作。
  2. 发布事件:在需要弹窗的地方,发布该事件。
  3. 订阅事件:在UI组件中订阅该事件,并在事件回调中调用弹窗API(如Dialog.show())来显示弹窗。

这种方式实现了非UI组件与UI组件的解耦,确保弹窗操作在UI线程中执行。

回到顶部