概述
弹窗是应用开发需要实现的基础功能,通常用来展示用户当前需要或用户必须关注的信息或操作,可用于广告、中奖、警告、软件更新等与用户交互响应的操作。在应用开发中,经常需要实现自定义UI和功能要求,系统弹窗往往无法满足需求,此时就需要使用到自定义弹窗了。
自定义弹窗选型
合理选择不同的系统能力实现弹窗,有利于提升应用开发效率,实现更好的功能需求,因此了解自定义弹窗的选型和差异非常重要。在应用开发中,为了选择出合适的弹窗选型,从使用场景上,需要重点关注以下两点:
-
弹窗与界面代码解耦
在开发业务逻辑时,例如遇到一些网络请求失败的场景,需要触发相应的弹窗提醒用户进行操作,由于在任何页面都有可能触发对应的弹窗,此时弹窗不是与某个页面相关联,这个情况下,就需要弹窗与界面的解耦。
-
弹窗在界面跳转后保留
在一些权限配置页,用户首次进入应用时会弹出权限配置弹窗,让用户进行操作,此时若点击跳转到隐私详情页面,返回后弹窗需要保留在页面上。
从能力角度,系统提供了四种不同的方式来实现自定义弹窗,分别是CustomDialog、promptAction、UIContext.getPromptAction、Navigation.Dialog,在开发业务时,需要结合每种弹窗的特点来选择弹窗。
- [CustomDialog]弹窗,必须在@Component struct内部定义,即在UI层创建控制器,当一个页面需要弹出多个自定义弹窗时,就需要创建对应个数的CustomDialogController,这会造成UI层代码冗余,无法做到弹窗与界面的解耦。
- [promptAction]弹窗,为了解决CustomDialog弹窗的问题,支持了UI元素复用机制@Builder,但依赖UI组件。
- [UIContext.getPromptAction]弹窗,基于promptAction弹窗演进而来,支持全局自定义弹窗,不依赖UI组件,依赖UIContext,支持在非页面文件中使用,弹窗内容支持动态修改,支持自定义弹窗圆角半径、大小和位置,适合在[与页面解耦的全局弹窗]、[自定义弹窗显示和退出动画]等场景下使用。
- [Navigation.Dialog]弹窗,基于Navigation路由形式,以Component组件页面存在于路由栈中,以进出栈的方式打开或关闭弹窗,可以实现弹窗与UI界面解耦,默认透明显示,适合在[切换页面弹窗不消失]场景下使用。
上述四种弹窗的使用区别,可以从是否支持弹窗与界面代码解耦、弹窗在界面跳转后是否保留这两个方面去进行对比,总结如下:
CustomDialog | promptAction | UIContext.getPromptAction | Navigation.Dialog | |
---|---|---|---|---|
弹窗与界面代码解耦 | 不支持 | 不支持 | 支持 | 支持 |
弹窗在界面跳转后保留 | 否 | 否 | 否 | 是 |
在对解耦要求不高的情况下,可以使用上面CustomDialog、promptAction两个弹窗,本文围绕开发时主要常见的场景和问题,介绍UIContext.getPromptAction、Navigation.Dialog两个弹窗的使用。
与页面解耦的全局弹窗
在应用开发的过程中,开发者要实现在不关联页面的情况下进行弹窗,此时需要对弹窗进行封装。
典型场景
- 登录提示弹窗
- 全局的广告弹窗
- 网络请求与其他操作行为的提示弹窗
- 请求或操作异常时的警告弹窗
实现思路
可以使用[UIContext.getPromptAction.openCustomDialog]的方式,创建并弹出对应的自定义弹窗,支持弹窗和页面解耦。使用Navigation.Dialog弹窗也可以实现全局自定义弹窗,但需要自己实现弹窗模态效果。
示例代码
- 封装弹窗内容和样式,创建Params类方便开发者进行传参,开发者可以在@Builder里自定义组件的内容。
// DialogComponent.ets
class Params {
text: string = '';
constructor(text: string) {
this.text = text;
}
}
@Builder
export function buildText(params: Params) {
Column() {
Text(params.text)
.fontSize(16)
.fontWeight(FontWeight.Bold)
}
.width(328)
.padding(24)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(Color.White)
.borderRadius(24)
}
- 在需要使用弹窗的页面中引入弹窗组件。
import { buildText } from '../uitls/DialogComponent'
import { BusinessError } from '@kit.BasicServicesKit';
import { ComponentContent, promptAction } from '@kit.ArkUI';
class Params {
text: string = '';
constructor(text: string) {
this.text = text;
}
}
@Entry
@Component
struct GlobalDialogDecoupledFromThePage {
@State message: string = 'hello';
build() {
Row() {
Column() {
Button('click me')
.fontSize(16)
.width(328)
.backgroundColor('#0A59F7')
.onClick(() => {
let uiContext = this.getUIContext();
let promptAction = uiContext.getPromptAction();
let contentNode = new ComponentContent(uiContext, wrapBuilder(buildText), new Params(this.message));
let options: promptAction.BaseDialogOptions = {
alignment: DialogAlignment.Center
};
try {
promptAction.openCustomDialog(contentNode, options);
} catch (error) {
let message = (error as BusinessError).message;
let code = (error as BusinessError).code;
console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`);
}
})
}
.width('100%')
.height('100%')
.margin({ bottom: 40 })
.justifyContent(FlexAlign.End)
}
.height('100%')
}
}
效果演示
拦截物理返回按钮、手势滑动关闭弹窗
用户只能通过按钮关闭弹窗,不允许使用物理返回按钮、手势滑动关闭弹窗。
典型场景
- 密码输入框,输入密码之前不允许关闭弹窗
- 展示隐私协议弹窗时,用户必须点击同意才能继续使用应用
实现思路
方式一 基于UIContext.getPromptAction弹窗,使用[弹窗的选项] 对象中的onWillDismiss交互式关闭回调函数,支持物理拦截返回。当用户执行点击遮障层关闭、左滑/右滑、三键back、键盘ESC关闭交互操作时,如果注册该回调函数,则不会立刻关闭弹窗。在回调函数中可以通过[DismissReason] 得到关闭弹窗的操作类型,从而根据原因选择是否能关闭弹窗。
示例代码
import { BusinessError } from '@kit.BasicServicesKit';
import { ComponentContent } from "@kit.ArkUI";
class Params {
text: string = '';
constructor(text: string) {
this.text = text;
}
}
@Builder
function buildText(params: Params) {
Row() {
Column() {
Text(params.text)
.fontSize(16)
.margin({ top: 24 })
Row() {
Button('Confirm')
.fontSize(16)
.backgroundColor('#0A59F7')
.onClick(() => {
let link1: SubscribedAbstractProperty<number> = storage.link('PropA');
let num = link1.get();
console.info(':::num', link1.get());
link1.set(++num);
})
.width('100%')
.margin({
top: 8,
bottom: 16
})
}
.padding({
left: 16,
right: 16
})
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(24)
.width(328)
}
.height('100%')
}
//自定义组件的内容
let para: Record<string, number> = { 'PropA': 1 };
let storage: LocalStorage = new LocalStorage(para); // 创建新实例并使用给定对象初始化
@Entry(storage)
@Component
struct InterceptReturn01 {
@State message: string = 'Dialog Confirm';
@LocalStorageLink('uiContext') context: UIContext = this.getUIContext();
@LocalStorageLink('PropAA') contentNode: ComponentContent<Params> | null = null;
@LocalStorageLink('PropA') @Watch('onchange') nums: number = 0;
onchange() {
try {
this.context.getPromptAction().closeCustomDialog(this.contentNode);
} catch (error) {
let message = (error as BusinessError).message;
let code = (error as BusinessError).code;
console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`);
}
}
build() {
Row() {
Column() {
Button('click me')
.fontSize(16)
.width(328)
.backgroundColor('#0A59F7')
.onClick(() => {
let uiContext = this.getUIContext();
let promptAction = uiContext.getPromptAction();
let contentNode =
new ComponentContent(uiContext, wrapBuilder(buildText), new Params(this.message)); // 上下文、自定义节点、传参
this.contentNode = contentNode;
try {
promptAction.openCustomDialog(contentNode, {
onWillDismiss: (dismissDialogAction: DismissDialogAction) => {
console.info('reason' + JSON.stringify(dismissDialogAction.reason));
}
});
} catch (error) {
let message = (error as BusinessError).message;
let code = (error as BusinessError).code;
console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`);
}
})
}
.width('100%')
.height('100%')
.margin({ bottom: 40 })
.justifyContent(FlexAlign.End)
}
.height('100%')
}
}
效果演示
方式二 可以基于Navigation自定义弹窗实现,使用NavDestination的回调函数[onBackPressed] ,当与Navigation绑定的页面栈中存在内容时,此回调生效。当点击物理返回按钮或使用手势滑动时,触发该回调。返回值为true时,表示重写返回键逻辑,即可实现拦截。
示例代码
@Entry
@Component
struct InterceptReturn02 {
@Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack();
@Builder
PagesMap(name: string) {
if (name === 'DialogPage') {
DialogPage()
}
}
build() {
Navigation(this.pageStack) {
Button('Push DialogPage')
.fontSize(16)
.width(328)
.backgroundColor('#0A59F7')
.onClick(() => {
this.pageStack.pushPathByName('DialogPage', '');
})
}
.mode(NavigationMode.Stack)
.title('Main')
.navDestination(this.PagesMap)
}
}
@Component
export struct DialogPage {
@Consume('NavPathStack') pageStack: NavPathStack;
build() {
NavDestination() {
Stack({ alignContent: Alignment.Center }) {
Column() {
Text('Dialog NavDestination')
.fontSize(20)
.margin({ top: 24 })
Row() {
Button('Close')
.fontSize(16)
.backgroundColor('#0A59F7')
.onClick(() => {
this.pageStack.pop();
})
.width('100%')
.margin({
top: 8,
bottom: 16
})
}
.padding({
left: 16,
right: 16
})
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(24)
.width(328)
}
.height('100%')
.width('100%')
}
.backgroundColor('rgba(0,0,0,0.5)')
.hideTitleBar(true)
.mode(NavDestinationMode.DIALOG)
.onBackPressed((): boolean => {
return true;
})
}
}
效果演示
切换页面弹窗不消失
点击弹窗中的按钮或链接打开新页面,返回后自定义弹窗还在原页面上展示。
典型场景
用户首次进入应用需要进行权限配置,弹出弹窗后,点击跳转到隐私详情页面,返回后弹窗还在显示。
实现思路
[NavDestinationMode.DIALOG弹窗] 存在于路由栈中,可以实现切换页面弹窗不消失。
示例代码
@Entry
@Component
struct CustomDialogNotDisappear {
@Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack();
@Builder
PagesMap(name: string) {
if (name === 'DialogPage') {
DialogPage()
}
if (name === 'PageOne') {
PageOne()
}
}
build() {
Navigation(this.pageStack) {
Button('Push DialogPage')
.fontSize(16)
.width(328)
.backgroundColor('#0A59F7')
.onClick(() => {
this.pageStack.pushPathByName('DialogPage', '');
})
}
.mode(NavigationMode.Stack)
.title('Main')
.navDestination(this.PagesMap)
}
}
@Component
export struct DialogPage {
@Consume('NavPathStack') pageStack: NavPathStack;
build() {
NavDestination() {
Stack({ alignContent: Alignment.Center }) {
Column() {
Text('Dialog NavDestination')
.fontSize(16)
.margin({ top: 24 })
Row() {
Button('confirm')
.fontSize(16)
.width(328)
.backgroundColor('#0A59F7')
.onClick(() => {
this.pageStack.pushPathByName('PageOne', 'PageOne Param');
})
.width('100%')
.margin({
top: 8,
bottom: 16
})
}
.padding({
left: 16,
right: 16
})
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(24)
.width(328)
}
.height('100%')
.width('100%')
}
.backgroundColor('rgba(0,0,0,0.5)')
.hideTitleBar(true)
.mode(NavDestinationMode.DIALOG)
}
}
@Component
export struct PageOne {
@Consume('NavPathStack') pageStack: NavPathStack;
build() {
NavDestination() {
Stack({ alignContent: Alignment.Center }) {
Column() {
Text('PageOne')
.fontSize(20)
.margin({ bottom: 100 })
Button('back')
.fontSize(16)
.width(328)
.backgroundColor('#0A59F7')
.onClick(() => {
this.pageStack.pop();
})
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.Orange)
.borderRadius(10)
.height('100%')
.width('100%')
}
.height('100%')
.width('100%')
}
.backgroundColor('rgba(0,0,0,0.5)')
.hideTitleBar(true)
.mode(NavDestinationMode.STANDARD)
}
}
效果演示
自定义弹窗显示和退出动画
典型场景
在应用开发中,系统弹窗的显示和退出动画往往不满足需求,若要实现自定义弹窗出入动画,可以使用以下方式,例如:1)渐隐渐显的方式弹出,2)从左往右弹出,从右往左收回,3)从下往上的抽屉式弹出、关闭时从上往下收回。我们以从下往上的抽屉式弹出、关闭时从上往下收回为例,来介绍自定义弹窗的显示和退出动画。
实现思路
可以基于[UIContext.getPromptAction] 弹窗实现,通过CustomDialogOptions自定义弹窗的内容,[BaseDialogOptions] 弹窗选项transition参数可以设置弹窗显示和退出的过渡效果。
示例代码
import { ComponentContent, PromptAction, promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
class TransitionDialogParam {
text: string = '这是一个显示和隐藏动画不同的自定义弹窗';
}
@Builder
function customDialogBuilder(params: TransitionDialogParam) {
Column() {
Text(params.text)
.fontSize(16)
}
.width(328)
.padding(24)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(Color.White)
.borderRadius(24)
}
@Entry
@Component
struct CustomDialogDisplayAndExitAnimations {
promptAction: PromptAction | null = null;
contentNode: ComponentContent<TransitionDialogParam> | null = null;
build() {
Row() {
Column() {
Button('打开弹窗')
.fontSize(16)
.width(328)
.backgroundColor('#0A59F7')
.onClick(() => {
let uiContext = this.getUIContext();
let promptAction = uiContext.getPromptAction();
this.promptAction = promptAction;
let contentNode =
new ComponentContent(uiContext, wrapBuilder(customDialogBuilder), new TransitionDialogParam());
this.contentNode = contentNode;
let options: promptAction.BaseDialogOptions = {
alignment: DialogAlignment.Center,
// 这里设置两个动画,分别对应弹窗显示和隐藏动画
transition: TransitionEffect.asymmetric(
TransitionEffect.OPACITY.animation({ duration: 1000 }).combine(
TransitionEffect.translate({ y: 1000 }).animation({ duration: 1000 }))
,
TransitionEffect.OPACITY.animation({ delay: 1000, duration: 1000 }).combine(
TransitionEffect.translate({ y: 1000 }).animation({ duration: 1000 }))
)
};
try {
promptAction.openCustomDialog(contentNode, options);
} catch (error) {
let message = (error as BusinessError).message;
let code = (error as BusinessError).code;
console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`);
}
})
}
.width('100%')
.height('100%')
.margin({ bottom: 40 })
.justifyContent(FlexAlign.End)
}
.height('100%')
}
}
效果演示
总结
本文从自定义弹窗选型对比、使用场景的角度,主要介绍了以下弹窗的使用区别:
- UIContext.getPromptAction弹窗:适合全局自定义弹窗,不依赖UI组件的场景
- Navigation.Dialog弹窗:适合Navigation路由形态、透明页面、切换页面弹窗不消失的场景
同时对于开发者在弹窗使用中经常遇到的问题,给出详细的解决方案,帮助开发者快速选择自定义弹窗的实现方式,提升开发效率。