diff --git a/package.json b/package.json index 1243ee7..5bd4637 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kunpocc", - "version": "1.0.19", + "version": "1.0.20", "type": "module", "description": "基于creator3.0+的kunpocc库", "main": "./dist/kunpocc.cjs", diff --git a/src/cocos/CocosUIModule.ts b/src/cocos/CocosUIModule.ts index fdf49ee..bb25fcb 100644 --- a/src/cocos/CocosUIModule.ts +++ b/src/cocos/CocosUIModule.ts @@ -34,7 +34,7 @@ export class CocosUIModule extends ModuleBase { // this._uiInitializer.init(this.reAdaptWhenScreenResize, this.fullIfWideScreen); this.node.destroyAllChildren(); /** 注册窗口信息 */ - WindowManager.registerUI() + WindowManager.registerUI(); this.onInit(); } diff --git a/src/fgui/WindowBase.ts b/src/fgui/WindowBase.ts index 1aac463..62188a3 100644 --- a/src/fgui/WindowBase.ts +++ b/src/fgui/WindowBase.ts @@ -135,7 +135,7 @@ export abstract class WindowBase extends GComponent implements IWindow { return this._header as T; } - public setHeader(header: T): void { + public _setHeader(header: T): void { this._header = header; } diff --git a/src/kunpocc.ts b/src/kunpocc.ts index d392bdc..f34a519 100644 --- a/src/kunpocc.ts +++ b/src/kunpocc.ts @@ -41,7 +41,8 @@ export { Ticker as BTTicker } from "./behaviortree/Ticker"; /** UI */ export { Window } from "./fgui/Window"; export { WindowHeader } from "./fgui/WindowHeader"; -export * from "./ui/header"; +export { AdapterType, WindowType } from "./ui/header"; +export { IPackageConfig } from "./ui/IPackageConfig"; export { _uidecorator } from "./ui/UIDecorator"; export { WindowGroup } from "./ui/WindowGroup"; export { WindowHeaderInfo } from "./ui/WindowHeaderInfo"; diff --git a/src/ui/IPackageConfig.ts b/src/ui/IPackageConfig.ts new file mode 100644 index 0000000..5f07bbf --- /dev/null +++ b/src/ui/IPackageConfig.ts @@ -0,0 +1,42 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-25 + * @Description: 包配置格式 + */ + + + +export interface IPackageConfig { + /** UI所在resources中的路径 */ + uiPath: string; + /** + * 手动管理资源的包 + * 1. 用于基础UI包, 提供一些最基础的组件,所有其他包都可能引用其中的内容 + * 2. 资源header所在的包 + * 3. 用于一些特殊场景, 比如需要和其他资源一起加载, 并且显示进度条的包 + */ + manualPackages: string[]; + /** + * 不推荐配置 只是提供一种特殊需求的实现方式 + * 窗口引用到其他包中的资源 需要的配置信息 + */ + linkPackages: { [windowName: string]: string[] }; + + /** + * 关闭界面后,需要立即释放资源的包名(建议尽量少) + * 一般不建议包进行频繁装载卸载,因为每次装载卸载必然是要消耗CPU时间(意味着耗电)和产生大量GC的。UI系统占用的内存是可以精确估算的,你可以按照包的使用频率设定哪些包是需要立即释放的。 + * 不包括手动管理的包 + */ + imReleasePackages: string[]; +} + +export interface IPackageConfigRes { + /** 配置信息 */ + config: IPackageConfig; + /** 显示加载等待窗 */ + showWaitWindow: () => void; + /** 隐藏加载等待窗 */ + hideWaitWindow: () => void; + /** 打开窗口时UI包加载失败 */ + fail: (windowName: string, errmsg: string, pkgs: string[]) => void; +} \ No newline at end of file diff --git a/src/ui/IWindow.ts b/src/ui/IWindow.ts index 6e76793..c6deb69 100644 --- a/src/ui/IWindow.ts +++ b/src/ui/IWindow.ts @@ -75,5 +75,5 @@ export interface IWindow { /** 获取资源栏数据 */ getHeaderInfo(): WindowHeaderInfo; - setHeader(header: IWindowHeader): void; + _setHeader(header: IWindowHeader): void; } \ No newline at end of file diff --git a/src/ui/WindowGroup.ts b/src/ui/WindowGroup.ts index e0e0c29..9b95ae9 100644 --- a/src/ui/WindowGroup.ts +++ b/src/ui/WindowGroup.ts @@ -339,7 +339,7 @@ export class WindowGroup { let name = headerInfo.name; let header = this._getHeader(name); if (header) { - window.setHeader(header); + window._setHeader(header); header._addRef(); } else { // 创建header节点 @@ -347,7 +347,7 @@ export class WindowGroup { let newHeader = UIPackage.createObject(pkg, name) as WindowHeader; newHeader.name = name; newHeader.opaque = false; - window.setHeader(newHeader); + window._setHeader(newHeader); newHeader.visible = false; PropsHelper.serializeProps(newHeader, pkg); newHeader._init(); @@ -390,4 +390,14 @@ export class WindowGroup { this._alphaGraph.setSize(Screen.ScreenWidth, Screen.ScreenHeight, true); this._alphaGraph.setPivot(0.5, 0.5, true); } + + /** + * 关闭窗口组中的所有窗口 + */ + public closeAllWindow(): void { + while (this.size > 0) { + let name = this.getTopWindowName(); + WindowManager.closeWindow(name); + } + } } diff --git a/src/ui/WindowManager.ts b/src/ui/WindowManager.ts index 7ca9d89..f031384 100644 --- a/src/ui/WindowManager.ts +++ b/src/ui/WindowManager.ts @@ -6,6 +6,7 @@ import { debug, warn } from "../tool/log"; import { ComponentExtendHelper } from "./ComponentExtendHelper"; +import { IPackageConfigRes } from "./IPackageConfig"; import { IWindow } from "./IWindow"; import { _uidecorator } from "./UIDecorator"; import { WindowGroup } from "./WindowGroup"; @@ -21,24 +22,39 @@ export class WindowManager { /** 初始化时传入实例 */ private static _resPool: WindowResPool; - /** - * 打开一个窗口 - * @param windowName 窗口名 - * @param userdata 用户数据 - */ - public static showWindow(windowName: string, userdata?: any): void { - //TODO::如果没有资源 加载资源 - this.showWindowIm(windowName, userdata); + /** 配置UI包的一些信息 (可以不配置 完全手动管理) */ + public static initPackageConfig(res: IPackageConfigRes): void { + this._resPool.initPackageConfig(res); } /** - * 显示指定名称的窗口,并传递可选的用户数据。 + * 异步打开一个窗口 (如果UI包的资源未加载, 会自动加载 配合 WindowManager.initPackageConfig一起使用) + * @param windowName 窗口名 + * @param userdata 用户数据 + */ + public static async showWindow(windowName: string, userdata?: any): Promise { + return new Promise((resolve, reject) => { + this._resPool.loadWindowRes(windowName, { + complete: () => { + this.showWindowIm(windowName, userdata); + resolve(); + }, + fail: (pkgs: string[]) => { + reject(pkgs); + } + }); + }); + } + + /** + * 显示指定名称的窗口,并传递可选的用户数据。(用于已加载过资源的窗口) * @param windowName - 窗口的名称。 * @param userdata - 可选参数,用于传递给窗口的用户数据。 */ public static showWindowIm(windowName: string, userdata?: any): void { const info = this._resPool.get(windowName); const windowGroup = this.getWindowGroup(info.group); + this._resPool.addResRef(windowName); windowGroup.showWindow(info, userdata); } @@ -71,6 +87,25 @@ export class WindowManager { } } + /** + * 关闭所有窗口 + * @param ignoreNames 忽略关闭的窗口名 + */ + public static closeAllWindow(ignoreNames: string[] = []): void { + let existIgnore = ignoreNames.length > 0; + this._windows.forEach((window: IWindow, name: string) => { + if (!existIgnore) { + this.closeWindow(name); + } else if (!ignoreNames.includes(name)) { + this.closeWindow(name); + } + }); + + if (!existIgnore) { + this._windows.clear(); + } + } + /** * 获取当前最顶层的窗口实例。 * @template T - 窗口实例的类型,必须继承自 IWindow 接口。 @@ -162,6 +197,7 @@ export class WindowManager { if (this.hasWindow(name)) { this._windows.get(name)._close(); this._windows.delete(name); + this._resPool.releaseWindowRes(name); } } @@ -175,6 +211,7 @@ export class WindowManager { for (const { ctor, res } of _uidecorator.getWindowMaps().values()) { debug(`窗口注册 窗口名:${res.name} 包名:${res.pkg} 组名:${res.group}`); this._resPool.add(ctor, res.group, res.pkg, res.name); + } // 窗口header注册 for (const { ctor, res } of _uidecorator.getHeaderMaps().values()) { diff --git a/src/ui/WindowResPool.ts b/src/ui/WindowResPool.ts index 180bb5b..7f3f7f8 100644 --- a/src/ui/WindowResPool.ts +++ b/src/ui/WindowResPool.ts @@ -4,7 +4,9 @@ * @Description: */ -import { UIObjectFactory } from "fairygui-cc"; +import { UIObjectFactory, UIPackage } from "fairygui-cc"; +import { warn } from "../kunpocc"; +import { IPackageConfigRes } from "./IPackageConfig"; export interface WindowInfo { /** 类的构造函数 */ @@ -28,7 +30,23 @@ export class WindowResPool { /** 窗口header信息池 */ protected _headerInfos: Map = new Map(); - /** 可扩展 窗口资源引用计数 */ + /** 是否设置过配置内容 */ + private _isInit: boolean = false; + /** 窗口名对应的包名列表 */ + private _windowPkgs: Map = new Map(); + /** 包的引用计数 */ + private _pkgRefs: { [pkg: string]: number } = {}; + private _uipath: string = ""; + private _manualPackages: Set = new Set(); + private _imReleasePackages: Set = new Set(); + + /** 注册的回调函数 */ + private _showWaitWindow: () => void = null; + private _hideWaitWindow: () => void = null; + private _fail: (windowName: string, errmsg: string, pkgs: string[]) => void = null; + + /** 等待窗口的引用计数 */ + private _waitRef: number = 0; /** * 注册窗口信息 @@ -44,6 +62,7 @@ export class WindowResPool { pkg: pkg, name: name }); + this.addWindowPkg(name, pkg); // 窗口组件扩展 UIObjectFactory.setExtension(`ui://${pkg}/${name}`, ctor); } @@ -82,4 +101,192 @@ export class WindowResPool { } return this._headerInfos.get(name); } + + /** 资源配置相关接口 */ + public initPackageConfig(res: IPackageConfigRes): void { + if (!res || !res.config) { + return; + } + if (this._isInit) { + throw new Error("资源配置已初始化,请勿重复设置"); + } + this._isInit = true; + this._showWaitWindow = res?.showWaitWindow; + this._hideWaitWindow = res?.hideWaitWindow; + this._fail = res?.fail; + + this._uipath = res.config?.uiPath || ""; + // 如果uipath不以/结尾 则添加/ + if (this._uipath != "" && !this._uipath.endsWith("/")) { + this._uipath += "/"; + } + + this._manualPackages = new Set(res.config.manualPackages || []); + this._imReleasePackages = new Set(res.config.imReleasePackages || []); + + let windowPkgs = res.config.linkPackages || {}; + for (const windowName in windowPkgs) { + let pkgs = windowPkgs[windowName]; + for (const pkg of pkgs || []) { + this.addWindowPkg(windowName, pkg); + } + } + + // 遍历一遍,剔除手动管理的包 + this._windowPkgs.forEach((pkgs: string[], windowName: string) => { + for (let i = pkgs.length - 1; i >= 0; i--) { + if (this._manualPackages.has(pkgs[i])) { + pkgs.splice(i, 1); + } + } + if (pkgs.length <= 0) { + this._windowPkgs.delete(windowName); + } + }); + } + /** 添加窗口对应的包名 */ + public addWindowPkg(windowName: string, pkgName: string): void { + if (!this._windowPkgs.has(windowName)) { + this._windowPkgs.set(windowName, [pkgName]); + } else { + this._windowPkgs.get(windowName).push(pkgName); + } + } + + /** + * 加载窗口需要的包资源 + * @param windowName 窗口名 + */ + public loadWindowRes(windowName: string, listenter: { complete: () => void, fail: (pkgs: string[]) => void }): void { + // 资源配置未初始化 直接返回成功 + if (!this._isInit) { + warn(`UI包信息未配置 将手动管理所有UI包资源的加载,如果需要配置,请使用 【WindowManager.initPackageConfig】接口`); + listenter.complete(); + return; + } + // 不需要包资源 直接返回成功 + if (!this.hasWindowPkg(windowName)) { + listenter.complete(); + return; + } + if (this._waitRef++ <= 0) { + // 调用注入的回调函数 用来显示等待窗 + this._showWaitWindow?.(); + } + this.loadPackages({ + pkgs: this.getWindowPkgs(windowName), + complete: () => { + if (--this._waitRef <= 0) { + // 调用注入的回调函数 关闭等待窗 + listenter.complete(); + this._hideWaitWindow?.(); + } + }, + fail: (pkgs: string[]) => { + warn(`界面${windowName}打开失败`); + listenter.fail(pkgs); + this._fail?.(windowName, "UI包加载失败", pkgs); + if (--this._waitRef <= 0) { + // 调用注入的回调函数 关闭等待窗 + this._hideWaitWindow?.(); + } + } + }); + } + + public addResRef(windowName: string): void { + if (!this._isInit) { + return; + } + // 不需要包资源 直接返回成功 + if (!this.hasWindowPkg(windowName)) { + return; + } + let pkgs = this.getWindowPkgs(windowName); + for (const pkg of pkgs) { + this.addRef(pkg); + } + } + + /** + * 释放窗口资源 + * @param windowName 窗口名 + */ + public releaseWindowRes(windowName: string): void { + if (!this._isInit || !this.hasWindowPkg(windowName)) { + return; + } + let pkgs = this.getWindowPkgs(windowName); + for (const pkg of pkgs) { + this.decRef(pkg); + } + } + + /** + * 加载fgui包 + * @param pkgs 包名集合 + * @param progress 进度回调 + * @param complete 加载完成回调 + */ + private loadPackages(res: { pkgs: string[], complete: () => void, fail: (pkgs: string[]) => void }): void { + // 过滤已经加载的包 + let needLoadPkgs = res.pkgs.filter(pkg => this.getRef(pkg) <= 0); + + let successPkgs: string[] = []; + let failPkgs: string[] = []; + let total: number = needLoadPkgs.length; + if (total <= 0) { + res.complete(); + return; + } + for (const pkg of needLoadPkgs) { + UIPackage.loadPackage(this._uipath + pkg, (err: any) => { + total--; + err ? failPkgs.push(pkg) : successPkgs.push(pkg); + if (total > 0) { + return; + } + if (failPkgs.length > 0) { + res.fail(failPkgs); + } else { + res.complete(); + } + }); + } + } + + /** 获取窗口对应的包名列表 */ + private getWindowPkgs(windowName: string): string[] { + if (this._windowPkgs.has(windowName)) { + return this._windowPkgs.get(windowName); + } + return []; + } + + private hasWindowPkg(windowName: string): boolean { + return this._windowPkgs.has(windowName); + } + + /** 获取包的引用计数 */ + private getRef(pkg: string): number { + return this._pkgRefs[pkg] ? this._pkgRefs[pkg] : 0; + } + + /** 增加包的引用计数 */ + private addRef(pkg: string): void { + this._pkgRefs[pkg] = this.getRef(pkg) + 1; + } + + /** 减少包的引用计数 */ + private decRef(pkg: string): void { + this._pkgRefs[pkg] = this.getRef(pkg) - 1; + if (this.getRef(pkg) <= 0) { + delete this._pkgRefs[pkg]; + + // 如果需要立即释放 释放包资源 + if (this._imReleasePackages.has(pkg)) { + UIPackage.removePackage(pkg); + } + } + } } \ No newline at end of file