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

668 lines
46 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. 【Interaction 阶段】- 用户交互 ║
║ UIWindow.Back(data) // 处理返回键(公共接口,不可重写) ║
║ └─ *OnBack(data) // 返回键处理逻辑(虚方法,可重写) ║
║ └─ BackStrategy.HandleBack() // 默认使用策略模式 ║
║ ├─ None: 不响应,向下传递 ║
║ ├─ CloseSelf: 关闭当前窗口 ║
║ ├─ DoNothing: 拦截不操作 ║
║ └─ ShowExitConfirm: 显示退出确认 ║
║ ║
║ 7. 【Close 阶段】- 关闭 ║
║ UIWindow.CloseWindow(destroy) // 关闭窗口(公共接口,不可重写) ║
║ ├─ *OnHide() // 关闭回调 ║
║ ├─ SetActive(false) // 隐藏GameObject ║
║ └─ 发送UI隐藏事件 // EventManager.Send ║
║ ║
║ 8. 【Destroy 阶段】- 销毁 ║
║ UIWindow.CloseWindow(true) // 关闭并销毁窗口 ║
║ └─ DoDestroy() // 内部销毁逻辑 ║
║ ├─ *OnHide() // 关闭回调(如果还未调用) ║
║ ├─ *OnRelease() // 释放资源,清理引用 ║
║ ├─ DoRelease() // 基础资源释放 ║
║ ├─ Destroy(gameObject) // 销毁GameObject ║
║ └─ 卸载预制体资源 // AssetManager.Unload ║
║ ║
║ 【架构设计原则】 ║
║ • 🔒 接口保护: 公共接口方法不可重写,确保框架逻辑完整性 ║
║ • 🎯 职责分离: Init和Show独立调用Show负责显示配置Refresh负责内容更新 ║
║ • ⚡ 性能优化: 支持预初始化窗口,避免显示时的初始化卡顿 ║
║ • 🛡️ 异常安全: 所有公共接口都有异常处理和状态检查 ║
║ • 📋 模板方法: 使用模板方法模式,框架控制流程,子类实现细节 ║
║ ║
║ 【使用方式】 ║
║ • 初始化: UIManager.CreateWindow() → Init(data) → ShowWindow(data) ║
║ • 数据更新: 调用 Refresh(data) 更新已显示窗口的内容 ║
║ • 返回处理: 按下ESC/返回键 → Back() → OnBack() → BackStrategy处理 ║
║ • 关闭显示: CloseWindow(false) 关闭窗口(保留内存) / ShowWindow() 显示窗口 ║
║ • 销毁释放: CloseWindow(destroy: true) 完整销毁窗口和资源 ║
║ ║
║ 【子类实现指南 - 标记为 * 的虚方法】 ║
║ ║
║ 🔸 *OnBeforeInit(data) - 【不推荐】初始化预处理 ║
║ • 调用时机DoInit和OnInit之前 ║
║ • 实现用途:解析传入数据,设置初始状态 ║
║ • ⚠️ 不建议使用容易造成逻辑分散建议统一在OnInit中处理 ║
║ • 替代方案在OnInit()中处理所有初始化逻辑 ║
║ ║
║ 🔸 *OnInit() - 【推荐】UI组件初始化 ║
║ • 调用时机DoInit之后窗口显示之前 ║
║ • 实现用途初始化UI组件绑定事件设置默认值 ║
║ • 典型示例:按钮事件绑定,列表组件初始化,默认文本设置 ║
║ • 注意事项此时GameObject已创建可安全访问UI组件 ║
║ ║
║ 🔸 *AutoBindField() - 【自动】组件绑定 ║
║ • 调用时机DoInit过程中自动调用 ║
║ • 实现用途自动绑定UI组件到字段 ║
║ • 注意事项:通常由代码生成工具处理,不建议手动重写 ║
║ ║
║ 🔸 *OnBeforeShow(data) - 【可选】显示前验证 ║
║ • 调用时机DoShow和OnShow之前 ║
║ • 实现用途:显示前的条件检查和验证 ║
║ • 返回值返回false可以取消本次显示操作 ║
║ • 使用场景:权限检查,前置条件验证 ║
║ ║
║ 🔸 *OnShow(data) - 【不推荐】显示时配置 ║
║ • 调用时机窗口显示过程中SetActive之前 ║
║ • 实现用途:设置显示配置,处理显示参数 ║
║ • 典型示例:设置窗口模式,配置显示状态,处理显示参数 ║
║ • ⚠️ 已废弃建议使用OnAfterShow替代 ║
║ ║
║ 🔸 *OnAfterShow(data) - 【推荐】显示后处理 ║
║ • 调用时机窗口完全显示后SetActive之后 ║
║ • 实现用途:启动后续逻辑,播放动画效果 ║
║ • 典型示例:播放入场动画,开始计时器,发送显示事件 ║
║ ║
║ 🔸 *OnUpdate(dt) - 【可选】逐帧更新 ║
║ • 调用时机:每帧调用(仅在窗口激活时) ║
║ • 实现用途:处理需要逐帧更新的逻辑 ║
║ • 典型示例:倒计时更新,进度条刷新,动画状态更新 ║
║ • 性能注意:避免重度计算,优先使用事件驱动 ║
║ ║
║ 🔸 *OnRefresh(data) - 【推荐】内容数据刷新 ║
║ • 调用时机外部调用Refresh(data)时触发 ║
║ • 实现用途:更新窗口显示内容,刷新数据绑定 ║
║ • 典型示例:刷新列表数据,更新文本内容,重新计算显示 ║
║ • 职责边界负责内容更新不负责显示配置由Show处理
║ • 调用规范只能通过Refresh(data)公共接口间接调用 ║
║ ║
║ 🔸 *OnBack(data) - 【可选】返回键处理 ║
║ • 调用时机Android返回键或ESC键按下时由Back()公共接口调用 ║
║ • 实现用途:自定义返回键行为 ║
║ • 返回值true表示已处理false表示未处理继续向下层窗口传递
║ • 典型示例:显示确认对话框,保存状态后关闭,自定义返回逻辑 ║
║ • 默认行为通过BackStrategy配置None/CloseSelf/DoNothing/ShowExitConfirm
║ • 注意事项如果重写可以选择调用base.OnBack()使用默认策略,或完全自定义 ║
║ • 异常处理由Back()公共接口统一处理异常 ║
║ ║
║ 🔸 *OnHide() - 【可选】隐藏时清理 ║
║ • 调用时机:窗口隐藏或销毁时 ║
║ • 实现用途:暂停逻辑,保存状态,停止动画 ║
║ • 典型示例:暂停计时器,保存用户输入,停止音效播放 ║
║ ║
║ 🔸 *OnRelease() - 【重要】资源释放 ║
║ • 调用时机窗口销毁时OnHide之后 ║
║ • 实现用途:释放资源,取消订阅,清理引用 ║
║ • 典型示例取消事件监听释放Tween动画清理对象池断开网络连接 ║
║ • 重要性:防止内存泄漏的关键环节 ║
║ ║
║ 【关键约束和最佳实践】 ║
║ ║
║ 🚫 【禁止行为】 ║
║ • 不要重写公共接口方法Init, ShowWindow, Update, Refresh, Back, CloseWindow等
║ • 不要直接调用OnXxx虚方法必须通过对应的公共接口 ║
║ • 不要在OnXxx方法中调用base.OnXxx()(基类实现通常为空或默认逻辑) ║
║ ║
║ ✅ 【推荐做法】 ║
║ • 统一在OnInit中处理初始化避免使用OnBeforeInit ║
║ • 明确区分Show显示配置和Refresh内容更新的职责 ║
║ • 在OnRelease中完整清理资源防止内存泄漏 ║
║ • 优先使用事件驱动减少OnUpdate中的重度计算 ║
║ • 通过公共接口调用Init(data), ShowWindow(data), Refresh(data), Back(data) 等 ║
║ • 返回键行为优先通过BackStrategy配置只在特殊需求时重写OnBack ║
║ • 重写OnBack时优先考虑调用base.OnBack()保留默认策略行为 ║
║ ║
║ 💡 【设计理念】 ║
║ 框架采用"模板方法模式",公共接口控制执行流程和异常处理, ║
║ 虚方法提供扩展点供子类实现具体业务逻辑,确保框架稳定性和扩展性的平衡。 ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*/
#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}\n===Stack===\n{2}\n======", WindowName, e.Message, e.StackTrace);
}
}
/// <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);
}
// 🎯 在显示前截图(如果需要)
CaptureScreenshotIfNeeded();
// 🎯 自动设置模糊背景(如果需要)
SetupBlurBackgroundIfNeeded();
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;
OnAfterShow(data);
bool isShowOpenAnim = IsShowOpenAnim;
if (isShowOpenAnim)
{
AnimShow(() =>
{
//TODO
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}\n===Stack===\n{2}\n======", WindowName, e.Message, e.StackTrace);
}
}
/// <summary>
/// 在显示前截图(如果窗口配置需要截图)
/// </summary>
private void CaptureScreenshotIfNeeded()
{
var config = UIManager.Instance.GetWindowInfo(WindowName);
if (config.NeedScreenshot)
{
ScreenshotManager.Instance.CaptureScreenshot();
}
}
/// <summary>
/// 自动设置模糊背景(如果窗口配置需要截图)
/// </summary>
private void SetupBlurBackgroundIfNeeded()
{
var config = UIManager.Instance.GetWindowInfo(WindowName);
if (!config.NeedScreenshot) return;
// 查找约定的 GaussianBlurMask 节点
var blurTransform = transform.Find("GaussianBlurMask");
if (blurTransform != null)
{
var blurMask = blurTransform.GetComponent<UnityEngine.UI.RawImage>();
if (blurMask != null)
{
blurMask.texture = ScreenshotManager.Instance.GetScreenTexture2D();
blurMask.gameObject.SetActive(true);
}
}
}
/// <summary>
/// 更新入口
/// </summary>
public void Update(float dt)
{
try
{
_notchScreenAdapter?.UpdateCheck();
OnUpdate(dt);
}
catch (System.Exception e)
{
DebugUtil.LogError("window : {0} update window error: {1}\n===Stack===\n{2}\n======", WindowName, e.Message, e.StackTrace);
}
}
/// <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}\n===Stack===\n{2}\n======", WindowName, e.Message, e.StackTrace);
}
}
/// <summary>
/// 关闭窗口
///
/// 参数说明:
/// - destroy = false: 关闭但保留在内存中(快速重开,如背包、角色面板)
/// - destroy = true: 关闭并销毁,释放资源(不再需要的窗口)
///
/// 行为:
/// 1. 恢复主相机状态
/// 2. 处理导航栈移除(由栈管理器根据配置决定)
/// 3. 恢复下层窗口可见性(由可见性管理器根据配置决定)
/// 4. 调用 OnHide() 生命周期
/// 5. 隐藏窗口渲染GameObject.SetActive(false)
/// 6. 修改逻辑状态_isShow = false
/// 7. 发送 UIHide 事件
/// 8. 播放关闭动画(如果配置了)
/// 9. 如果 destroy = true销毁 GameObject 并释放资源
///
/// 注意:
/// - 如果窗口已关闭_isShow = false且 destroy = false跳过
/// - 如果窗口已关闭_isShow = false且 destroy = true跳过关闭逻辑但仍执行销毁
/// </summary>
/// <param name="destroy">是否销毁窗口</param>
public async void CloseWindow(bool destroy)
{
try
{
// 如果窗口已关闭
if (!_isShow)
{
// 如果不需要销毁,直接返回
if (!destroy)
{
DebugUtil.LogWarning("window : {0} is already closed", WindowName);
return;
}
// 如果需要销毁,跳过关闭逻辑,直接执行销毁
DebugUtil.LogG($"窗口已关闭直接销毁UI:{WindowName}");
IsClosing = true;
UIManager.Instance.RemoveWindow(mWindowName);
DoDestroy();
IsClosing = false;
return;
}
IsClosing = true;
// 1. 恢复主相机
RestoreMainCamera();
// 2. 获取窗口配置
var config = UIManager.Instance.GetWindowInfo(WindowName);
// 3. 可见性管理(由 UIWindowVisibilityManager 根据配置决定是否处理)
UIWindowVisibilityManager.Instance.HandleVisibilityOnClose(this, config.VisibilityStrategy);
// 4. 隐藏逻辑
OnHide();
gameObject?.SetActive(false);
_isShow = false;
EventManager.Instance.Send(EventManager.EventName.UIHide, WindowName);
// 5. 播放关闭动画
bool isShowCloseAnim = IsShowOpenAnim;
if (isShowCloseAnim)
{
UIManager.Instance.BlockUIForCloseOperation();
await AnimHide(() =>
{
// 动画播放完毕后的回调
if (destroy)
{
DoDestroy();
UIManager.Instance.RemoveWindow(mWindowName);
}
IsClosing = false;
});
}
else
{
// 7. 如果需要销毁(无动画情况)
if (destroy)
{
DoDestroy();
UIManager.Instance.RemoveWindow(mWindowName);
}
IsClosing = false;
}
}
catch (System.Exception e)
{
DebugUtil.LogError("window : {0} close window error: {1}\n===Stack===\n{2}\n======", WindowName, e.Message, e.StackTrace);
}
}
#endregion
#region User Interaction
/// <summary>
/// 处理返回键(公共接口,不可重写)
/// 框架会按照从上到下的顺序调用所有可见窗口的Back()方法,
/// 直到某个窗口处理并返回true为止
/// </summary>
/// <returns>是否成功处理返回键true表示已处理false表示未处理继续向下传递</returns>
public async UniTask<bool> Back()
{
try
{
return await OnBack();
}
catch (System.Exception e)
{
DebugUtil.LogError("window : {0} back error: {1}\n===Stack===\n{2}\n======", WindowName, e.Message, e.StackTrace);
return false;
}
}
/// <summary>
/// 返回键处理(虚方法,子类可重写)
/// 默认通过返回策略模式处理
/// </summary>
/// <returns>是否成功处理返回键true表示已处理false表示未处理继续向下传递</returns>
protected virtual async UniTask<bool> OnBack()
{
if (!GraphicRaycasterManager.IsEnableGraphicRaycaster)
return true;
// 使用策略模式处理返回键
if (BackStrategy != null)
{
return await BackStrategy.HandleBack(this);
}
return false;
}
#endregion
#region Internal Implementation
/// <summary>
/// 恢复主相机状态
/// </summary>
private void RestoreMainCamera()
{
var config = UIManager.Instance.GetWindowInfo(WindowName);
if (config.VisibilityStrategy == UIVisibilityStrategy.Occlusion && _shutMainCamera)
{
if (!CameraManager.Instance.CheckMainCameraIsActive())
{
CameraManager.Instance.SetMainCameraActive(true);
}
}
}
/// <summary>
/// 执行基础初始化
/// </summary>
private void DoInit()
{
_isShow = false;
var metaInfo = UIManager.Instance.GetWindowInfo(WindowName);
_shutMainCamera = metaInfo.ShutMainCamera;
IsShowOpenAnim = metaInfo.IsAutoAnim;
BackStrategy = UIBackStrategyFactory.CreateStrategy(metaInfo.BackStrategyType);
AutoBindField();
InitScreenAdaption();
BindDefaultCloseButtons();
BindDefaultHomeButtons();
BindDefaultBackButtons();
}
/// <summary>
/// 执行显示逻辑
/// </summary>
private void DoShow()
{
if (UIManager.Instance.WindowInfos.TryGetValue(WindowName, out var windowInfo))
{
if (CameraManager.Instance.CheckMainCameraIsActive())
{
if (windowInfo.VisibilityStrategy == UIVisibilityStrategy.Occlusion)
{
// 特殊处理主界面
if (_shutMainCamera)
{
CameraManager.Instance.SetMainCameraActive(false);
}
}
}
}
}
/// <summary>
/// 执行销毁逻辑(内部方法)
/// </summary>
private void DoDestroy()
{
DebugUtil.LogG($"DoDestroy:{WindowName}");
// 如果窗口还在显示状态,先调用 OnHide
if (_isShow)
{
OnHide();
_isShow = false;
}
// 确保 GameObject 隐藏
if (gameObject != null)
{
gameObject.SetActive(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;
}
#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
}