Files
Monopoly/extensions/app/assets/manager/ui/UIManager.ts
2026-03-30 09:39:59 +08:00

1617 lines
54 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Asset, AssetManager, Camera, Canvas, Component, Event, Layers, Node, Prefab, RenderTexture, ResolutionPolicy, Scene, SceneAsset, Settings, UITransform, Widget, _decorator, director, instantiate, isValid, js, screen, settings, size, view } from 'cc';
import { DEV } from 'cc/env';
import { IMiniViewName, IViewName } from '../../../../../assets/app-builtin/app-admin/executor';
import Core from '../../Core';
import BaseManager from '../../base/BaseManager';
import BaseView, { IHideParamOnHide, IShade, IShowParamAttr, IShowParamOnHide, IShowParamOnShow, IViewType, ViewType } from '../../base/BaseView';
import UIMgrLoading from './comp/UIMgrLoading';
import UIMgrShade from './comp/UIMgrShade';
import UIMgrToast from './comp/UIMgrToast';
import UIMgrZOrder from './comp/UIMgrZOrder';
const { ccclass, property } = _decorator;
interface IShowParams<T, IShow = any, IShowReturn = any, IHideReturn = any> {
/**UI名 */
name: T,
/**
* 数据
* - 被onShow接收
*/
data?: IShow,
/**
* 是否将UI显示在最上
* - 默认true
*/
top?: boolean,
/**
* 队列模式一个UI关闭后是否展示下一个UI
* - join: 排队
* - jump: 插队(到首位)
*/
queue?: 'join' | 'jump',
/**静默 默认false(不显示加载loading也不屏蔽触摸) */
silent?: boolean,
/**UI触发onShow后 */
onShow?: IShowParamOnShow<IShowReturn>,
/**UI触发onHide后 */
onHide?: IShowParamOnHide<IHideReturn>,
/**当code的值为ErrorCode.LogicError时如果返回true则自动重试 */
onError?: (result: string, code: ErrorCode) => true | void,
/**
* @private
* @deprecated
*/
attr?: IShowParamAttr,
}
interface IShowAsyncParams<T, IShow = any, IShowReturn = any> {
/**UI名 */
name: T,
/**
* 数据
* - 被onShow接收
*/
data?: IShow,
/**
* 是否将UI显示在最上
* - 默认true
*/
top?: boolean,
/**
* 队列模式一个UI关闭后是否展示下一个UI
* - join: 排队
* - jump: 插队(到首位)
*/
queue?: 'join' | 'jump',
/**静默 默认false(不显示加载loading也不屏蔽触摸) */
silent?: boolean,
/**UI触发onShow后 */
onShow?: IShowParamOnShow<IShowReturn>,
/**当code的值为ErrorCode.LogicError时如果返回true则自动重试 */
onError?: (result: string, code: ErrorCode) => true | void,
/**
* @private
* @deprecated
*/
attr?: IShowParamAttr,
}
interface IHideParams<T, IHide = any, IHideReturn = any> {
name: T,
data?: IHide,
onHide?: IHideParamOnHide<IHideReturn>
}
const UIScene = 'UIScene';
const UserInterfacePath = 'UserInterface';
const ViewTypes = [ViewType.Page, ViewType.Paper, ViewType.Pop, ViewType.Top];
const BlockEvents = [
Node.EventType.TOUCH_START, Node.EventType.TOUCH_MOVE, Node.EventType.TOUCH_END, Node.EventType.TOUCH_CANCEL,
Node.EventType.MOUSE_DOWN, Node.EventType.MOUSE_MOVE, Node.EventType.MOUSE_UP,
Node.EventType.MOUSE_ENTER, Node.EventType.MOUSE_LEAVE, Node.EventType.MOUSE_WHEEL
];
/**
* 错误码
*/
enum ErrorCode {
/**加载失败 */
LoadError,
/**beforeShow返回错误 */
LogicError,
/**UI无效(UI的isViewValid返回false) */
InvalidError,
}
/**
* 界面名字枚举
*/
const ViewName: { [key in IViewName]: key } = new Proxy({} as any, {
get: function (target, key) {
if (target[key]) return target[key];
target[key] = key;
return key;
}
});
/**
* 子界面名字枚举
*/
const MiniViewName: { [key in IMiniViewName]: key } = new Proxy({} as any, {
get: function (target, key) {
if (target[key]) return target[key];
target[key] = key;
return key;
}
});
/**
* 将驼峰命名转成串式命名
* @param str 驼峰字符串
* @returns
*/
function stringCaseNegate(str: string) {
return str.replace(/[A-Z]/g, (searchStr, startIndex) => {
if (startIndex === 0) {
return searchStr.toLowerCase();
} else {
return '-' + searchStr.toLowerCase();
}
});
}
@ccclass('UIManager')
export default class UIManager<UIName extends string, MiniName extends string> extends BaseManager {
/**静态设置 */
static setting: {
/**预加载列表会在UI初始化阶段进行 */
preload?: (IViewName | IMiniViewName | Array<IViewName | IMiniViewName>)[],
/**默认UI框架初始化完毕后会自动加载 */
defaultUI?: IViewName,
/**给默认UI传递的数据 */
defaultData?: any,
/**弹窗背景遮罩的参数 */
shade?: IShade,
/**
* 是否自动适配分辨率策略
* - 开启后会弃用当前的适配策略,并根据实际设备分辨率与设计分辨率的比值,计算出新的适配策略(宽适配或高适配),保证游戏区域不会被裁减只会扩边
* - 当实际设备分辨率「高/宽」>= 设计分辨率「高/宽」时,为宽适配
* - 当实际设备分辨率「高/宽」< 设计分辨率「高/宽」时,为高适配
*/
autoFit?: boolean,
} = {};
/**错误码 */
static ErrorCode = ErrorCode;
/**界面名字枚举 */
static ViewName = ViewName;
/**子界面名字枚举 */
static MiniViewName = MiniViewName;
@property({
type: Prefab,
tooltip: '位置: app://manager/ui/prefab/UIMgrLoading'
})
private loadingPre: Prefab = null;
@property({
type: Prefab,
tooltip: '位置: app://manager/ui/prefab/UIMgrShade'
})
private shadePre: Prefab = null;
@property({
type: Prefab,
tooltip: '位置: app://manager/ui/prefab/UIMgrToast'
})
private toastPre: Prefab = null;
// UI根节点
private UserInterface: Node = null;
// 加载和遮罩节点
private loading: Node = null;
private shade: Node = null;
private toast: Node = null;
private defaultUI: UIName = null;
private defaultData: string = '';
private currScene: string = '';
private currPage: BaseView = null;
private currFocus: BaseView = null;
// 预制体缓存
private prefabCache: { [name in string]: Prefab } = {};
private sceneCache: { [name in string]: SceneAsset } = {};
// 全局触摸有效
private touchEnabled: boolean = true;
// 记录触摸屏蔽
private touchMaskMap = new Map<string, boolean>();
// 记录展示加载
private showLoadingMap = new Map<string, boolean>();
// 记录正在加载中的有效的ui
private uiLoadingMap: Map<UIName, string[]> = new Map();
// 记录正在展示中的有效的ui
private uiShowingMap: Map<BaseView, UIName> = new Map();
private showQueue: IShowParams<UIName>[] = [];
/**UI根节点 */
public get root() {
return this.node.parent.parent;
}
/**相机 */
public get camera() {
return this.canvas.cameraComponent;
}
/**画布*/
public get canvas() {
return this.root.getComponent(Canvas);
}
protected init(finish: Function) {
const setting = UIManager.setting;
this.defaultUI = setting.defaultUI as UIName;
this.defaultData = setting.defaultData;
super.init(finish);
// 预加载,符合AnyTask规则
if (setting.preload?.length) {
const task = Core.inst.lib.task.createAny();
setting.preload.forEach((preload) => {
if (preload instanceof Array) {
task.add(preload.map(name => {
return next => this.preload(name as any, next);
}));
} else {
task.add(next => this.preload(preload as any, next));
}
});
task.start();
}
}
protected onLoad() {
this.UserInterface = this.root.getChildByName(UserInterfacePath);
this.root.getComponentsInChildren(Camera).forEach(camera => {
// 避免camera.priority<0的情况否则会造成渲染异常
if (camera.priority < 0) camera.priority = 0;
// 避免camera.far<=camera.near的情况否则会造成渲染异常
if (camera.far <= camera.near) camera.far = camera.near + 1;
});
director.addPersistRootNode(this.root);
this.createTypeRoot();
this.shade = instantiate(this.shadePre);
this.shade.parent = this.UserInterface;
this.shade.active = false;
this.shade.getComponent(Widget).target = this.root;
this.loading = instantiate(this.loadingPre);
this.loading.parent = this.node;
this.loading.active = false;
// toast是后面加的需要做容错
if (this.toastPre) {
this.toast = instantiate(this.toastPre);
this.toast.parent = this.node;
}
// 自动适配分辨率策略
if (UIManager.setting.autoFit) {
const designResolution = settings.querySettings(Settings.Category.SCREEN, 'designResolution') as { width: number, height: number, policy: number };
const windowSize = size(screen.windowSize);
let resolutionPolicy = designResolution.policy;
const autoFitResolutionPolicy = function () {
if (windowSize.width / windowSize.height > designResolution.width / designResolution.height) {
if (resolutionPolicy === ResolutionPolicy.FIXED_HEIGHT) return;
view.setResolutionPolicy(ResolutionPolicy.FIXED_HEIGHT);
resolutionPolicy = ResolutionPolicy.FIXED_HEIGHT;
} else {
if (resolutionPolicy === ResolutionPolicy.FIXED_WIDTH) return;
view.setResolutionPolicy(ResolutionPolicy.FIXED_WIDTH);
resolutionPolicy = ResolutionPolicy.FIXED_WIDTH;
}
};
autoFitResolutionPolicy();
this.schedule(() => {
if (windowSize.equals(screen.windowSize)) return;
windowSize.set(screen.windowSize);
autoFitResolutionPolicy();
}, 0.5);
}
}
private createTypeRoot() {
ViewTypes.forEach((type) => {
const d2 = new Node(type);
d2.layer = Layers.Enum.UI_2D;
d2.addComponent(UIMgrZOrder);
d2.parent = this.UserInterface;
d2.addComponent(UITransform);
const widget = d2.addComponent(Widget);
widget.isAlignBottom = true;
widget.isAlignLeft = true;
widget.isAlignRight = true;
widget.isAlignTop = true;
widget.top = 0;
widget.left = 0;
widget.right = 0;
widget.bottom = 0;
widget.alignMode = Widget.AlignMode.ON_WINDOW_RESIZE;
if (DEV) {
d2.on(Node.EventType.CHILD_ADDED, (child: Node) => {
if (!child) return;
if (child === this.shade) return;
if (this.getBaseView(child)) return;
this.warn(`${UserInterfacePath}/${type}下非必要请不要添加非UI节点:`, child?.name);
}, this);
}
});
}
private addTouchMaskListener() {
if (!this.touchEnabled) return;
if (this.touchMaskMap.size > 0) return;
for (let i = 0; i < BlockEvents.length; i++) {
this.root.on(BlockEvents[i], this.stopPropagation, this, true);
}
}
private removeTouchMaskListener() {
if (!this.touchEnabled) return;
if (this.touchMaskMap.size > 0) return;
for (let i = 0; i < BlockEvents.length; i++) {
this.root.off(BlockEvents[i], this.stopPropagation, this, true);
}
}
private stopPropagation(event: Event) {
if (!this.touchEnabled || this.touchMaskMap.size > 0) {
event.propagationStopped = true;
if (event.type !== Node.EventType.MOUSE_MOVE) {
this.log('屏蔽触摸');
}
}
}
/**
* 获取一个节点上的BaseView组件, 获取不到返回null
*/
private getBaseView(node: Node): BaseView {
if (!node) return null;
return node.components.find(component => component instanceof BaseView) as BaseView;
}
/**
* 在所有父节点中找到一个最近的view组件
* @param target
* @returns
*/
private getViewInParents(target: Node) {
let node = target;
let com: BaseView = null;
while (node.parent && !(node.parent instanceof Scene)) {
com = this.getBaseView(node.parent);
if (!com) {
node = node.parent;
} else {
break;
}
}
return com;
}
/**
* 在子节点中找到一个最近的view组件
* @param target
* @returns
*/
private getViewInChildren(target: Node) {
for (let index = 0; index < target.children.length; index++) {
const node = target.children[index];
const com = this.getBaseView(node);
if (com) return com;
}
return null;
}
/**
* 根据UI名字获取它的脚本类
*/
private getUIClass(name: string): typeof BaseView {
return js.getClassByName(name) as (typeof BaseView);
}
/**
* 根据UI名字获取UI路径
* @param name ui名字
* @returns
*/
private getUIPath(name: string) {
return name;
}
/**
* 获取前缀
* @param name ui名字
*/
private getUIPrefix(name: string): ViewType {
for (let index = 0; index < ViewTypes.length; index++) {
const typeName = ViewTypes[index];
if (name.indexOf(typeName) === 0) {
return typeName;
}
}
this.error('getUIPrefix', `${name}`);
}
/**
* 根据UI名字查询父节点
* @param name ui名字
*/
private getUIParent(name: string): Node {
if (this.currScene === name) {
return director.getScene();
}
const prefix = this.getUIPrefix(name);
for (let index = 0; index < ViewTypes.length; index++) {
const viewType = ViewTypes[index];
if (viewType === prefix) {
return this.UserInterface.getChildByName(viewType);
}
}
this.error('getUIParent', `找不到${name}对应的Parent`);
return null;
}
/**
* 根据UI名字获取场景内的节点
* @param name ui名字
*/
private getUIInScene(name: string): Node;
private getUIInScene(name: string, multiple: false): Node;
private getUIInScene(name: string, multiple: true): Node[];
private getUIInScene(name: string, multiple = false) {
const parent = this.getUIParent(name);
if (multiple) {
const result = parent.children.filter(node => node.name === name);
if (result.length) return result.filter(node => isValid(node, true));
} else {
const result = parent.children.find(node => node.name === name);
if (result) return isValid(result, true) ? result : null;
}
return multiple ? [] : null;
}
/**
* 根据UI名字获取展示中的节点
* @param name ui名字
*/
private getUIInShowing(name: string): Node;
private getUIInShowing(name: string, multiple: false): Node;
private getUIInShowing(name: string, multiple: true): Node[];
private getUIInShowing(name: string, multiple = false) {
if (multiple) {
const result: Node[] = [];
this.uiShowingMap.forEach((_name, com) => {
if (_name === name) result.push(com.node);
});
return result;
} else {
let result: Node = null;
this.uiShowingMap.forEach((_name, com) => {
if (!result && _name === name) result = com.node;
});
return result;
}
}
/**
* 获取UI骨架Bundle名字
* @deprecated 将会移除,请改为其它方案
*/
public getNativeBundleName(uiName: UIName | MiniName) {
// 兼容旧版本
const oldBundleName = `app-view_${uiName}`;
const projectBundles = settings.querySettings(Settings.Category.ASSETS, 'projectBundles') as string[];
if (projectBundles && projectBundles.indexOf(oldBundleName) >= 0) {
return oldBundleName;
}
return stringCaseNegate(uiName);
}
/**
* 获取UI资源Bundle名字
* @deprecated 将会移除,请改为其它方案
*/
public getResBundleName(uiName: UIName | MiniName) {
// 兼容旧版本
const oldBundleName = `app-view_${uiName}_Res`;
const projectBundles = settings.querySettings(Settings.Category.ASSETS, 'projectBundles') as string[];
if (projectBundles && projectBundles.indexOf(oldBundleName) >= 0) {
return oldBundleName;
}
return `${stringCaseNegate(uiName)}-res`;
}
/**
* 初始化Bundle
*/
private initBundle(name: UIName | MiniName, onFinish: (result: [AssetManager.Bundle, AssetManager.Bundle]) => any) {
Core.inst.lib.task.createASync<[AssetManager.Bundle, AssetManager.Bundle]>()
.add((next) => {
Core.inst.manager.loader.loadBundle({
bundle: this.getNativeBundleName(name),
onComplete: next
});
})
.add((next) => {
Core.inst.manager.loader.loadBundle({
bundle: this.getResBundleName(name),
onComplete: next
});
})
.start(onFinish);
}
/**
* 安装UI
*/
private installUI(name: UIName | MiniName, complete?: (result: Prefab | SceneAsset | null) => any, progress?: (finish: number, total: number, item: AssetManager.RequestItem) => void) {
if (this.sceneCache[name]) {
complete && setTimeout(() => {
if (!isValid(this)) return;
complete(this.sceneCache[name]);
});
return;
} else if (this.prefabCache[name]) {
complete && setTimeout(() => {
if (!isValid(this)) return;
complete(this.prefabCache[name]);
});
return;
}
const task = Core.inst.lib.task.createSync<[[AssetManager.Bundle, AssetManager.Bundle], Prefab | SceneAsset]>()
.add(next => {
this.initBundle(name, next);
})
.add((next) => {
// 失败
const uiBundles = task.results[0];
if (!uiBundles || !uiBundles[0] || !uiBundles[1]) return next(null);
const isScene = uiBundles[0].getSceneInfo(name);
Core.inst.manager.loader.load({
bundle: this.getNativeBundleName(name),
path: this.getUIPath(name),
type: isScene ? SceneAsset : Prefab,
onProgress: progress,
onComplete: next
});
})
.start((results) => {
if (!isValid(this)) return;
// 验证缓存
const cache = this.sceneCache[name] || this.prefabCache[name];
if (cache) {
return complete && complete(cache);
}
// 验证有效
const asset = results[1];
if (!asset) {
return complete && complete(null);
}
// 添加引用计数
asset.addRef();
// 添加缓存
if (asset instanceof Prefab) {
this.prefabCache[name] = asset;
} else {
this.sceneCache[name] = asset;
}
this.log(`加载: ${name}`);
return complete && complete(asset);
});
}
/**
* 卸载UI
*/
private uninstallUI(name: UIName | MiniName) {
if (this.sceneCache[name]) {
// 释放引用计数
this.sceneCache[name].decRef();
// 删除缓存
delete this.sceneCache[name];
} else if (this.prefabCache[name]) {
// 释放引用计数
this.prefabCache[name].decRef();
// 删除缓存
delete this.prefabCache[name];
}
const resBundle = this.getResBundleName(name);
const naBundle = this.getNativeBundleName(name);
Core.inst.manager.loader.releaseAll(resBundle);
Core.inst.manager.loader.releaseAll(naBundle);
Core.inst.manager.loader.removeBundle(resBundle);
Core.inst.manager.loader.removeBundle(naBundle);
this.log(`卸载: ${name}`);
}
/**
* 加载ui内部资源
*/
public loadRes<T extends typeof Asset>(target: Component, path: string, type: T, callback?: (item: InstanceType<T> | null) => any) {
if (typeof target === 'string') {
Core.inst.manager.loader.load({
bundle: this.getResBundleName(target),
path: path,
type: type,
onComplete: callback
});
} else {
const view = this.getBaseView(target.node) || this.getViewInParents(target.node) || this.getViewInChildren(director.getScene());
if (view) {
Core.inst.manager.loader.load({
bundle: this.getResBundleName(view.viewName as UIName | MiniName),
path: path,
type: type,
onComplete: callback
});
} else {
this.error('loadRes', target.name, path);
callback && callback(null);
}
}
}
/**
* 预加载ui内部资源
*/
public preloadRes<T extends typeof Asset>(target: Component | UIName | MiniName, path: string, type: T, complete?: (item: AssetManager.RequestItem[] | null) => any) {
if (typeof target === 'string') {
Core.inst.manager.loader.preload({
bundle: this.getResBundleName(target),
path: path,
type: type,
onComplete: complete
});
} else {
const view = this.getBaseView(target.node) || this.getViewInParents(target.node) || this.getViewInChildren(director.getScene());
if (view) {
Core.inst.manager.loader.preload({
bundle: this.getResBundleName(view.viewName as UIName | MiniName),
path: path,
type: type,
onComplete: complete
});
} else {
this.error('preloadRes', target.name, path);
}
}
}
/**
* 加载ui内部资源
*/
public loadResDir<T extends typeof Asset>(target: Component, path: string, type: T, callback?: (items: InstanceType<T>[] | null) => any) {
if (typeof target === 'string') {
Core.inst.manager.loader.loadDir({
bundle: this.getResBundleName(target),
path: path,
type: type,
onComplete: callback
});
} else {
const view = this.getBaseView(target.node) || this.getViewInParents(target.node) || this.getViewInChildren(director.getScene());
if (view) {
Core.inst.manager.loader.loadDir({
bundle: this.getResBundleName(view.viewName as UIName | MiniName),
path: path,
type: type,
onComplete: callback
});
} else {
this.error('loadResDir', target.name, path);
callback && callback([]);
}
}
}
/**
* 预加载ui内部资源
*/
public preloadResDir<T extends typeof Asset>(target: Component | UIName | MiniName, path: string, type: T, complete?: (item: AssetManager.RequestItem[] | null) => any) {
if (typeof target === 'string') {
Core.inst.manager.loader.preloadDir({
bundle: this.getResBundleName(target),
path: path,
type: type,
onComplete: complete
});
} else {
const view = this.getBaseView(target.node) || this.getViewInParents(target.node) || this.getViewInChildren(director.getScene());
if (view) {
Core.inst.manager.loader.preloadDir({
bundle: this.getResBundleName(view.viewName as UIName | MiniName),
path: path,
type: type,
onComplete: complete
});
} else {
this.error('preloadResDir', target.name, path);
}
}
}
/**
* 预加载UI
*/
public preload(name: UIName | MiniName, complete?: (item: AssetManager.RequestItem[] | null) => any) {
// 验证name是否为真
if (!name) {
this.error('preload', 'fail');
complete && setTimeout(() => {
if (!isValid(this)) return;
complete(null);
});
return;
}
this.initBundle(name, ([naBundle]) => {
const isScene = naBundle.getSceneInfo(name);
Core.inst.manager.loader.preload({
bundle: this.getNativeBundleName(name),
path: this.getUIPath(name),
type: isScene ? SceneAsset : Prefab,
onComplete: complete
});
});
}
/**
* 加载UI
*/
public load(name: UIName | MiniName): void;
public load(name: UIName | MiniName, complete: (result: Prefab | SceneAsset | null) => any): void;
public load(name: UIName | MiniName, progress: (finish: number, total: number, item: AssetManager.RequestItem) => void, complete: (result: Prefab | SceneAsset | null) => any): void;
public load(name: UIName | MiniName, ...args: Function[]): void {
const progress = (args[1] && args[0]) as (finish: number, total: number, item: AssetManager.RequestItem) => void;
const complete = (args[1] || args[0]) as (result: any) => any;
// 验证name是否为真
if (!name) {
this.error('load', 'fail');
complete && setTimeout(() => {
if (!isValid(this)) return;
complete(null);
});
return;
}
// 异步加载
this.installUI(name, (result) => {
if (!result) return complete && complete(null);
return complete && complete(result);
}, progress);
}
/**
* 销毁UI释放资源
* - 直接销毁不管是否是show状态
* - 此流程一定是同步的
*/
public release(nameOrCom: UIName | MiniName | BaseView) {
const uiName = typeof nameOrCom === 'string' ? nameOrCom : nameOrCom.viewName;
if (!uiName) {
this.error('release', `${nameOrCom} fail`);
return;
}
// 传入字符串是释放所有
if (typeof nameOrCom === 'string') {
this.getUIInScene(uiName, true).forEach((node) => {
const com = this.getBaseView(node);
if (!com) {
this.error('release', `${uiName}不存在BaseView组件`);
return;
}
if (com.isShow) {
this.warn('release', `${uiName}正处于show状态, 此处将直接销毁`);
}
if (com === this.currPage) {
this.currPage = null;
}
if (com === this.currFocus) {
this.currFocus = null;
}
this.uiShowingMap.delete(com);
if (node && isValid(node, true)) {
node.parent = null;
node.destroy();
}
});
}
// 传入组件是释放单个
else {
if (nameOrCom.isShow) {
this.warn('release', `${uiName}正处于show状态, 此处将直接销毁`);
}
if (nameOrCom === this.currPage) {
this.currPage = null;
}
if (nameOrCom === this.currFocus) {
this.currFocus = null;
}
this.uiShowingMap.delete(nameOrCom);
const node = nameOrCom.node;
if (node && isValid(node, true)) {
node.parent = null;
node.destroy();
}
}
// 当全部释放时才清除缓存
const nodes = this.getUIInScene(uiName, true);
if (nodes.length === 0 || nodes.every(node => !isValid(node, true))) {
this.uninstallUI(uiName as UIName | MiniName);
this.log(`释放资源: ${uiName}`);
}
}
/**
* 销毁全部UI释放资源
* - 直接销毁不管是否是show状态
* - 此流程一定是同步的
*/
public releaseAll(exclude?: UIName[]) {
Object.keys(this.prefabCache).forEach((name: UIName) => {
if (exclude && exclude.indexOf(name) !== -1) return;
this.release(name);
});
Object.keys(this.sceneCache).forEach((name: UIName) => {
if (exclude && exclude.indexOf(name) !== -1) return;
this.release(name);
});
}
/**
* 检查UI是否有效
* - -1: 加载失败
* - 0: UI无效
* - 1: UI有效
*/
private checkUIValid(name: UIName | MiniName, data: any, callback: (valid: -1 | 0 | 1) => any) {
this.installUI(name, (result) => {
if (!result) return callback(-1);
const View = this.getUIClass(name);
if (!View) return callback(0);
if (!View.isViewValid) return callback(1);
View.isViewValid((valid: boolean) => {
callback(valid ? 1 : 0);
}, data);
});
}
/**
* 更新阴影的层级及显示
*/
public refreshShade() {
// 借助refreshShade实现onFocus、onLostFocus(onFocus不会被每次都触发只有产生变化时才触发)
let onFocus = false;
// 倒序遍历uiRoots
let uiRoots = this.UserInterface.children;
for (let index = uiRoots.length - 1; index >= 0; index--) {
const uiRoot = uiRoots[index];
if (uiRoot !== this.shade && uiRoot !== this.loading) {
// 倒序遍历uiRoot
let children = uiRoot.children;
for (let i = children.length - 1; i >= 0; i--) {
const node = children[i];
if (node === this.shade) continue;
const com = this.getBaseView(node);
if (!com) continue;
// 触发onFocus
if (!onFocus && com.isCaptureFocus && com.isShow) {
onFocus = true;
if (this.currFocus !== com) {
isValid(this.currFocus, true) && this.currFocus.constructor.prototype.focus.call(this.currFocus, false);
this.currFocus = com;
this.currFocus.constructor.prototype.focus.call(this.currFocus, true);
}
}
// 添加遮罩
if (com.isNeedShade && com.isShow) {
const shadeSetting = Object.assign({}, UIManager.setting.shade, com.constructor.prototype.onShade.call(com));
if (shadeSetting.blur) {
this.shade.getComponent(UIMgrShade).init(0, 255, 255, 0, true);
} else {
this.shade.getComponent(UIMgrShade).init(
typeof shadeSetting.delay !== 'number' ? 0 : shadeSetting.delay,
typeof shadeSetting.begin !== 'number' ? 60 : shadeSetting.begin,
typeof shadeSetting.end !== 'number' ? 180 : shadeSetting.end,
typeof shadeSetting.speed !== 'number' ? 100 : shadeSetting.speed,
false,
);
}
this.shade.layer = node.layer;
this.shade.parent = uiRoot;
this.shade.active = true;
// 以z坐标来代替2.x时代的zIndex
this.shade.setPosition(this.shade.position.x, this.shade.position.y, node.position.z);
let shadeIndex = this.shade.getSiblingIndex();
let nodeIndex = node.getSiblingIndex();
if (shadeIndex > nodeIndex) {
this.shade.setSiblingIndex(nodeIndex);
} else {
this.shade.setSiblingIndex(nodeIndex - 1);
}
return;
}
}
}
}
this.shade.active = false;
this.shade.getComponent(UIMgrShade).clear();
if (!onFocus) {
isValid(this.currFocus, true) && this.currFocus.constructor.prototype.focus.call(this.currFocus, false);
this.currFocus = null;
}
}
// 解析prefab
private parsingPrefab(prefab: Prefab, name: string) {
if (!prefab) return null;
const node = instantiate(prefab);
node.active = false;
if (node.name !== name) {
this.warn('parsingPrefab', `节点名与UI名不一致, 已重置为UI名: ${this.getUIPath(name)}`);
node.name = name;
}
node.parent = this.getUIParent(name);
node.getComponent(Widget)?.updateAlignment();
return node;
}
// 解析scene
private parsingScene(asset: SceneAsset, name: string) {
if (!asset || !asset.scene) return null;
if (asset.scene.name !== name) {
this.warn('parsingScene', `场景名与UI名不一致, 已重置为UI名: ${this.getUIPath(name)}`);
asset.scene.name = name;
}
const view = this.getViewInChildren(asset.scene);
if (!view) {
this.error('parsingScene', `解析场景时未查询到根节点存在BaseView: ${this.getUIPath(name)}`);
return null;
}
view.node.active = false;
if (view.node.name !== name) {
this.warn('parsingScene', `节点名与UI名不一致, 已重置为UI名: ${this.getUIPath(name)}`);
view.node.name = name;
}
return view.node;
}
private addUILoadingUuid(name: UIName, loadingUuid?: string) {
const uuid = loadingUuid || this.createUUID();
if (!this.uiLoadingMap.has(name)) {
this.uiLoadingMap.set(name, [uuid]);
} else {
this.uiLoadingMap.get(name).push(uuid);
}
return uuid;
}
private removeUILoadingUuid(name: UIName, uuid: string) {
if (!this.uiLoadingMap.has(name)) return false;
const index = this.uiLoadingMap.get(name).indexOf(uuid);
if (index === -1) return false;
this.uiLoadingMap.get(name).splice(index, 1);
return true;
}
/**
* 创建UI
*/
private createUI(name: UIName, silent: boolean, callback: (node: Node, scene?: Scene) => any) {
// 生成一个UI加载的UUID
const loadingUuid = silent ? '' : this.showLoading();
const uiLoadingUuid = this.addUILoadingUuid(name, loadingUuid);
// 验证name
if (!name) {
setTimeout(() => {
if (!isValid(this)) return;
// 验证本次加载是否有效
if (this.removeUILoadingUuid(name, uiLoadingUuid) === false) {
return this.hideLoading(loadingUuid);
}
callback(null);
this.hideLoading(loadingUuid);
});
return;
}
// 判断是否已经存在节点并且是单例模式
const node = this.getUIInScene(name);
if (isValid(node, true) && this.getBaseView(node).isSingleton === true) {
setTimeout(() => {
if (!isValid(this)) return;
// 验证本次加载是否有效
if (this.removeUILoadingUuid(name, uiLoadingUuid) === false) {
return this.hideLoading(loadingUuid);
}
// 验证节点是否有效
if (isValid(node, true)) {
if (this.currScene === name) {
callback(node, director.getScene());
} else {
callback(node);
}
this.hideLoading(loadingUuid);
} else {
this.createUI(name, silent, callback);
this.hideLoading(loadingUuid);
}
});
return;
}
// 加载UI
this.load(name, (asset) => {
if (!isValid(this)) return;
// 验证本次加载是否有效
if (this.removeUILoadingUuid(name, uiLoadingUuid) === false) {
return this.hideLoading(loadingUuid);
}
// 是场景
if (asset instanceof SceneAsset) {
callback(this.parsingScene(asset, name), asset.scene);
this.hideLoading(loadingUuid);
return;
}
// 验证是否是单例(一个单例会有被同时load多次的情况因为判断一个ui是否是单例必须要至少实例化一个后才能获取)
const node = this.getUIInScene(name);
if (!isValid(node, true) || this.getBaseView(node).isSingleton === false) {
callback(this.parsingPrefab(asset, name));
this.hideLoading(loadingUuid);
} else {
callback(node);
this.hideLoading(loadingUuid);
}
});
}
/**
* 展示默认View
*/
public showDefault(onShow?: (result?: any) => any) {
if (this.defaultUI) {
this.show({
name: this.defaultUI,
data: this.defaultData,
onShow
});
} else {
Core.inst.manager.ui.showToast('请先设置首界面\n在setting.ts中修改defaultUI', 100);
onShow && onShow();
this.warn('defaultUI不存在请在setting.ts中修改');
}
}
/**
* 是否展示了(包括加载中和队列中)
*/
public isShow(name: UIName) {
return !!this.getUIInShowing(name) ||
this.isInQueue(name) ||
this.isLoading(name);
}
/**
* 是否在队列中
*/
public isInQueue(name: UIName) {
return !!this.showQueue.find((v) => { return v.name == name; });
}
/**
* 是否在加载中
*/
public isLoading(name: UIName) {
return this.uiLoadingMap.has(name) && this.uiLoadingMap.get(name).length > 0;
}
/**
* 放入队列
*/
private putInShowQueue(data: IShowParams<UIName>) {
if (data.queue === 'join' || this.showQueue.length === 0) {
this.showQueue.push(data);
} else {
this.showQueue.splice(1, 0, data);
}
if (this.showQueue.length === 1) {
this.consumeShowQueue();
}
}
/**
* 消耗队列
*/
private consumeShowQueue() {
if (this.showQueue.length === 0) return;
const data = this.showQueue[0];
this.show({
name: data.name,
data: data.data,
onShow: data.onShow,
onHide: (result: any) => {
data.onHide && data.onHide(result);
this.showQueue.shift();
this.consumeShowQueue();
},
onError: data.onError ? (error: string, code: 0 | 1) => {
const ret = data.onError(error, code);
this.showQueue.shift();
this.consumeShowQueue();
return ret;
} : undefined,
top: data.top,
attr: data.attr,
silent: data.silent
});
}
private showUI(params: IShowParams<UIName>) {
const { name, data, onShow, onHide, onError, top = true, attr = null, silent = false } = params;
this.createUI(name, silent, (node, scene) => {
if (!node) {
this.error('show', `${name} 不存在或加载失败`);
// 「没有指定onError」或「onError返回true」会自动发起重试
if (onError && onError(`${name} 不存在或加载失败`, UIManager.ErrorCode.LoadError) !== true) {
return;
}
this.scheduleOnce(() => this.showUI(params), 1);
if (!silent) this.showLoading(1);
return;
}
!scene && top && node.setSiblingIndex(-1);
const com = this.getBaseView(node);
this.uiShowingMap.set(com, name);
com.constructor.prototype.show.call(com, data, attr,
// onShow
(result: any) => {
this.uiShowingMap.set(com, name);
onShow && onShow(result);
},
// onHide
(result: any) => {
this.uiShowingMap.delete(com);
onHide && onHide(result);
},
// beforeShow
(error: string) => {
if (error) {
this.uiShowingMap.delete(com);
onError && onError(error, UIManager.ErrorCode.LogicError);
} else if (BaseView.isPage(name)) {
this.uiShowingMap.set(com, name);
const oldCom = this.currPage;
this.currPage = com;
if (isValid(oldCom, true) && oldCom !== com && oldCom.isShow && oldCom.isAlwaysExist === false) {
oldCom.constructor.prototype.hide.call(oldCom, { name });
}
if (scene) {
if (oldCom !== com) {
this.currScene = name;
director.runSceneImmediate(scene, null, () => {
this.log(`切换场景: ${name}`);
});
}
} else if (this.currScene !== UIScene) {
this.currScene = UIScene;
const scene = new Scene(UIScene);
scene.autoReleaseAssets = true;
director.runSceneImmediate(scene, null, () => {
this.log(`切换场景: ${UIScene}`);
});
}
}
}
);
});
}
/**
* 展示一个UI
* - 此流程一定是异步的
*/
public show<UI extends BaseView>(params
// @ts-ignore
: IShowParams<UIName, Parameters<UI['onShow']>[0], ReturnType<UI['onShow']>, ReturnType<UI['onHide']>>) {
const { name, data, queue, onError, silent = false } = params;
// 加入队列中
if (queue) {
this.putInShowQueue(params);
return;
}
this.log(`show: ${name}`);
// 生成一个UI加载的UUID
const loadingUuid = silent ? '' : this.showLoading();
const uiLoadingUuid = this.addUILoadingUuid(name, loadingUuid);
// 判断ui是否有效
Core.inst.lib.task.execute((retry) => {
this.checkUIValid(name, data, (valid) => {
// 验证本次加载是否有效
if (this.removeUILoadingUuid(name, uiLoadingUuid) === false) {
this.hideLoading(loadingUuid);
return;
}
// 加载失败
if (valid === -1) {
this.error('show', `${name} 不存在或加载失败`);
// 「没有指定onError」或「onError返回true」会自动发起重试
if (onError && onError(`${name} 不存在或加载失败`, UIManager.ErrorCode.LoadError) !== true) {
return this.hideLoading(loadingUuid);
}
return retry(1);
}
// ui无效
if (valid === 0) {
this.warn('show', `${name} 无效`);
this.uninstallUI(name);
onError && onError(`${name} 无效`, UIManager.ErrorCode.InvalidError);
this.hideLoading(loadingUuid);
return;
}
this.showUI(params);
this.hideLoading(loadingUuid);
});
});
}
/**
* 展示一个UI
* - 此流程一定是异步的
*/
public showAsync<UI extends BaseView>(params
// @ts-ignore
: IShowAsyncParams<UIName, Parameters<UI['onShow']>[0], ReturnType<UI['onShow']>>): Promise<ReturnType<UI['onHide']>> {
return new Promise((resolve) => {
this.show({
...params,
onHide(result) {
resolve(result);
}
});
});
}
/**
* 关闭View
* - 此流程一定是同步的
*/
public hide<UI extends BaseView>({ name, data, onHide }
// @ts-ignore
: IHideParams<UIName, Parameters<UI['onHide']>[0], ReturnType<UI['onHide']>>) {
const nodes = this.getUIInShowing(name, true);
this.log(`hide: ${name}`);
if (nodes.length === 0) {
if (!this.uiLoadingMap.has(name) || this.uiLoadingMap.get(name).length === 0) {
return this.warn('hide', `${name} 不存在`);
}
}
if (this.uiLoadingMap.has(name)) {
this.uiLoadingMap.get(name).forEach((loadingUuid) => this.hideLoading(loadingUuid));
this.uiLoadingMap.get(name).length = 0;
}
for (let index = nodes.length - 1; index >= 0; index--) {
const node = nodes[index];
const com = this.getBaseView(node);
if (this.currPage === com) {
this.currPage = null;
}
com.constructor.prototype.hide.call(com, data, onHide);
}
}
/**
* 从顶部关闭一个View(不会重复关闭节点)
* - 此流程一定是同步的
*/
public pop<UI extends BaseView>({ name, data, onHide }
// @ts-ignore
: IHideParams<UIName, Parameters<UI['onHide']>[0], ReturnType<UI['onHide']>>) {
const nodes = this.getUIInShowing(name, true);
if (this.uiLoadingMap.has(name) && this.uiLoadingMap.get(name).length) {
const loadingUuid = this.uiLoadingMap.get(name).pop();
this.hideLoading(loadingUuid);
this.log(`pop: ${name}`);
return;
}
if (nodes.length) {
const node = nodes.pop();
const com = this.getBaseView(node);
if (this.currPage === com) {
this.currPage = null;
}
com.constructor.prototype.hide.call(com, data, onHide);
this.log(`pop: ${name}`);
return;
}
this.warn('pop', `${name} 不存在`);
}
/**
* 从底部关闭一个View(不会重复关闭节点)
* - 此流程一定是同步的
*/
public shift<UI extends BaseView>({ name, data, onHide }
// @ts-ignore
: IHideParams<UIName, Parameters<UI['onHide']>[0], ReturnType<UI['onHide']>>) {
const nodes = this.getUIInShowing(name, true);
if (nodes.length) {
const node = nodes[0];
const com = this.getBaseView(node);
if (this.currPage === com) {
this.currPage = null;
}
com.constructor.prototype.hide.call(com, data, onHide);
this.log(`shift: ${name}`);
return;
}
if (this.uiLoadingMap.has(name) && this.uiLoadingMap.get(name).length) {
const loadingUuid = this.uiLoadingMap.get(name).shift();
this.hideLoading(loadingUuid);
this.log(`shift: ${name}`);
return;
}
this.warn('shift', `${name} 不存在`);
}
/**
* 关闭全部View
* - 不关闭展示中的Page(加载中的会停止)
* - 此流程一定是同步的
*/
public hideAll({ data, exclude }: { data?: any, exclude?: UIName[] } = {}): void {
this.log('hideAll');
// 展示中的
this.uiShowingMap.forEach((name, com) => {
if (BaseView.isPaper(name)) return;
if (exclude && exclude.indexOf(name) !== -1) return;
if (com === this.currPage) return;
com.constructor.prototype.hide.call(com, data);
});
// 加载中的
this.uiLoadingMap.forEach((value, name) => {
if (BaseView.isPaper(name)) return;
if (exclude && exclude.indexOf(name) !== -1) return;
value.forEach((loadingUuid) => this.hideLoading(loadingUuid));
value.length = 0;
});
}
public showLoading(timeout = 0) {
this.loading.active = true;
this.loading.setSiblingIndex(-1);
if (this.loading.getComponent(UIMgrLoading)) {
this.loading.getComponent(UIMgrLoading).init();
} else {
// 兼容旧版本
this.loading.getComponentInChildren(UIMgrLoading)?.init();
}
const uuid = this.createUUID();
this.showLoadingMap.set(uuid, true);
if (timeout > 0) this.scheduleOnce(() => {
this.hideLoading(uuid);
}, timeout);
return uuid;
}
public hideLoading(uuid: string) {
if (!uuid) return;
this.showLoadingMap.delete(uuid);
if (this.showLoadingMap.size === 0) {
if (this.loading.getComponent(UIMgrLoading)) {
this.loading.getComponent(UIMgrLoading).clear();
} else {
// 兼容旧版本
this.loading.getComponentInChildren(UIMgrLoading)?.clear();
}
this.loading.active = false;
}
}
/**
* 添加触摸屏蔽
*/
public addTouchMask(timeout = 0) {
this.addTouchMaskListener();
const uuid = this.createUUID();
this.touchMaskMap.set(uuid, true);
if (timeout > 0) this.scheduleOnce(() => {
this.removeTouchMask(uuid);
}, timeout);
return uuid;
}
/**
* 移除触摸屏蔽
* @param uuid addTouchMask的返回值
*/
public removeTouchMask(uuid: string) {
if (!uuid) return;
this.touchMaskMap.delete(uuid);
this.removeTouchMaskListener();
}
/**
* 显示Toast
* @param message 文本
* @param timeout 持续时间(秒)默认2秒
*/
public showToast(message: string, timeout?: number) {
if (!this.toast) {
return this.error('showToast', '请确认首场景中「Canvas/Manager/UIManager」的「Toast Pre」属性存在');
}
this.toast.setSiblingIndex(-1);
this.toast.getComponent(UIMgrToast).add({
message, timeout
});
}
/**
* 清理Toast
*/
public clearToast() {
if (!this.toast) return;
this.toast.getComponent(UIMgrToast).clear();
}
/**
* 设置触摸是否启用
* @param enabled 是否启用
*/
public setTouchEnabled(enabled: boolean) {
if (enabled) {
this.touchEnabled = true;
this.removeTouchMaskListener();
} else {
this.addTouchMaskListener();
this.touchEnabled = false;
}
this.warn('setTouchEnabled', this.touchEnabled);
}
/**
* 在2DUI根节点上处理事件
*/
public onUserInterface(...args: Parameters<Node['on']>) {
Node.prototype.on.apply(this.UserInterface, args);
}
/**
* 在2DUI根节点上处理事件
*/
public onceUserInterface(...args: Parameters<Node['once']>) {
Node.prototype.once.apply(this.UserInterface, args);
}
/**
* 在2DUI根节点上处理事件
*/
public offUserInterface(...args: Parameters<Node['off']>) {
Node.prototype.off.apply(this.UserInterface, args);
}
/**
* 在2DUI根节点上处理事件
*/
public targetOffUserInterface(...args: Parameters<Node['targetOff']>) {
Node.prototype.targetOff.apply(this.UserInterface, args);
}
/**
* 立即给2DUI的子节点排序
*/
public sortUserInterface(name: IViewType) {
this.UserInterface
?.getChildByName(name)
?.getComponent(UIMgrZOrder)
?.updateZOrder();
}
/**
* 屏幕截图
* - 需要在Director.EVENT_BEFORE_RENDER事件中调用
* @example
* director.once(Director.EVENT_BEFORE_RENDER, () => {
* const renderTexture = new RenderTexture();
* const size = view.getVisibleSize();
* renderTexture.reset({ width: size.width, height: size.height });
* app.manager.ui.screenshot(renderTexture);
* });
*/
public screenshot(renderTexture: RenderTexture, opts?: {
/**摄像机筛选 */
cameraFilter?: (camera: Camera) => boolean;
/**摄像机列表 */
cameraList?: Camera[];
}) {
const cameras = opts?.cameraList || director.getScene().getComponentsInChildren(Camera);
const cameraList = cameras.sort((a, b) => a.priority - b.priority)
.filter(camera => {
if (!camera.enabledInHierarchy) return false;
if (camera.targetTexture) return false;
return opts?.cameraFilter ? opts.cameraFilter(camera) : true;
});
const cameraList2 = cameraList.map(camera => camera.camera);
cameraList.forEach(camera => {
camera.targetTexture = renderTexture;
});
director.root.pipeline.render(cameraList2);
cameraList.forEach(camera => {
camera.targetTexture = null;
});
return renderTexture;
}
}