赞
踩
场景描述
自定义弹窗是应用开发需要实现的基础功能,包括但不限于HarmonyOS开发者文档中定义的模态、半模态、Toast等形式,封装一个好用且和UI组件解耦的弹窗组件是开发者的高频诉求
自定义弹窗通常的使用场景有:
场景一:在公共逻辑中触发弹窗
登录提示弹窗、全屏广告弹窗、网络请求与其他操作行为的提示、异常弹窗
场景二:侧滑手势拦截
隐私弹窗的拦截,退出登录时的确认弹窗
场景三:切换页面弹窗不消失
隐私弹窗和二级页面中的半模态弹窗
场景四:自定义弹出、关闭动画
从下往上的抽屉式弹出、关闭时从上往下收回
场景五:透明、模态、半模态背景
应用实现自定义的背景颜色
方案描述
1. 使用Navigation.Dialog
基于Navigation.Dialog的透明页面特性,可以用于实现弹窗效果
而且Navigation.Dialog存在于路由栈中,天然可以实现切换页面弹窗不消失
当前限制:
弹窗组件中的动效建议开发者自行实现
Navigation.Dialog自身无颜色,需要开发者自行实现模态遮罩,以及手势事件。
演示效果:
对于少量弹窗的实现,可以直接使用Navigation来进行路由跳转,参考基于Navigation的路由管理 - - 3MS知识管理社区 (huawei.com)
其他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)
进阶用法:可以参考动态路由案例实现动态路由,HarmonyOS NEXT应用开发案例集 - Gitee.com
步骤二:封装弹窗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();
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。