HarmonyOS 自定义弹窗一定得在组件中去实现并展示么?

由于app设计风格的统一性,我们要求所有的弹窗都基于自定义的弹窗,来做展示。

但我们发现,有些弹窗是没有具体对应的场景,例如登录接口的报错。我们有好几个登录的场景,同时还有一套自动重连的机制,登录的报错,之前我们直接在非组件里面用promptAction.showDialog()系统弹窗去弹了,但如果需要改成自定义弹窗,这块就很难处理了。是否有比较好的办法解决。

这类场景在其他平台中,直接就可以弹,自定义弹窗跟系统弹窗不会有逻辑上的限制。

HarmonyOS
2024-10-21 10:14:53
浏览
收藏 0
回答 1
待解决
回答 1
按赞同
/
按时间
FengTianYa

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

定义路由工具类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();
分享
微博
QQ
微信
回复
2024-10-21 15:22:57
相关问题
使用自定义弹窗实现分享弹窗
522浏览 • 1回复 待解决
自定义弹窗自定义转场动画
1064浏览 • 1回复 待解决
HarmonyOS 自定义弹窗选择
311浏览 • 1回复 待解决
如何在全局实现自定义dialog弹窗
2768浏览 • 1回复 待解决
HarmonyOS 自定义弹窗的问题
581浏览 • 1回复 待解决
HarmonyOS 自定义弹窗CustomDialog问题
532浏览 • 1回复 待解决
HarmonyOS 全局自定义弹窗demo
244浏览 • 1回复 待解决
HarmonyOS 自定义弹窗 (CustomDialog)问题
304浏览 • 1回复 待解决
HarmonyOS CoverFlow效果自定义组件实现
193浏览 • 1回复 待解决
HarmonyOS 自定义弹窗遮罩未全屏
549浏览 • 1回复 待解决
如何在自定义弹窗中再次弹窗
2200浏览 • 1回复 待解决
如何快速开发出自定义弹窗
321浏览 • 1回复 待解决