You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/mx/module/ui/UIBase.ts

518 lines
18 KiB

3 weeks ago
import { _decorator, Component, Node, Layout, sp, Tween, tween, UIOpacity, UIRenderer, Vec3 } from 'cc';
import { UIInfo } from './UIManager';
import { ResPakage } from '../res/ResourcesManager';
import {
IPopAnimConfig,
IPopAnimSession,
IPopRevealConfig,
PopCenterPreset,
PopMaskPreset,
popStaggerNode,
PopStaggerPreset,
preparePopReveal,
runPopReveal,
stopPopStagger,
POP_REVEAL_START_DELAY,
} from './popAnim';
const { ccclass } = _decorator;
@ccclass('UIBase')
export class UIBase extends Component {
getResPkg(): ResPakage {
return null;
}
onLoad(): void {
}
uiInfo: UIInfo = null;
prema: any;
private _onOpenAnimEnd: Function;
private _onClose: Function;
private _showLoading: number = 0;
private _popSession: IPopAnimSession = { mask: 'none', center: 'none' };
private _popOpenOnceDone = false;
/** onInit 里动态创建的待分帧节点(center 入场完后播放) */
private _pendingRevealNodes: Node[] = [];
private _pendingRevealPreset: PopStaggerPreset = 'scale';
/** 内容区入场完成,动态项可立即错峰 */
private _popRevealReady = false;
private _revealSeq = 0;
/** popRevealEnqueue 登记过的节点,用于跳过动效时一次性显示 */
private _revealTrackNodes: Node[] = [];
/** 道具/列表项错峰动效是否已全部播完(含跳过) */
private _itemRevealComplete = true;
/** 内部 reveal 是否已启动(与 center 弹入重叠,不等 center 结束) */
private _revealBegun = false;
private _pendingRevealCfg: IPopAnimConfig | null = null;
private _markItemRevealComplete = (): void => {
this._itemRevealComplete = true;
};
private _beginItemRevealIfNeeded = (): void => {
if (this._revealBegun) return;
const cfg = this._pendingRevealCfg;
if (!cfg) return;
this._revealBegun = true;
this._runRevealFromConfig(cfg);
};
private _scheduleBeginItemReveal(cfg: IPopAnimConfig): void {
this._pendingRevealCfg = cfg;
this.unschedule(this._beginItemRevealIfNeeded);
const center = cfg.center ?? 'none';
const delay = center === 'none' ? 0.02 : POP_REVEAL_START_DELAY;
this.scheduleOnce(this._beginItemRevealIfNeeded, delay);
}
private _resolveRevealConfig(cfg: IPopAnimConfig): IPopRevealConfig | null {
if (cfg.reveal) return cfg.reveal;
if (cfg.stagger && cfg.staggerNode) {
return { itemsNode: cfg.staggerNode, itemsPreset: cfg.stagger };
}
return null;
}
/** 上屏前同步初始姿态(遮罩透明、内容区 t=0、列表项隐藏) */
preparePopOpenPose(): void {
const cfg = this.uiInfo?.uiconf?.popAnim as IPopAnimConfig | undefined;
if (!cfg) return;
if (cfg.once && this._popOpenOnceDone) return;
this._popRevealReady = false;
this._revealSeq = 0;
this._revealTrackNodes = [];
this._itemRevealComplete = true;
this._revealBegun = false;
this._pendingRevealCfg = null;
this.unschedule(this._markItemRevealComplete);
this.unschedule(this._beginItemRevealIfNeeded);
gg.ui.popAnim.prepareOpen(
this.node,
cfg.mask ?? 'none',
cfg.center ?? 'none',
);
const reveal = this._resolveRevealConfig(cfg);
if (reveal) {
const center = gg.ui.popAnim.resolveCenterNode(this.node);
preparePopReveal(this.node, center, reveal);
}
}
/** UIManager 在 onInit 前调用(含复开) */
beforePanelInit(): void {
this.preparePopOpenPose();
this.playPopFromConfig();
}
/**
* 写入本次打开参数(含已打开再开)。
* @param args 打开参数
*/
applyOpenParam(args?: OpenParam): void {
this.prema = args?.data;
this._onOpenAnimEnd = args?.onOpenAnimEnd;
this._onClose = args?.onClose;
if (args?.showLoading !== undefined && args?.showLoading !== null) {
this._showLoading = args.showLoading;
}
}
/**
* 通过UI管理器打开面板时才会触发onAdded
* @param args
*/
onAdded(args: OpenParam): void | Promise<void> {
this.applyOpenParam(args);
let resPkg = this.getResPkg();
if (resPkg) {
return new Promise<void>((resolve) => {
let settled = false;
const settle = (): void => {
if (settled) return;
settled = true;
gg.game.hideLoading();
resolve();
};
// 资源包挂起/onInit 抛错都会让 openPanel inflight 永不结束 → 同 id 再也打不开
const guard = setTimeout(() => {
console.error('[UIBase] loadResPakg/onAdded timeout', this.node?.name);
if (this.isValid && this.node?.isValid) {
try {
this.beforePanelInit();
void Promise.resolve(this.onInit()).catch((e) => {
console.error('[UIBase] onInit after pkg timeout', this.node?.name, String(e));
});
} catch (e) {
console.error('[UIBase] onInit after pkg timeout', this.node?.name, String(e));
}
}
settle();
}, 30000);
try {
gg.res.loadResPakg(resPkg, () => { }, () => {
clearTimeout(guard);
if (!this.isValid || !this.node?.isValid) {
settle();
return;
}
void (async () => {
try {
this.beforePanelInit();
await Promise.resolve(this.onInit());
} catch (e) {
console.error('[UIBase] onInit failed', this.node?.name, String(e));
} finally {
settle();
}
})();
});
} catch (e) {
console.error('[UIBase] loadResPakg throw', this.node?.name, String(e));
clearTimeout(guard);
settle();
}
});
}
this.beforePanelInit();
return Promise.resolve(this.onInit());
}
onInit(): void | Promise<void> {
}
// ─── 弹窗动效库(按需组合,无 options 对象) ───
/** 遮罩入场 */
popMask(preset: PopMaskPreset = 'dim'): void {
if (gg.ui.popAnim.openMask(this.node, preset)) {
this._popSession.mask = preset;
}
}
/** 自定义入场动效结束后通知 UIManager(子类勿直接访问 _onOpenAnimEnd) */
protected notifyOpenAnimEnd(): void {
this._onOpenAnimEnd?.();
}
/** 面板关闭回调(子类 destroyPanel 复写时调用) */
protected notifyPanelClose(): void {
this._onClose?.();
}
/** 内容区入场 */
popCenter(preset: PopCenterPreset = 'scale', callback?: () => void): void {
const center = gg.ui.popAnim.resolveCenterNode(this.node);
const done = () => {
callback?.();
this._onOpenAnimEnd?.();
};
if (center) {
gg.ui.popAnim.prepareOpen(this.node, 'none', preset);
}
if (center && gg.ui.popAnim.openCenter(center, preset, done)) {
this._popSession.center = preset;
return;
}
done();
}
/** 读取 ConfigRes.popAnim 自动播放入场 */
playPopFromConfig(): void {
const cfg = this.uiInfo?.uiconf?.popAnim as IPopAnimConfig | undefined;
if (!cfg) return;
if (cfg.once && this._popOpenOnceDone) return;
const mask = cfg.mask ?? 'none';
const center = cfg.center ?? 'none';
if (mask !== 'none') this.popMask(mask);
const onCenterDone = () => {
if (cfg.once) this._popOpenOnceDone = true;
this._onOpenAnimEnd?.();
};
this._scheduleBeginItemReveal(cfg);
if (center !== 'none') {
this.popCenter(center, onCenterDone);
} else {
onCenterDone();
}
}
private _runRevealFromConfig(cfg: IPopAnimConfig): void {
const reveal = this._resolveRevealConfig(cfg);
const center = gg.ui.popAnim.resolveCenterNode(this.node);
if (reveal) {
runPopReveal(this.node, center, reveal);
}
this._flushPendingReveal();
this._popRevealReady = true;
this.onPopOpenRevealEnd();
}
/** 面板入场 + reveal 配置播完后回调(虚拟列表在此后停止入场动效) */
protected onPopOpenRevealEnd(): void { }
/** 道具/列表项错峰动效是否仍在播放 */
protected isItemRevealPlaying(): boolean {
return !this._itemRevealComplete;
}
/**
* 跳过面板入场与道具错峰动效,全部立即显示(不关闭面板)
* 用于获得页:点遮罩先跳过动画,再点才关闭
*/
protected skipPopReveal(): void {
this.unschedule(this._markItemRevealComplete);
this.unschedule(this._beginItemRevealIfNeeded);
this._revealBegun = true;
gg.ui.popAnim.snapCenterOpen(this.node);
const reveal = this._resolveRevealConfig(
(this.uiInfo?.uiconf?.popAnim as IPopAnimConfig | undefined) ?? {},
);
if (reveal) {
const center = gg.ui.popAnim.resolveCenterNode(this.node);
const names = [
...(reveal.itemNodes ?? []),
...(reveal.lastNodes ?? []),
];
if (reveal.itemsNode) {
const container = center?.getChildByName(reveal.itemsNode)
?? this.node.getChildByName(reveal.itemsNode);
container?.children.forEach((c) => {
if (c.active) stopPopStagger(c);
});
}
for (const name of names) {
const n = center?.getChildByName(name) ?? this.node.getChildByName(name);
if (n?.isValid) stopPopStagger(n);
}
}
for (const n of this._revealTrackNodes) {
if (n?.isValid) stopPopStagger(n);
}
for (const n of this._pendingRevealNodes) {
if (n?.isValid) stopPopStagger(n);
}
this._pendingRevealNodes = [];
this._popRevealReady = true;
this._itemRevealComplete = true;
this.onPopOpenRevealEnd();
}
private _armItemRevealCompleteTimer(): void {
this.unschedule(this._markItemRevealComplete);
const total = Math.max(this._revealTrackNodes.length, this._revealSeq);
if (total === 0) {
this._itemRevealComplete = true;
return;
}
this._itemRevealComplete = false;
const delay = (total - 1) * 0.04 + 0.22;
this.scheduleOnce(this._markItemRevealComplete, delay);
}
private _flushPendingReveal(): void {
const nodes = this._pendingRevealNodes;
if (!nodes.length) return;
this._pendingRevealNodes = [];
const preset = this._pendingRevealPreset;
nodes.forEach((n, i) => {
if (n?.isValid) popStaggerNode(n, preset, i * 0.04);
});
this._revealSeq = nodes.length;
this._armItemRevealCompleteTimer();
}
/** 动态列表项:先隐藏,与 center 弹入重叠错峰出现(在 onInit instantiate 后调用) */
popRevealEnqueue(node: Node, preset: PopStaggerPreset = 'scale'): void {
if (!node?.isValid) return;
if (!this._revealTrackNodes.includes(node)) {
this._revealTrackNodes.push(node);
}
this._itemRevealComplete = false;
if (preset === 'scale' || preset === 'celebrate') {
node.setScale(Vec3.ZERO);
} else {
node.setScale(Vec3.ONE);
const op = node.getComponent(UIOpacity) || node.addComponent(UIOpacity);
op.opacity = 0;
}
if (this._popRevealReady) {
popStaggerNode(node, preset, this._revealSeq * 0.04);
this._revealSeq++;
this._armItemRevealCompleteTimer();
return;
}
this._pendingRevealNodes.push(node);
this._pendingRevealPreset = preset;
}
/** 容器子节点错峰 */
popStagger(container: Node, preset: PopStaggerPreset = 'scale'): void {
const children = container.children.filter(c => c.active);
children.forEach((child, i) => {
popStaggerNode(child, preset, i * 0.04);
});
}
/** 单个节点错峰(立即播放,不等待 center) */
popStaggerItem(node: Node, index: number, preset: PopStaggerPreset = 'scale'): void {
popStaggerNode(node, preset, index * 0.04);
}
// ─── 兼容旧 API(内部委托 popAnim 库) ───
showOpenCenterEff(callback?: () => void) {
this.popCenter('scale', callback);
}
showOpenCenterEffCute(callback?: () => void) {
this.popCenter('bounce', callback);
}
showCloseCenterEff(type = 1, callback?: () => void) {
const center = gg.ui.popAnim.resolveCenterNode(this.node);
if (!center) {
callback?.();
return false;
}
if (type === 1) {
return gg.ui.popAnim.closeCenter(center, 'scale', callback);
}
if (type === 2) {
Tween.stopAllByTarget(center);
tween(center).to(0.2, { opacity: 0 }).set({ opacity: 255 }).call(() => callback?.()).start();
return true;
}
callback?.();
return false;
}
showCloseCenterEffCute(callback?: () => void) {
const center = gg.ui.popAnim.resolveCenterNode(this.node);
if (!center) {
callback?.();
return false;
}
return gg.ui.popAnim.closeCenter(center, 'bounce', callback);
}
showOpenMaskEff(type = 1) {
if (type === 1) this.popMask('root');
else this.popMask('dim');
}
showOpenMaskEffCute() {
this.popMask('dimFast');
}
closeMaskEff(callback?: () => void) {
gg.ui.popAnim.closeMask(this.node, this._popSession.mask, callback);
return this._popSession.mask !== 'none';
}
async close(tweenType = 1) {
await gg.ui.closePanel(this.uiInfo.id, tweenType);
}
/** 关页/切场景前由 UIManager 或 GameController 调用:中止仍依赖有效资源的子表现 */
teardownBeforeDestroy(): void {
}
/** 销毁前断开渲染与 Spine,避免 bundle 释放后 SkeletonSystem 仍 update */
protected prepareDestroyForRender(): void {
if (!this.node?.isValid) return;
// 先停渲染,再 active=false(否则 SpriteLoad.onDisable 会 decRef,脏列表仍 updateUVs)
const renderers = this.node.getComponentsInChildren(UIRenderer);
for (const r of renderers) {
if (r?.isValid) r.enabled = false;
}
this.node.active = false;
const stopTweenDeep = (n: Node) => {
if (!n?.isValid) return;
Tween.stopAllByTarget(n);
for (const c of n.children) {
stopTweenDeep(c);
}
};
stopTweenDeep(this.node);
const layouts = this.node.getComponentsInChildren(Layout);
for (const layout of layouts) {
if (layout?.isValid && layout.enabled) {
layout.enabled = false;
}
}
const sks = this.node.getComponentsInChildren(sp.Skeleton);
for (const sk of sks) {
if (sk?.isValid) {
sk.enabled = false;
try {
const skAny = sk as sp.Skeleton & { isAnimationCached?: () => boolean };
if (typeof skAny.isAnimationCached !== 'function' || !skAny.isAnimationCached()) {
sk.clearTracks();
}
sk.setCompleteListener(null);
} catch (_) { /* wasm 已释放等 */ }
}
}
}
destroyPanel(tweenType = 1) {
return new Promise((resolve) => {
const finish = () => {
this.prepareDestroyForRender();
if (this.node.isValid) {
this.node.destroy();
}
resolve(true);
this.notifyPanelClose();
};
if (tweenType === 2 || tweenType === 3) {
gg.ui.popAnim.playClose(this.node, this._popSession, tweenType, finish);
return;
}
finish();
});
}
protected onDestroy(): void {
let resPkg = this.getResPkg();
if (resPkg) {
gg.res.clearResPkg(resPkg.name);
}
this.prema = null;
this._onOpenAnimEnd = null;
this._onClose = null;
this.uiInfo = null;
this._popSession = { mask: 'none', center: 'none' };
this._popOpenOnceDone = false;
this._popRevealReady = false;
this._revealSeq = 0;
this._pendingRevealNodes = [];
this._revealTrackNodes = [];
this.unschedule(this._markItemRevealComplete);
this.unschedule(this._beginItemRevealIfNeeded);
this._revealBegun = false;
this._pendingRevealCfg = null;
}
}
/**
* 打开面板参数
*/
export interface OpenParam {
onOpenAnimEnd?: Function;
onClose?: Function;
data?: any;
showLoading?: number;
}