import { Node, tween, Tween, UIOpacity } from 'cc'; import { RuntimeLoadingConfig } from './RuntimeLoadingConfig'; /** * 运行时 loading 门闩:ref 计数 + 延迟显示 + 渐显渐隐。 * 约束:ref<=0 时 UI 必须不可见;孤儿 UI(active 但 ref<=0)会被自动清掉。 */ export class RuntimeLoadingGate { private _ref = 0; private _visible = false; private _fadingOut = false; private _pendingIndex = 2; private _showTimer: ReturnType | null = null; private _watchdogTimer: ReturnType | null = null; private _orphanTimer: ReturnType | null = null; /** show 定时器代际,forceHide / dismiss 时递增以作废过期回调 */ private _showGeneration = 0; private _opacity: UIOpacity | null = null; constructor( private _getLoadingUI: () => Node | null, private _getBootLoading: () => Node | null, private _isRuntimePhase: () => boolean, private _onWatchdog: (() => void) | null = null, ) { } requestShow(index: number, immediate = false): void { if (!this._isRuntimePhase()) { this._showImmediate(index); return; } this._ref++; this._pendingIndex = index; this._cancelOrphanTimer(); if (this._fadingOut) { this._cancelFadeOut(); this._visible = true; this._applyIndex(index); this._armWatchdog(); return; } if (this._visible) { this._applyIndex(index); return; } if (immediate) { this._invalidateShowTimer(); this._fadeIn(index); return; } if (this._showTimer) { return; } const gen = this._showGeneration; this._showTimer = setTimeout(() => { this._showTimer = null; if (gen !== this._showGeneration || this._ref <= 0) { return; } this._fadeIn(this._pendingIndex); }, RuntimeLoadingConfig.showAfterMs); } requestHide(): void { if (!this._isRuntimePhase()) { this._hideImmediate(); return; } this._ref = Math.max(0, this._ref - 1); if (this._ref > 0) { return; } this._dismiss(false); } forceHide(): void { this._ref = 0; this._invalidateShowTimer(); if (!this._isRuntimePhase()) { this._hideImmediate(); return; } this._dismiss(true); } dispose(): void { this._invalidateShowTimer(); this._cancelWatchdog(); this._cancelOrphanTimer(); if (this._opacity) { Tween.stopAllByTarget(this._opacity); } } private _invalidateShowTimer(): void { this._showGeneration++; if (this._showTimer) { clearTimeout(this._showTimer); this._showTimer = null; } } private _dismiss(immediate: boolean): void { this._invalidateShowTimer(); const ui = this._getLoadingUI(); const needHide = this._visible || (ui?.active ?? false); if (!needHide) { this._visible = false; this._fadingOut = false; this._cancelWatchdog(); this._scheduleOrphanCheck(); return; } if (immediate || !this._visible) { this._finalizeHidden(); return; } this._fadeOut(); } private _fadeIn(index: number): void { const ui = this._getLoadingUI(); const opacity = this._ensureOpacity(); if (!ui || !opacity) { return; } this._fadingOut = false; this._applyIndex(index); ui.active = true; const boot = this._getBootLoading(); if (boot?.active) { boot.active = false; } Tween.stopAllByTarget(opacity); opacity.opacity = 0; tween(opacity) .to(RuntimeLoadingConfig.fadeInMs / 1000, { opacity: 255 }) .start(); this._visible = true; this._armWatchdog(); } private _fadeOut(): void { const opacity = this._ensureOpacity(); if (!opacity) { this._finalizeHidden(); return; } this._fadingOut = true; Tween.stopAllByTarget(opacity); tween(opacity) .to(RuntimeLoadingConfig.fadeOutMs / 1000, { opacity: 0 }) .call(() => { this._fadingOut = false; if (this._ref <= 0) { this._finalizeHidden(); } else { this._visible = true; opacity.opacity = 255; this._armWatchdog(); } }) .start(); } private _cancelFadeOut(): void { const opacity = this._ensureOpacity(); if (opacity) { Tween.stopAllByTarget(opacity); opacity.opacity = 255; } this._fadingOut = false; } private _finalizeHidden(): void { this._cancelWatchdog(); const opacity = this._ensureOpacity(); if (opacity) { Tween.stopAllByTarget(opacity); opacity.opacity = 0; } const ui = this._getLoadingUI(); if (ui) { ui.active = false; } this._visible = false; this._fadingOut = false; this._scheduleOrphanCheck(); } private _showImmediate(index: number): void { const ui = this._getLoadingUI(); if (!ui) { return; } ui.active = true; this._applyIndex(index); const opacity = this._ensureOpacity(); if (opacity) { Tween.stopAllByTarget(opacity); opacity.opacity = 255; } const boot = this._getBootLoading(); if (boot?.active) { boot.active = false; } } private _hideImmediate(): void { this._invalidateShowTimer(); this._cancelWatchdog(); this._cancelOrphanTimer(); if (this._opacity) { Tween.stopAllByTarget(this._opacity); } const ui = this._getLoadingUI(); if (ui) { ui.active = false; } this._visible = false; this._fadingOut = false; } private _applyIndex(index: number): void { const ui = this._getLoadingUI(); if (!ui) { return; } const node1 = ui.find('1'); const node2 = ui.find('2'); if (node1) node1.active = index === 1; if (node2) node2.active = index === 2; } private _ensureOpacity(): UIOpacity | null { const ui = this._getLoadingUI(); if (!ui) { return null; } if (!this._opacity) { this._opacity = ui.getComponent(UIOpacity) ?? ui.addComponent(UIOpacity); } return this._opacity; } private _armWatchdog(): void { this._cancelWatchdog(); this._watchdogTimer = setTimeout(() => { this._watchdogTimer = null; const ui = this._getLoadingUI(); if (!ui?.active) { return; } console.log('[RuntimeLoadingGate] loading 显示超时,强制关闭 ref=', this._ref); this._onWatchdog?.(); this.forceHide(); }, RuntimeLoadingConfig.maxVisibleMs); } private _cancelWatchdog(): void { if (this._watchdogTimer) { clearTimeout(this._watchdogTimer); this._watchdogTimer = null; } } private _scheduleOrphanCheck(): void { this._cancelOrphanTimer(); this._orphanTimer = setTimeout(() => { this._orphanTimer = null; if (this._ref > 0) { return; } const ui = this._getLoadingUI(); if (ui?.active) { console.log('[RuntimeLoadingGate] 检测到孤儿 loading UI,强制关闭'); this._finalizeHidden(); } }, RuntimeLoadingConfig.orphanCheckMs); } private _cancelOrphanTimer(): void { if (this._orphanTimer) { clearTimeout(this._orphanTimer); this._orphanTimer = null; } } }