597 lines
40 KiB
C#
597 lines
40 KiB
C#
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
|
||
}
|