UI封装 原创

游戏技术分享
发布于 2025-5-8 15:55
浏览
0收藏

弹窗类UI封装

游戏中存在很多调用系统UI组件进行绘制的场景,这部分UI要求绘制在游戏画面(XComponent组件)上方,同时不影响游戏的正常渲染,如游戏登录及其他弹框、webview页面、扫码页面等等,这种弹窗类UI常常需要统一的封装和控制逻辑,同时支持与页面解耦,可全局使用。

当前系统侧实现自定义弹窗有以下几种方式:

不同弹窗在使用上存在其局限性,详情可以参考如下表格中的能力支持情况:

  UI封装 -鸿蒙开发者社区

游戏场景下,推荐使用UIContext.OverlayManager或UIContext.PromptAction实现弹窗类UI的封装,二者使用方法类似,以下以OverlayMananger为例,介绍如何封装弹窗类UI。

HarmonyOS Next提供了ComponentContent类,用于封装@Builder装饰的自定义组件函数,可以在非UI组件中实例化,从而实现解耦。同时提供OverlayManager管理类,来管理实例化后的ComponentContent的显隐,其节点层级在Page页面之上,在Dialog、Popup、Menu、BindSheet、BindContentCover和Toast等之下。将OverlayManager和ComponentContent结合即可实现弹窗类UI的统一封装和控制,整体逻辑如下图所示:

  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;  
}

原文链接: ​​华为开发者文章​


 更多问题可关注:

鸿蒙游戏官方网站:​​已有游戏移植-鸿蒙游戏-华为开发者联盟​

公开课:​​华为开发者学堂​

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分类
收藏
回复
举报
回复
    相关推荐