NLDClient-yudde/ProjectNLD/Assets/Code/Scripts/Framework/UI/UIWindow.Lifecycle.cs

597 lines
40 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using Cysharp.Threading.Tasks;
using DG.Tweening;
using Framework;
using PhxhSDK;
using UnityEngine.UI;
public abstract partial class UIWindow
{
/*
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ UIWindow 生命周期 (Lifecycle) ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 1. 【Load 阶段】- 资源加载 ║
║ UIManager.CreateWindow() ║
║ ├─ 加载预制体资源 ║
║ ├─ 实例化GameObject ║
║ └─ 创建UIWindow实例 ║
║ ║
║ 2. 【Init 阶段】- 初始化 ║
║ UIWindow.Init(data) // 窗口初始化(公共接口,不可重写) ║
║ ├─ DoInit() // 基础初始化 ║
║ │ ├─ *AutoBindField() // 绑定UI组件引用 ║
║ │ ├─ InitScreenAdaption() // 屏幕适配 ║
║ │ └─ _TryBindBgClose() // 绑定背景关闭按钮 ║
║ └─ *OnInit() // 初始化UI状态设置默认值 ║
║ ║
║ 3. 【Show 阶段】- 显示 ║
║ UIWindow.ShowWindow(data) // 显示窗口(公共接口,不可重写) ║
║ ├─ *OnBeforeShow(data) // 显示前检查 ║
║ ├─ DoShow() // 基础显示逻辑 ║
║ ├─ SetActive(true) // 激活GameObject ║
║ ├─ *OnAfterShow(data) // 显示后处理 ║
║ └─ 发送UI打开事件 // EventManager.Send ║
║ ║
║ 4. 【Update 阶段】- 运行时更新 ║
║ UIWindow.Update(dt) // 更新入口(公共接口,不可重写) ║
║ └─ *OnUpdate(dt) // 每帧更新处理UI逻辑 ║
║ ║
║ 5. 【Refresh 阶段】- 数据刷新 ║
║ UIWindow.Refresh(data) // 数据刷新(公共接口,不可重写) ║
║ └─ *OnRefresh(data) // 刷新UI内容更新显示数据 ║
║ ║
║ 6. 【Hide 阶段】- 隐藏 ║
║ UIWindow.HideWindow() // 隐藏窗口(公共接口,不可重写) ║
║ ├─ *OnHide() // 隐藏回调 ║
║ ├─ SetActive(false) // 隐藏GameObject ║
║ └─ 发送UI隐藏事件 // EventManager.Send ║
║ ║
║ 7. 【Close 阶段】- 关闭销毁 ║
║ UIWindow.CloseWindow() // 关闭窗口(公共接口,不可重写) ║
║ └─ DestroyWindow() // 内部销毁逻辑 ║
║ ├─ *OnHide() // 隐藏回调 ║
║ ├─ *OnRelease() // 释放资源,清理引用 ║
║ ├─ DoRelease() // 基础资源释放 ║
║ ├─ Destroy(gameObject) // 销毁GameObject ║
║ └─ 卸载预制体资源 // AssetManager.Unload ║
║ ║
║ 【架构设计原则】 ║
║ • 🔒 接口保护: 公共接口方法不可重写,确保框架逻辑完整性 ║
║ • 🎯 职责分离: Init和Show独立调用Show负责显示配置Refresh负责内容更新 ║
║ • ⚡ 性能优化: 支持预初始化窗口,避免显示时的初始化卡顿 ║
║ • 🛡️ 异常安全: 所有公共接口都有异常处理和状态检查 ║
║ • 📋 模板方法: 使用模板方法模式,框架控制流程,子类实现细节 ║
║ ║
║ 【使用方式】 ║
║ • 初始化: UIManager.CreateWindow() → Init(data) → ShowWindow(data) ║
║ • 数据更新: 调用 Refresh(data) 更新已显示窗口的内容 ║
║ • 隐藏显示: HideWindow() / ShowWindow() 控制窗口可见性 ║
║ • 销毁释放: CloseWindow() 完整销毁窗口和资源 ║
║ ║
║ 【子类实现指南 - 标记为 * 的虚方法】 ║
║ ║
║ 🔸 *OnBeforeInit(data) - 【不推荐】初始化预处理 ║
║ • 调用时机DoInit和OnInit之前 ║
║ • 实现用途:解析传入数据,设置初始状态 ║
║ • ⚠️ 不建议使用容易造成逻辑分散建议统一在OnInit中处理 ║
║ • 替代方案在OnInit()中处理所有初始化逻辑 ║
║ ║
║ 🔸 *OnInit() - 【推荐】UI组件初始化 ║
║ • 调用时机DoInit之后窗口显示之前 ║
║ • 实现用途初始化UI组件绑定事件设置默认值 ║
║ • 典型示例:按钮事件绑定,列表组件初始化,默认文本设置 ║
║ • 注意事项此时GameObject已创建可安全访问UI组件 ║
║ ║
║ 🔸 *AutoBindField() - 【自动】组件绑定 ║
║ • 调用时机DoInit过程中自动调用 ║
║ • 实现用途自动绑定UI组件到字段 ║
║ • 注意事项:通常由代码生成工具处理,不建议手动重写 ║
║ ║
║ 🔸 *OnBeforeShow(data) - 【可选】显示前验证 ║
║ • 调用时机DoShow和OnShow之前 ║
║ • 实现用途:显示前的条件检查和验证 ║
║ • 返回值返回false可以取消本次显示操作 ║
║ • 使用场景:权限检查,前置条件验证 ║
║ ║
║ 🔸 *OnShow(data) - 【不推荐】显示时配置 ║
║ • 调用时机窗口显示过程中SetActive之前 ║
║ • 实现用途:设置显示配置,处理显示参数 ║
║ • 典型示例:设置窗口模式,配置显示状态,处理显示参数 ║
║ • 职责边界负责显示配置不负责数据内容更新由Refresh处理
║ ║
║ 🔸 *OnAfterShow(data) - 【可选】显示后处理 ║
║ • 调用时机窗口完全显示后SetActive之后 ║
║ • 实现用途:启动后续逻辑,播放动画效果 ║
║ • 典型示例:播放入场动画,开始计时器,发送显示事件 ║
║ ║
║ 🔸 *OnUpdate(dt) - 【可选】逐帧更新 ║
║ • 调用时机:每帧调用(仅在窗口激活时) ║
║ • 实现用途:处理需要逐帧更新的逻辑 ║
║ • 典型示例:倒计时更新,进度条刷新,动画状态更新 ║
║ • 性能注意:避免重度计算,优先使用事件驱动 ║
║ ║
║ 🔸 *OnRefresh(data) - 【推荐】内容数据刷新 ║
║ • 调用时机外部调用Refresh(data)时触发 ║
║ • 实现用途:更新窗口显示内容,刷新数据绑定 ║
║ • 典型示例:刷新列表数据,更新文本内容,重新计算显示 ║
║ • 职责边界负责内容更新不负责显示配置由Show处理
║ • 调用规范只能通过Refresh(data)公共接口间接调用 ║
║ ║
║ 🔸 *OnHide() - 【可选】隐藏时清理 ║
║ • 调用时机:窗口隐藏或销毁时 ║
║ • 实现用途:暂停逻辑,保存状态,停止动画 ║
║ • 典型示例:暂停计时器,保存用户输入,停止音效播放 ║
║ ║
║ 🔸 *OnRelease() - 【重要】资源释放 ║
║ • 调用时机窗口销毁时OnHide之后 ║
║ • 实现用途:释放资源,取消订阅,清理引用 ║
║ • 典型示例取消事件监听释放Tween动画清理对象池断开网络连接 ║
║ • 重要性:防止内存泄漏的关键环节 ║
║ ║
║ 【关键约束和最佳实践】 ║
║ ║
║ 🚫 【禁止行为】 ║
║ • 不要重写公共接口方法Init, ShowWindow, Refresh, HideWindow等
║ • 不要直接调用OnXxx虚方法必须通过对应的公共接口 ║
║ • 不要在OnXxx方法中调用base.OnXxx()(基类实现为空) ║
║ ║
║ ✅ 【推荐做法】 ║
║ • 统一在OnInit中处理初始化避免使用OnBeforeInit ║
║ • 明确区分Show显示配置和Refresh内容更新的职责 ║
║ • 在OnRelease中完整清理资源防止内存泄漏 ║
║ • 优先使用事件驱动减少OnUpdate中的重度计算 ║
║ • 通过公共接口调用Refresh(data)、ShowWindow(data)等 ║
║ ║
║ 💡 【设计理念】 ║
║ 框架采用"模板方法模式",公共接口控制执行流程和异常处理, ║
║ 虚方法提供扩展点供子类实现具体业务逻辑,确保框架稳定性和扩展性的平衡。 ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*/
#region Lifecycle Management
/// <summary>
/// 初始化窗口
/// </summary>
/// <param name="data">初始化数据</param>
public void Init(object data = null)
{
try
{
OnBeforeInit(data);
if (isInited)
{
DebugUtil.LogWarning("window : {0} already inited", WindowName);
}
else
{
DoInit();
OnInit();
isInited = true;
}
}
catch (System.Exception e)
{
DebugUtil.LogError("window : {0} init window error: {1}", WindowName, e.Message);
}
}
/// <summary>
/// 显示窗口
/// </summary>
/// <param name="data">显示数据</param>
public void ShowWindow(object data = null)
{
try
{
if (_isShow)
{
DebugUtil.LogWarning("window : {0} is show, can't show again", WindowName);
}
if (!OnBeforeShow(data))
{
DebugUtil.LogError("window : {0} on before show return false", WindowName);
return;
}
DoShow();
OnShow(data);
if (gameObject is null)
{
DebugUtil.Log("UI已被销毁:{0}", WindowName);
}
else
{
gameObject.SetActive(true);
}
_isShow = true;
bool isShowOpenAnim = IsShowOpenAnim;
if (isShowOpenAnim)
{
AnimShow(() =>
{
OnAfterShow(data);
EventManager.Instance.Send(EventManager.EventName.UIOpen, WindowName);
}).Forget();
}
else
{
OnAfterShow(data);
EventManager.Instance.Send(EventManager.EventName.UIOpen, WindowName);
}
}
catch (System.Exception e)
{
DebugUtil.LogError("window : {0} show window error: {1}", WindowName, e.Message);
}
}
/// <summary>
/// 更新入口
/// </summary>
public void Update(float dt)
{
try
{
_notchScreenAdapter?.UpdateCheck();
OnUpdate(dt);
}
catch (System.Exception e)
{
DebugUtil.LogError("window : {0} update window error: {1}", WindowName, e.Message);
}
}
/// <summary>
/// 刷新窗口数据
/// </summary>
/// <param name="data">新数据</param>
public void Refresh(object data)
{
try
{
if (isInited)
{
OnRefresh(data);
}
else
{
DebugUtil.LogError("window : {0} not inited", WindowName);
}
}
catch (System.Exception e)
{
DebugUtil.LogError("window : {0} refresh with data error: {1}", WindowName, e.Message);
}
}
/// <summary>
/// 隐藏窗口
/// </summary>
public void HideWindow()
{
if (!_isShow)
{
DebugUtil.LogWarning("window : {0} is hide, can't hide again", WindowName);
}
try
{
if (UIManager.Instance.WindowInfos.TryGetValue(WindowName, out var windowInfo))
{
if (windowInfo.UIOpenType == UIOpenType.FullScreen)
{
if (!CameraManager.Instance.CheckMainCameraIsActive())
{
CameraManager.Instance.SetMainCameraActive(true);
}
}
}
OnHide();
gameObject?.SetActive(false);
_isShow = false;
EventManager.Instance.Send(EventManager.EventName.UIHide, WindowName);
}
catch (System.Exception e)
{
DebugUtil.LogError("window : {0} hide window error: {1}", WindowName, e.Message);
}
}
/// <summary>
/// 关闭窗口
/// </summary>
/// <param name="closeType">关闭类型</param>
public async void CloseWindow(UIWindowCloseType closeType)
{
try
{
IsClosing = true;
DebugUtil.LogG($"关闭UI:{WindowName}, closeType{closeType}");
if (UIManager.Instance.CheckWindowIsFullScreen(this))
{
if (_shutMainCamera)
{
CameraManager.Instance.SetMainCameraActive(true);
}
}
if (closeType == UIWindowCloseType.HideAndDestroy)
{
var last = UIManager.Instance.GetLast();
if (last == this)
{
DebugUtil.LogG($"PopWindow{this.WindowName}");
UIManager.Instance.PopWindow();
}
if (UIManager.Instance.UiWindowStack.Count > 0)
{
if (UIManager.Instance.CheckWindowIsFullScreen(this))
{
var window = UIManager.Instance.GetLastFullScreen();
if (window != null && window != this)
{
if (UIManager.Instance.CheckIsWindowInStack(this))
{
UIManager.Instance.ForceRemoveWindow(this);
DebugUtil.LogG($"UI出栈,{WindowName}");
}
}
for (int i = UIManager.Instance.UiWindowStack.Count - 1; i >= 0; i--)
{
var latestWindow = UIManager.Instance.UiWindowStack[i];
latestWindow.ShowWindow();
if (UIManager.Instance.CheckWindowIsFullScreen(latestWindow))
break;
}
}
else
{
UIManager.Instance.ForceRemoveWindow(this);
}
}
bool isShowCloseAnim = IsShowOpenAnim;
if (isShowCloseAnim)
{
UIManager.Instance.BlockUIForCloseOperation();
await AnimHide(() =>
{
DestroyWindow();
UIManager.Instance.RemoveWindow(mWindowName);
IsClosing = false;
});
}
else
{
UIManager.Instance.RemoveWindow(mWindowName);
DestroyWindow();
IsClosing = false;
}
}
else if (closeType == UIWindowCloseType.ForceDestroy)
{
// 强制清除所有界面时使用
DestroyWindow();
UIManager.Instance.RemoveWindow(mWindowName);
IsClosing = false;
}
}
catch (System.Exception e)
{
DebugUtil.LogError("window : {0} close window error: {1}", WindowName, e.Message);
}
}
#endregion
#region Internal Implementation
/// <summary>
/// 执行基础初始化
/// </summary>
private void DoInit()
{
_isShow = false;
_shutMainCamera = UIManager.Instance.IsShutMainCamera(WindowName);
IsShowOpenAnim = UIManager.Instance.GetIsAutoPlayAnim(WindowName);
AutoBindField();
InitScreenAdaption();
_TryBindBgClose();
}
/// <summary>
/// 执行显示逻辑
/// </summary>
private void DoShow()
{
if (UIManager.Instance.WindowInfos.TryGetValue(WindowName, out var windowInfo))
{
if (CameraManager.Instance.CheckMainCameraIsActive())
{
if (windowInfo.UIOpenType == UIOpenType.FullScreen)
{
// 特殊处理主界面
if (_shutMainCamera && WindowName != UINameConst.UI_MainPanel)
{
CameraManager.Instance.SetMainCameraActive(false);
}
}
}
}
}
/// <summary>
/// 销毁窗口
/// </summary>
private void DestroyWindow()
{
DebugUtil.LogG($"DestroyWindow:{WindowName}");
OnHide();
gameObject?.SetActive(false);
_isShow = false;
EventManager.Instance.Send(EventManager.EventName.UIHide, WindowName);
if (gameObject)
{
if (isInited)
{
OnRelease();
DoRelease();
isInited = false;
}
if (UIManager.Instance.UIWindowObjDic.ContainsKey(gameObject))
{
UIManager.Instance.UIWindowObjDic.Remove(gameObject);
}
UnityEngine.Object.Destroy(gameObject);
}
AssetManager.Instance.Unload(string.Format(UIManager.UI_PREFAB_PATH, mWindowName), true);
gameObject = null;
}
/// <summary>
/// 基础资源释放
/// </summary>
private void DoRelease()
{
EventManager.Instance.Send(EventManager.EventName.UIDestroy, WindowName);
UnLoad();
_openAnimSeq?.Kill();
_assetReferenceManager?.Dispose();
_assetReferenceManager = null;
_notchScreenAdapter = null;
_graphicRaycasterManager = null;
}
/// <summary>
/// 绑定背景关闭按钮
/// </summary>
private void _TryBindBgClose()
{
var trans = transform.Find("BgClose");
if (trans)
{
var btn = trans.GetComponent<Button>();
if (btn)
{
BindButton(btn, () =>
{
var clickAudio = btn.GetComponent<ClickAudio>();
if (clickAudio)
{
if (clickAudio.IsPlayAudio && !string.IsNullOrEmpty(clickAudio.AudioKey))
{
AudioManager.Instance.PlayAudio(clickAudio.AudioKey);
}
else
{
CommonUtilsFramework.PlayUICommonSound();
}
}
if (_onBgCloseFunc == null)
{
CloseWindow(UIWindowCloseType.HideAndDestroy);
return;
}
_onBgCloseFunc?.Invoke();
});
}
}
}
#endregion
#region Virtual Methods
// Init Phase
/// <summary>
/// 初始化前预处理不推荐使用建议在OnInit中处理
/// </summary>
[System.Obsolete("OnBeforeInit已废弃请使用OnInit方法替代。OnInit提供更清晰的初始化流程和更好的代码组织。")]
protected virtual void OnBeforeInit(object data = null){}
/// <summary>
/// UI初始化
/// </summary>
protected virtual void OnInit(){}
/// <summary>
/// 自动绑定字段
/// </summary>
protected virtual void AutoBindField(){}
// Show Phase
/// <summary>
/// 显示前检查
/// </summary>
protected virtual bool OnBeforeShow(object data)
{
return true;
}
/// <summary>
/// 显示时处理
/// </summary>
[System.Obsolete("OnShow已废弃请使用OnAfterShow方法替代。OnAfterShow在窗口完全显示后调用提供更稳定的显示时机。")]
protected virtual void OnShow(object data){}
/// <summary>
/// 显示后处理
/// </summary>
protected virtual void OnAfterShow(object data){}
/// <summary>
/// 每帧更新
/// </summary>
protected virtual void OnUpdate(float dt){}
// Refresh Phase
/// <summary>
/// 数据刷新
/// </summary>
protected virtual void OnRefresh(object data) {}
// Hide Phase
/// <summary>
/// 隐藏时处理
/// </summary>
protected virtual void OnHide(){}
// Release Phase
/// <summary>
/// 资源释放
/// </summary>
protected virtual void OnRelease(){}
#endregion
}