场景描述
自定义弹窗是应用开发需要实现的基础功能,包括但不限于HarmonyOS开发者文档中定义的模态、半模态、Toast等形式,封装一个好用且和UI组件解耦的弹窗组件是开发者的高频诉求
自定义弹窗通常的使用场景有:
场景一:在公共逻辑中触发弹窗
登录提示弹窗、全屏广告弹窗、网络请求与其他操作行为的提示、异常弹窗
场景二:侧滑手势拦截
隐私弹窗的拦截,退出登录时的确认弹窗
场景三:切换页面弹窗不消失
隐私弹窗和二级页面中的半模态弹窗
场景四:自定义弹出、关闭动画
从下往上的抽屉式弹出、关闭时从上往下收回
场景五:透明、模态、半模态背景
应用实现自定义的背景颜色
方案描述
1. 使用Navigation.Dialog
基于Navigation.Dialog的透明页面特性,可以用于实现弹窗效果
而且Navigation.Dialog存在于路由栈中,天然可以实现切换页面弹窗不消失
当前限制:
弹窗组件中的动效建议开发者自行实现
Navigation.Dialog自身无颜色,需要开发者自行实现模态遮罩,以及手势事件。
演示效果:
对于少量弹窗的实现,可以直接使用Navigation来进行路由跳转,参考 Navigation常见场景及解决方案
其他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();
鸿蒙全栈开发全新学习指南
有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以要有一份实用的鸿蒙(HarmonyOS NEXT)学习路线与学习文档用来跟着学习是非常有必要的。
针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。
本路线共分为四个阶段:
第一阶段:鸿蒙初中级开发必备技能
第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH
第三阶段:应用开发中高级就业技术
第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH
《鸿蒙 (Harmony OS)开发学习手册》(共计892页)
如何快速入门?
1.基本概念
2.构建第一个ArkTS应用
3.……
开发基础知识:gitee.com/MNxiaona/733GH
1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……
基于ArkTS 开发
1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……
鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH
鸿蒙入门教学视频:
美团APP实战开发教学:gitee.com/MNxiaona/733GH
写在最后
- 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
- 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
- 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
- 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:
gitee.com/MNxiaona/733GH