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.
289 lines
8.3 KiB
289 lines
8.3 KiB
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;
|
|
}
|
|
}
|
|
}
|
|
|