📑往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)
✒️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
✒️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
✒️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
✒️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
✒️ 记录一场鸿蒙开发岗位面试经历~
✒️ 持续更新中……
概述
弹窗是应用开发需要实现的基础功能,通常用来展示用户当前需要或用户必须关注的信息或操作,可用于广告、中奖、警告、软件更新等与用户交互响应的操作。在应用开发中,经常需要实现自定义UI和功能要求,系统弹窗往往无法满足需求,此时就需要使用到自定义弹窗。
自定义弹窗选型
合理选择不同的系统能力实现弹窗,有利于提升应用开发效率,实现更好的功能需求,因此了解自定义弹窗的选型和差异非常重要。目前在HarmonyOS中实现自定义弹窗有以下方式:
- @CustomDialog弹窗
- PromptAction弹窗
- UIContext.getPromptAction弹窗
- bindsheet
- UIContext.OverlayManager
- Navigation.dialog
- window.createWindow/createWindowWithOption
不同弹窗在使用上存在其局限性,详情可以参考如下表格中的能力支持情况:
表1 弹窗能力选型
场景描述 | ArkUI自定义弹窗 | Navigation | 应用子窗口 |
---|---|---|---|
@CustomDialog | promptAction | bindsheet | UIContext PromptAction |
页面解耦 | × | × | × |
路由解耦 | √ | √ | √ |
事件分发到页面 | × | × | × |
切换页面弹窗不消失 | × | × | √ |
弹窗侧滑拦截/响应 | √ | √ | √ |
弹窗蒙层 | √ | √ | √ |
弹窗样式自定义(背景、圆角等) | √ | √ | √ |
自定义弹窗显示和退出动画 | √ | √ | × |
键盘避让模式选择 | √ | √ | × |
下面根据常见开发场景,选用对应弹窗的实现方案。
常见场景问题解决方案
了解了自定义弹窗的对比和使用区别后,接下来,我们通过场景下几个常见的问题,来介绍如何选择合适的自定义弹窗。
- 与页面解耦的全局弹窗
- 拦截物理返回按钮、手势滑动关闭弹窗
- 切换页面弹窗不消失
- 自定义弹窗显示和退出动画
与页面解耦的全局弹窗
在应用开发的过程中,开发者要实现在不关联页面的情况下进行弹窗,此时需要对弹窗进行封装。
典型场景
- 登录提示弹窗
- 全局的广告弹窗
- 网络请求与其他操作行为的提示弹窗
- 请求或操作异常时的警告弹窗
实现思路
可以使用UIContext.getPromptAction.openCustomDialog的方式,创建并弹出对应的自定义弹窗,支持弹窗和页面解耦。
示例代码
- 封装自定义弹窗类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}`);
})
}
}
}
- 在需要使用弹窗的页面中引入弹窗组件,调用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路由形态、透明页面、切换页面弹窗不消失的场景
同时对于开发者在弹窗使用中经常遇到的问题,给出详细的解决方案,帮助开发者快速选择自定义弹窗的实现方式,提升开发效率。