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/RuntimeLoadingGate.ts

290 lines
8.3 KiB

3 weeks ago
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<typeof setTimeout> | null = null;
private _watchdogTimer: ReturnType<typeof setTimeout> | null = null;
private _orphanTimer: ReturnType<typeof setTimeout> | 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;
}
}
}