UI封装_overlaymanager和window.create
弹窗类UI封装
游戏中存在很多调用系统UI组件进行绘制的场景,这部分UI要求绘制在游戏画面(XComponent组件)上方,同时不影响游戏的正常渲染,如游戏登录及其他弹框、webview页面、扫码页面等等,这种弹窗类UI常常需要统一的封装和控制逻辑,同时支持与页面解耦,可全局使用。
当前系统侧实现自定义弹窗有以下几种方式:
-
CustomDialog弹窗
-
PromptAction弹窗
-
UIContext.getPromptAction弹窗
-
bindsheet
-
UIContext.OverlayManager
-
Navigation.dialog
-
window.createWindow/createWindowWithOption
不同弹窗在使用上存在其局限性,详情可以参考如下表格中的能力支持情况:
游戏场景下,推荐使用UIContext.OverlayManager或UIContext.PromptAction实现弹窗类UI的封装,二者使用方法类似,以下以OverlayMananger为例,介绍如何封装弹窗类UI。
HarmonyOS Next提供了ComponentContent类,用于封装@Builder装饰的自定义组件函数,可以在非UI组件中实例化,从而实现解耦。同时提供OverlayManager管理类,来管理实例化后的ComponentContent的显隐,其节点层级在Page页面之上,在Dialog、Popup、Menu、BindSheet、BindContentCover和Toast等之下。将OverlayManager和ComponentContent结合即可实现弹窗类UI的统一封装和控制,整体逻辑如下图所示:
@Builder装饰器:自定义构建函数
ArkUI提供了一种轻量的UI元素复用机制@Builder,该自定义组件内部UI结构固定,仅与使用方进行数据传递。以下是一个简单的自定义构建函数的示例:
class myParam { msg: string = \'\'; } @Builder function myBuilder(p: myParam) { Text(p.msg) .fontSize(50) .fontWeight(FontWeight.Bold) }
@Builder通过按引用传递的方式传入参数,才会触发动态渲染UI,并且参数只能是一个,因此UI相关的参数建议都放在一个类里面。自定义构建函数本身是没有声明周期的,因此为了实现更复杂的UI场景,可以配合@Component自定义组件使用,示例如下:
class myParam { msg: string = \'\'; } @Component struct myCom { / * 使用@Prop装饰器,在@Builder函数的参数刷新时能够同步刷新@Component自定义组件中的UI状态 * / @Prop param: myParam; aboutToAppear(): void { console.info(\'aboutToAppear\'); } aboutToDisappear(): void { console.info(\'aboutToDisappear\'); } build() { Row() { Column() { Text(this.param.msg) .fontSize(50) .fontWeight(FontWeight.Bold) } .width(\'100%\') } .height(\'100%\') } } @Builder function myBuilder(p: myParam) { myCom({param: p}) }
以上为通过@Builder自定义构建函数实现UI绘制的方法,在将其封装为ComponentContent实例前,还需要进行一步操作,即通过wrapBuilder模板函数将自定义构建函数转换为WrappedBuilder对象,从而实现UI组件的全局赋值和传递,wrapBuilder模板函数接口格式及示例如下:
// 接口规格declare function wrapBuilder(builder: (...args: Args) => void): WrappedBuilder;// 使用示例let builderVar: WrappedBuilder = wrapBuilder(myBuilder);
其中WrappedBuilder中 Object[] 的参数类型需要和wrapBuilder参数中@Builder自定义构建函数的参数类型一致。更具体的说明请见官方文档:@Builder装饰器,wrapBuilder:封装全局@Builder
ComponentContent
ComponentContent类的构造函数如下:
constructor(uiContext: UIContext, builder: WrappedBuilder, args: T)
-
uiContext:当前窗口下的UI上下文,无论是ComponentContent还是OverlayManager都强依赖UIContext,因此强烈建议在ability的onWindowStageCreate生命周期loadContent方法的回调中获取后保存至全局变量或单例中。
-
builder:使用wrapBuilder将@Builder自定义构建函数转换成的WrappedBuilder对象。
-
args:@Builder自定义构建函数的参数。
使用示例如下:
// UIContext获取,在onWindowStageCreate生命周期loadContent方法的回调中let uiContext: UIContext = windowStage.getMainWindowSync().getUIContext();// 自定义构建函数封装let builderVar: WrappedBuilder = wrapBuilder(myBuilder); let p: myParam = {msg: \'test\'}; let myContent: ComponentContent = new ComponentContent(uiContext, builderVar, p);
多内容请参考官方文档ComponentContent。
OverlayManager
OverlayManager对象需要通过UIContext.getOverlayManager方法获取,里面提供了addComponentContent、showComponentContent、 hideComponentContent等方法,来控制封装好的ComponentContent对象的显隐,整合上文提到的所有代码后,以下为通过OverlayManager打开某个弹窗的代码示例:
import { ComponentContent } from \'@kit.ArkUI\'; class myParam { msg: string = \'\'; } @Component struct myCom { @Prop param: myParam; aboutToAppear(): void { console.info(\'aboutToAppear\'); } aboutToDisappear(): void { console.info(\'aboutToDisappear\'); } build() { Row() { Column() { Text(this.param.msg) .fontSize(50) .fontWeight(FontWeight.Bold) .backgroundColor(Color.White) .borderRadius(10) } .width(\'100%\') } .height(\'100%\') .backgroundColor(\'#93000000\') } } @Builder function myBuilder(p: myParam) { myCom({param: p}) } let builderVar: WrappedBuilder = wrapBuilder(myBuilder); let p: myParam = {msg: \'test\'}; @Entry @Component struct Index { @State message: string = \'Hello World\'; uiContext: UIContext = this.getUIContext(); content: ComponentContent = new ComponentContent(this.uiContext, builderVar, p); build() { Row() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) .onClick(() => { this.uiContext.getOverlayManager().addComponentContent(this.content); }) } .width(\'100%\') } .height(\'100%\') } }
多内容请参考官方文档OverlayManager。
常见问题
@Component装饰的自定义组件无法触发onPageShow、onPageHide生命周期,需要在在结构体里做一些其他的配置,以实现上述三个常用的生命周期函数。ArkUI的组件可见区域变化事件onVisibleAreaChange,提供了判断组件是否完全或部分显示在屏幕中的能力。通过设置其ratios参数为[0.0,1.0],能够实现当前组件完全显示或完全消失在屏幕中时触发回调,在回调中绑定onPageShow和onPageHide,即可触发声明周期函数,详细代码如下:
@Component struct myCom { @Prop param: myParam; aboutToAppear(): void { console.info(\'aboutToAppear\'); } onPageShow(): void { console.info(\'onPageShow\'); } onPageHide(): void { console.info(\'onPageHide\'); } aboutToDisappear(): void { console.info(\'aboutToDisappear\'); } build() { Row() { Column() { Text(this.param.msg) .fontSize(50) .fontWeight(FontWeight.Bold) .backgroundColor(Color.White) .borderRadius(10) } .width(\'100%\') } .height(\'100%\') .backgroundColor(\'#93000000\') // 通过设置ratios为[0.0, 1.0],实现当前组件完全显示或消失在屏幕中时触发onpageshow和onpagehide生命周期 .onVisibleAreaChange([0.0, 1.0], (isVisible: boolean, currentRatio: number) => { if (isVisible && currentRatio >= 1.0) { this.onPageShow(); } if (!isVisible && currentRatio <= 0.0) { this.onPageHide(); } }) } }
Overlay节点下的自定义组件无法触发onBackPress生命周期(UIContext.PromptAction创建的弹窗能够响应,无需关注此问题),需要在Page页面的onBackPress生命周期中注册关闭和销毁ComponentContent的逻辑,以下是封装OverlayMananger管理方法的单例及onBackPress配置示例:
const overlayContentStack: ComponentContent[] = [];// Overlay管理单例export class UIHelper { private static _instance: UIHelper; private uiContext: UIContext | undefined; private overlay: OverlayManager | undefined; public static getInstance(): UIHelper { if (!UIHelper._instance) { UIHelper._instance = new UIHelper(); } return UIHelper._instance; } // 在onWindowStageCreated声明周期中loadContent方法的回调中传入UIContext初始化 public init(uiContext: UIContext): void { this.uiContext = uiContext; this.overlay = this.uiContext.getOverlayManager(); } public closeUI(): boolean | void { if (overlayContentStack.length) { this.destroy(); if (overlayContentStack.length) { this.overlay?.showComponentContent(overlayContentStack[overlayContentStack.length - 1]); } return true; } return false; } /* builder: @Builder装饰的自定义构建函数封装后的对象,格式参考:wrapBuilder(testBuilder),其中testBuilder为自定义构建函数 * args: 自定义构建函数传入的参数 * hideLastUI: 是否隐藏上一个componentContent,默认隐藏 */ public showUI(builder: WrappedBuilder, args: T, hideLastUI: boolean = true): void { let content: UIContent = new ComponentContent(this.getUIContext(), builder, args); if (overlayContentStack.length && hideLastUI) { this.hide(); } this.overlay?.addComponentContent(content); overlayContentStack.push(content); } public getCurrentContent(): UIContent { if (overlayContentStack.length) { return overlayContentStack[overlayContentStack.length - 1]; } return undefined; } private getUIContext(): UIContext { if (!this.uiContext) { throw new Error(\'UIContext not init\'); } return this.uiContext as UIContext; } private hide(): void { let lastContent: UIContent = overlayContentStack[overlayContentStack.length - 1]; this.overlay?.hideComponentContent(lastContent); } private destroy(): void { let lastContent: UIContent = overlayContentStack.pop(); this.overlay?.removeComponentContent(lastContent); lastContent?.dispose(); }}// --------------------------------------------// Page页面的结构体中注册onBackPress生命周期函数onBackPress(): boolean | void { if (UIHelper.getInstance().closeUI()) { return true; } return false; }
原文链接: 华为开发者文章
更多问题可关注:
鸿蒙游戏官方网站:已有游戏移植-鸿蒙游戏-华为开发者联盟
公开课:华为开发者学堂