
UI封装 原创
弹窗类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< Args extends Object[]>(builder: (...args: Args) => void): WrappedBuilder;
// 使用示例
let builderVar: WrappedBuilder<[myParam]> = wrapBuilder(myBuilder);
其中WrappedBuilder<Object[]>中 Object[] 的参数类型需要和wrapBuilder参数中@Builder自定义构建函数的参数类型一致。更具体的说明请见官方文档:@Builder装饰器,wrapBuilder:封装全局@Builder
ComponentContent
ComponentContent类的构造函数如下:
constructor(uiContext: UIContext, builder: WrappedBuilder<[T]>, 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<[myParam]> = wrapBuilder(myBuilder);
let p: myParam = {msg: 'test'};
let myContent: ComponentContent<object> = 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<[myParam]> = wrapBuilder(myBuilder);
let p: myParam = {msg: 'test'};
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
uiContext: UIContext = this.getUIContext();
content: ComponentContent<object> = 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<object>[] = [];
// 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<T extends object>(builder: WrappedBuilder<[T]>, 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;
}
原文链接: 华为开发者文章
更多问题可关注:
鸿蒙游戏官方网站:已有游戏移植-鸿蒙游戏-华为开发者联盟
公开课:华为开发者学堂
