feat(ui): 添加UI页面生命周期和路由管理相关接口及实现

- 定义了IPageBehavior接口,提供UI页面的生命周期方法如OnEnter、OnExit、OnPause、OnResume等
- 创建了IUiFactory接口用于创建UI页面实例,以及IUiPage接口定义页面基本操作
- 添加了IUiPageEnterParam接口用于定义页面跳转参数数据结构
- 实现了IUiRouter接口提供页面栈管理功能,支持Push、Pop、Replace、Clear等操作
- 创建了UI切换处理器相关接口和实现,包括IUiTransitionHandler和UiTransitionPipeline
- 添加了UI切换事件系统,支持BeforeChange和AfterChange两个执行阶段
- 实现了日志记录处理器LoggingTransitionHandler用于记录UI切换信息
- 定义了多种UI切换策略枚举如UiTransitionPolicy、UiTransitionType等
- 提供了UI注册表接口用于管理UI实例的注册和获取功能
This commit is contained in:
GeWuYou 2026-01-15 08:44:56 +08:00
parent 14894814a5
commit 8fd7e2e952
20 changed files with 1061 additions and 0 deletions

View File

@ -0,0 +1,27 @@
using System;
namespace GFramework.Game.Abstractions.enums;
/// <summary>
/// UI切换阶段枚举定义UI切换过程中的不同阶段
/// </summary>
[Flags]
public enum UITransitionPhases
{
/// <summary>
/// UI切换前阶段在此阶段执行的Handler可以阻塞UI切换流程
/// 适用于:淡入淡出动画、用户确认对话框、数据预加载等需要等待完成的操作
/// </summary>
BeforeChange = 1,
/// <summary>
/// UI切换后阶段在此阶段执行的Handler不阻塞UI切换流程
/// 适用于:播放音效、日志记录、统计数据收集等后台操作
/// </summary>
AfterChange = 2,
/// <summary>
/// 所有阶段Handler将在BeforeChange和AfterChange阶段都执行
/// </summary>
All = BeforeChange | AfterChange
}

View File

@ -0,0 +1,32 @@
namespace GFramework.Game.Abstractions.enums;
/// <summary>
/// UI页面过渡策略枚举
/// 定义了UI页面在出栈时的不同处理方式
/// </summary>
public enum UiTransitionPolicy
{
/// <summary>
/// 出栈即销毁(一次性页面)
/// 页面从栈中移除时会完全销毁实例
/// </summary>
Destroy,
/// <summary>
/// 出栈隐藏,保留实例
/// 页面从栈中移除时仅隐藏显示,保留实例以便后续重用
/// </summary>
Hide,
/// <summary>
/// 覆盖显示(不影响下层页面)
/// 当前页面覆盖在其他页面之上显示,不影响下层页面的状态
/// </summary>
Overlay,
/// <summary>
/// 独占显示(下层页面 Pause + Hide
/// 当前页面独占显示区域,下层页面会被暂停并隐藏
/// </summary>
Exclusive
}

View File

@ -0,0 +1,27 @@
namespace GFramework.Game.Abstractions.enums;
/// <summary>
/// UI切换类型枚举定义不同的UI切换操作类型
/// </summary>
public enum UiTransitionType
{
/// <summary>
/// 压入新页面到栈顶
/// </summary>
Push,
/// <summary>
/// 弹出栈顶页面
/// </summary>
Pop,
/// <summary>
/// 替换当前页面
/// </summary>
Replace,
/// <summary>
/// 清空所有页面
/// </summary>
Clear,
}

View File

@ -0,0 +1,49 @@
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI页面接口定义了UI页面的生命周期方法
/// </summary>
public interface IPageBehavior
{
/// <summary>
/// 获取页面视图对象
/// </summary>
/// <returns>页面视图实例</returns>
object View { get; }
/// <summary>
/// 获取页面是否处于活动状态
/// </summary>
bool IsAlive { get; }
/// <summary>
/// 页面进入时调用的方法
/// </summary>
/// <param name="param">页面进入时传递的参数,可为空</param>
void OnEnter(IUiPageEnterParam? param);
/// <summary>
/// 页面退出时调用的方法
/// </summary>
void OnExit();
/// <summary>
/// 页面暂停时调用的方法
/// </summary>
void OnPause();
/// <summary>
/// 页面恢复时调用的方法
/// </summary>
void OnResume();
/// <summary>
/// 页面被覆盖时调用(不销毁)
/// </summary>
void OnHide();
/// <summary>
/// 页面重新显示时调用的方法
/// </summary>
void OnShow();
}

View File

@ -0,0 +1,16 @@
using GFramework.Core.Abstractions.utility;
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI工厂接口用于创建UI页面实例
/// </summary>
public interface IUiFactory : IContextUtility
{
/// <summary>
/// 根据UI键值创建对应的UI页面实例
/// </summary>
/// <param name="uiKey">UI标识键用于确定要创建的具体UI页面类型</param>
/// <returns>创建的UI页面实例实现IUiPage接口</returns>
IPageBehavior Create(string uiKey);
}

View File

@ -0,0 +1,39 @@
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI页面生命周期接口
/// 定义了UI页面的各种状态转换方法用于管理UI页面的进入、退出、暂停、恢复、显示和隐藏等生命周期事件
/// </summary>
public interface IUiPage
{
/// <summary>
/// 页面进入时调用的方法
/// </summary>
/// <param name="param">页面进入参数,可能为空</param>
void OnEnter(IUiPageEnterParam? param);
/// <summary>
/// 页面退出时调用的方法
/// </summary>
void OnExit();
/// <summary>
/// 页面暂停时调用的方法
/// </summary>
void OnPause();
/// <summary>
/// 页面恢复时调用的方法
/// </summary>
void OnResume();
/// <summary>
/// 页面显示时调用的方法
/// </summary>
void OnShow();
/// <summary>
/// 页面隐藏时调用的方法
/// </summary>
void OnHide();
}

View File

@ -0,0 +1,7 @@
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI页面进入参数接口
/// 该接口用于定义UI页面跳转时传递的参数数据结构
/// </summary>
public interface IUiPageEnterParam;

View File

@ -0,0 +1,13 @@
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI页面提供者接口用于创建UI页面实例
/// </summary>
public interface IUiPageProvider
{
/// <summary>
/// 获取UI页面实例
/// </summary>
/// <returns>UI页面实例</returns>
IPageBehavior GetPage();
}

View File

@ -0,0 +1,17 @@
using GFramework.Core.Abstractions.utility;
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI注册表接口用于根据UI键获取对应的UI实例
/// </summary>
/// <typeparam name="T">UI实例的类型参数使用协变修饰符out</typeparam>
public interface IUiRegistry<out T> : IUtility
{
/// <summary>
/// 根据指定的UI键获取对应的UI实例
/// </summary>
/// <param name="uiKey">UI的唯一标识键</param>
/// <returns>与指定键关联的UI实例</returns>
T Get(string uiKey);
}

View File

@ -0,0 +1,19 @@
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI根节点接口定义了UI页面容器的基本操作
/// </summary>
public interface IUiRoot
{
/// <summary>
/// 向UI根节点添加子页面
/// </summary>
/// <param name="child">要添加的UI页面子节点</param>
void AddUiPage(IPageBehavior child);
/// <summary>
/// 从UI根节点移除子页面
/// </summary>
/// <param name="child">要移除的UI页面子节点</param>
void RemoveUiPage(IPageBehavior child);
}

View File

@ -0,0 +1,64 @@
using GFramework.Core.Abstractions.system;
using GFramework.Game.Abstractions.enums;
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI路由管理器接口用于管理UI界面的导航和切换操作
/// </summary>
public interface IUiRouter : ISystem
{
/// <summary>
/// 绑定UI根节点
/// </summary>
/// <param name="root">UI根节点接口实例</param>
void BindRoot(IUiRoot root);
/// <summary>
/// 将指定的UI界面压入路由栈显示新的UI界面
/// </summary>
/// <param name="uiKey">UI界面的唯一标识符</param>
/// <param name="param">进入界面的参数,可为空</param>
/// <param name="policy">界面切换策略默认为Exclusive独占</param>
void Push(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive);
/// <summary>
/// 弹出路由栈顶的UI界面返回到上一个界面
/// </summary>
/// <param name="policy">界面弹出策略默认为Destroy销毁</param>
void Pop(UiPopPolicy policy = UiPopPolicy.Destroy);
/// <summary>
/// 替换当前UI界面为指定的新界面
/// </summary>
/// <param name="uiKey">新UI界面的唯一标识符</param>
/// <param name="param">进入界面的参数,可为空</param>
/// <param name="popPolicy">界面弹出策略,默认为销毁当前界面</param>
/// <param name="pushPolicy">界面过渡策略,默认为独占模式</param>
void Replace(
string uiKey,
IUiPageEnterParam? param = null,
UiPopPolicy popPolicy = UiPopPolicy.Destroy,
UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive
);
/// <summary>
/// 清空所有UI界面重置路由状态
/// </summary>
void Clear();
/// <summary>
/// 注册UI切换处理器
/// </summary>
/// <param name="handler">处理器实例</param>
/// <param name="options">执行选项</param>
void RegisterHandler(IUiTransitionHandler handler, UiTransitionHandlerOptions? options = null);
/// <summary>
/// 注销UI切换处理器
/// </summary>
/// <param name="handler">处理器实例</param>
void UnregisterHandler(IUiTransitionHandler handler);
}

View File

@ -0,0 +1,39 @@
using System.Threading;
using System.Threading.Tasks;
using GFramework.Game.Abstractions.enums;
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI切换处理器接口定义UI切换扩展点的处理逻辑
/// </summary>
public interface IUiTransitionHandler
{
/// <summary>
/// 处理器优先级,数值越小越先执行
/// </summary>
int Priority { get; }
/// <summary>
/// 处理器适用的阶段,默认为所有阶段
/// 可以使用Flags枚举指定多个阶段
/// </summary>
UITransitionPhases Phases { get; }
/// <summary>
/// 判断是否应该处理当前事件
/// 可以根据事件类型、UI key等信息进行条件过滤
/// </summary>
/// <param name="event">UI切换事件</param>
/// <param name="phases">当前阶段</param>
/// <returns>是否处理</returns>
bool ShouldHandle(UiTransitionEvent @event, UITransitionPhases phases);
/// <summary>
/// 处理UI切换事件
/// </summary>
/// <param name="event">UI切换事件</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>异步任务</returns>
Task HandleAsync(UiTransitionEvent @event, CancellationToken cancellationToken);
}

View File

@ -0,0 +1,16 @@
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// 可写的UI注册表接口
/// </summary>
/// <typeparam name="T">UI实例的类型参数</typeparam>
public interface IWritableUiRegistry<T> : IUiRegistry<T> where T : class
{
/// <summary>
/// 注册UI实例到注册表
/// </summary>
/// <param name="key">UI的唯一标识键</param>
/// <param name="scene">要注册的UI实例</param>
/// <returns>当前注册表实例</returns>
IWritableUiRegistry<T> Register(string key, T scene);
}

View File

@ -0,0 +1,17 @@
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// 定义UI弹窗的关闭策略枚举
/// </summary>
public enum UiPopPolicy
{
/// <summary>
/// 销毁模式关闭时完全销毁UI对象
/// </summary>
Destroy,
/// <summary>
/// 隐藏模式关闭时仅隐藏UI对象保留实例
/// </summary>
Hide
}

View File

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using GFramework.Game.Abstractions.enums;
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI切换事件包含UI切换过程中的上下文信息
/// </summary>
public sealed class UiTransitionEvent
{
/// <summary>
/// 用户自定义数据字典用于Handler之间传递数据
/// </summary>
private readonly Dictionary<string, object> _context = new(StringComparer.Ordinal);
/// <summary>
/// 源UI的标识符切换前的UI key
/// </summary>
public string FromUiKey { get; init; } = string.Empty;
/// <summary>
/// 目标UI的标识符切换后的UI key
/// </summary>
public string ToUiKey { get; init; } = string.Empty;
/// <summary>
/// UI切换类型
/// </summary>
public UiTransitionType TransitionType { get; init; }
/// <summary>
/// UI切换策略
/// </summary>
public UiTransitionPolicy Policy { get; init; }
/// <summary>
/// UI进入参数
/// </summary>
public IUiPageEnterParam? EnterParam { get; init; }
/// <summary>
/// 获取用户自定义数据
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="key">数据键</param>
/// <param name="defaultValue">默认值(当键不存在或类型不匹配时返回)</param>
/// <returns>用户数据</returns>
public T Get<T>(string key, T defaultValue = default!)
{
if (_context.TryGetValue(key, out var obj) && obj is T value)
return value;
return defaultValue;
}
/// <summary>
/// 尝试获取用户自定义数据
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="key">数据键</param>
/// <param name="value">输出值</param>
/// <returns>是否成功获取</returns>
public bool TryGet<T>(string key, out T value)
{
if (_context.TryGetValue(key, out var obj) && obj is T t)
{
value = t;
return true;
}
value = default!;
return false;
}
/// <summary>
/// 设置用户自定义数据
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="key">数据键</param>
/// <param name="value">数据值</param>
public void Set<T>(string key, T value)
{
_context[key] = value!;
}
/// <summary>
/// 检查是否存在指定的用户数据键
/// </summary>
/// <param name="key">数据键</param>
/// <returns>是否存在</returns>
public bool Has(string key)
{
return _context.ContainsKey(key);
}
/// <summary>
/// 移除指定的用户数据
/// </summary>
/// <param name="key">数据键</param>
/// <returns>是否成功移除</returns>
public bool Remove(string key)
{
return _context.Remove(key);
}
}

View File

@ -0,0 +1,6 @@
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI切换处理器执行选项
/// </summary>
public record UiTransitionHandlerOptions(int TimeoutMs = 0, bool ContinueOnError = true);

View File

@ -0,0 +1,319 @@
using GFramework.Core.Abstractions.logging;
using GFramework.Core.extensions;
using GFramework.Core.logging;
using GFramework.Core.system;
using GFramework.Game.Abstractions.enums;
using GFramework.Game.Abstractions.ui;
namespace GFramework.Game.ui;
/// <summary>
/// UI路由类提供页面栈管理功能
/// </summary>
public abstract class UiRouterBase : AbstractSystem, IUiRouter
{
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("UiRouterBase");
/// <summary>
/// UI切换处理器管道
/// </summary>
private readonly UiTransitionPipeline _pipeline = new();
/// <summary>
/// 页面栈用于管理UI页面的显示顺序
/// </summary>
private readonly Stack<IPageBehavior> _stack = new();
/// <summary>
/// UI工厂实例用于创建UI相关的对象
/// </summary>
private IUiFactory _factory = null!;
private IUiRoot _uiRoot = null!;
/// <summary>
/// 注册UI切换处理器
/// </summary>
/// <param name="handler">处理器实例</param>
/// <param name="options">执行选项</param>
public void RegisterHandler(IUiTransitionHandler handler, UiTransitionHandlerOptions? options = null)
{
_pipeline.RegisterHandler(handler, options);
}
/// <summary>
/// 注销UI切换处理器
/// </summary>
/// <param name="handler">处理器实例</param>
public void UnregisterHandler(IUiTransitionHandler handler)
{
_pipeline.UnregisterHandler(handler);
}
/// <summary>
/// 绑定UI根节点
/// </summary>
public void BindRoot(IUiRoot root)
{
_uiRoot = root;
Log.Debug("Bind UI Root: {0}", root.GetType().Name);
}
/// <summary>
/// 将指定UI页面压入栈顶并显示
/// </summary>
/// <param name="uiKey">UI页面标识符</param>
/// <param name="param">页面进入参数,可为空</param>
/// <param name="policy">页面切换策略</param>
public void Push(
string uiKey,
IUiPageEnterParam? param = null,
UiTransitionPolicy policy = UiTransitionPolicy.Exclusive
)
{
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
Log.Debug(
"Push UI Page: key={0}, policy={1}, stackBefore={2}",
uiKey, policy, _stack.Count
);
BeforeChange(@event);
DoPushInternal(uiKey, param, policy);
AfterChange(@event);
}
/// <summary>
/// 弹出栈顶页面并根据策略处理页面
/// </summary>
/// <param name="policy">弹出策略,默认为销毁策略</param>
public void Pop(UiPopPolicy policy = UiPopPolicy.Destroy)
{
if (_stack.Count == 0)
{
Log.Debug("Pop ignored: stack is empty");
return;
}
var nextUiKey = _stack.Count > 1
? _stack.ElementAt(1).View.GetType().Name
: string.Empty;
var @event = CreateEvent(nextUiKey, UiTransitionType.Pop);
BeforeChange(@event);
DoPopInternal(policy);
AfterChange(@event);
}
/// <summary>
/// 替换当前所有页面为新页面
/// </summary>
/// <param name="uiKey">新UI页面标识符</param>
/// <param name="param">页面进入参数,可为空</param>
/// <param name="popPolicy">弹出页面时的销毁策略,默认为销毁</param>
/// <param name="pushPolicy">推入页面时的过渡策略,默认为独占</param>
public void Replace(
string uiKey,
IUiPageEnterParam? param = null,
UiPopPolicy popPolicy = UiPopPolicy.Destroy,
UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive
)
{
var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param);
Log.Debug(
"Replace UI Stack with page: key={0}, popPolicy={1}, pushPolicy={2}",
uiKey, popPolicy, pushPolicy
);
BeforeChange(@event);
// 使用内部方法避免触发额外的Pipeline
DoClearInternal(popPolicy);
DoPushInternal(uiKey, param, pushPolicy);
AfterChange(@event);
}
/// <summary>
/// 清空所有页面栈中的页面
/// </summary>
public void Clear()
{
var @event = CreateEvent(string.Empty, UiTransitionType.Clear);
Log.Debug("Clear UI Stack, stackCount={0}", _stack.Count);
BeforeChange(@event);
// 使用内部方法避免触发额外的Pipeline
DoClearInternal(UiPopPolicy.Destroy);
AfterChange(@event);
}
/// <summary>
/// 初始化方法在页面初始化时获取UI工厂实例
/// </summary>
protected override void OnInit()
{
_factory = this.GetUtility<IUiFactory>()!;
Log.Debug("UiRouterBase initialized. Factory={0}", _factory.GetType().Name);
RegisterHandlers();
}
/// <summary>
/// 注册默认的UI切换处理器
/// </summary>
protected abstract void RegisterHandlers();
/// <summary>
/// 获取当前栈顶UI的key
/// </summary>
/// <returns>当前UI key如果栈为空则返回空字符串</returns>
private string GetCurrentUiKey()
{
if (_stack.Count == 0)
return string.Empty;
var page = _stack.Peek();
return page.View.GetType().Name;
}
/// <summary>
/// 创建UI切换事件
/// </summary>
private UiTransitionEvent CreateEvent(
string toUiKey,
UiTransitionType type,
UiTransitionPolicy? policy = null,
IUiPageEnterParam? param = null
)
{
return new UiTransitionEvent
{
FromUiKey = GetCurrentUiKey(),
ToUiKey = toUiKey,
TransitionType = type,
Policy = policy ?? UiTransitionPolicy.Exclusive,
EnterParam = param
};
}
/// <summary>
/// 执行UI切换前的Handler阻塞
/// </summary>
private void BeforeChange(UiTransitionEvent @event)
{
Log.Debug("BeforeChange phases started: {0}", @event.TransitionType);
_pipeline.ExecuteAsync(@event, UITransitionPhases.BeforeChange).GetAwaiter().GetResult();
Log.Debug("BeforeChange phases completed: {0}", @event.TransitionType);
}
/// <summary>
/// 执行UI切换后的Handler不阻塞
/// </summary>
private void AfterChange(UiTransitionEvent @event)
{
Log.Debug("AfterChange phases started: {0}", @event.TransitionType);
_ = Task.Run(async () =>
{
try
{
await _pipeline.ExecuteAsync(@event, UITransitionPhases.AfterChange).ConfigureAwait(false);
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
}
catch (Exception ex)
{
Log.Error("AfterChange phases failed: {0}, Error: {1}", @event.TransitionType, ex.Message);
}
});
}
/// <summary>
/// 执行Push的核心逻辑不触发Pipeline
/// </summary>
private void DoPushInternal(string uiKey, IUiPageEnterParam? param, UiTransitionPolicy policy)
{
if (_stack.Count > 0)
{
var current = _stack.Peek();
Log.Debug("Pause current page: {0}", current.GetType().Name);
current.OnPause();
if (policy == UiTransitionPolicy.Exclusive)
{
Log.Debug("Hide current page (Exclusive): {0}", current.GetType().Name);
current.OnHide();
}
}
var page = _factory.Create(uiKey);
Log.Debug("Create UI Page instance: {0}", page.GetType().Name);
_uiRoot.AddUiPage(page);
_stack.Push(page);
Log.Debug(
"Enter & Show page: {0}, stackAfter={1}",
page.GetType().Name, _stack.Count
);
page.OnEnter(param);
page.OnShow();
}
/// <summary>
/// 执行Pop的核心逻辑不触发Pipeline
/// </summary>
private void DoPopInternal(UiPopPolicy policy)
{
if (_stack.Count == 0)
return;
var top = _stack.Pop();
Log.Debug(
"Pop UI Page internal: {0}, policy={1}, stackAfterPop={2}",
top.GetType().Name, policy, _stack.Count
);
top.OnExit();
if (policy == UiPopPolicy.Destroy)
{
Log.Debug("Destroy UI Page: {0}", top.GetType().Name);
_uiRoot.RemoveUiPage(top);
}
else
{
Log.Debug("Hide UI Page: {0}", top.GetType().Name);
top.OnHide();
}
if (_stack.Count > 0)
{
var next = _stack.Peek();
Log.Debug("Resume & Show page: {0}", next.GetType().Name);
next.OnResume();
next.OnShow();
}
else
{
Log.Debug("UI stack is now empty");
}
}
/// <summary>
/// 执行Clear的核心逻辑不触发Pipeline
/// </summary>
private void DoClearInternal(UiPopPolicy policy)
{
Log.Debug("Clear UI Stack internal, count={0}", _stack.Count);
while (_stack.Count > 0)
DoPopInternal(policy);
}
}

View File

@ -0,0 +1,168 @@
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging;
using GFramework.Game.Abstractions.enums;
using GFramework.Game.Abstractions.ui;
namespace GFramework.Game.ui;
/// <summary>
/// UI切换处理器管道负责管理和执行UI切换扩展点
/// </summary>
public class UiTransitionPipeline
{
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("UiTransitionPipeline");
private readonly List<IUiTransitionHandler> _handlers = new();
private readonly Dictionary<IUiTransitionHandler, UiTransitionHandlerOptions> _options = new();
/// <summary>
/// 注册UI切换处理器
/// </summary>
/// <param name="handler">处理器实例</param>
/// <param name="options">执行选项</param>
public void RegisterHandler(IUiTransitionHandler handler, UiTransitionHandlerOptions? options = null)
{
ArgumentNullException.ThrowIfNull(handler);
if (_handlers.Contains(handler))
{
Log.Debug("Handler already registered: {0}", handler.GetType().Name);
return;
}
_handlers.Add(handler);
_options[handler] = options ?? new UiTransitionHandlerOptions();
Log.Debug(
"Handler registered: {0}, Priority={1}, Phases={2}, TimeoutMs={3}",
handler.GetType().Name,
handler.Priority,
handler.Phases,
_options[handler].TimeoutMs
);
}
/// <summary>
/// 注销UI切换处理器
/// </summary>
/// <param name="handler">处理器实例</param>
public void UnregisterHandler(IUiTransitionHandler handler)
{
ArgumentNullException.ThrowIfNull(handler);
if (!_handlers.Remove(handler)) return;
_options.Remove(handler);
Log.Debug("Handler unregistered: {0}", handler.GetType().Name);
}
/// <summary>
/// 执行指定阶段的所有Handler
/// </summary>
/// <param name="event">UI切换事件</param>
/// <param name="phases">执行阶段</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>异步任务</returns>
public async Task ExecuteAsync(
UiTransitionEvent @event,
UITransitionPhases phases,
CancellationToken cancellationToken = default
)
{
@event.Set("Phases", phases.ToString());
Log.Debug(
"Execute pipeline: Phases={0}, From={1}, To={2}, Type={3}, HandlerCount={4}",
phases,
@event.FromUiKey,
@event.ToUiKey,
@event.TransitionType,
_handlers.Count
);
var sortedHandlers = FilterAndSortHandlers(@event, phases);
if (sortedHandlers.Count == 0)
{
Log.Debug("No handlers to execute for phases: {0}", phases);
return;
}
Log.Debug(
"Executing {0} handlers for phases {1}",
sortedHandlers.Count,
phases
);
foreach (var handler in sortedHandlers)
{
var options = _options[handler];
await ExecuteSingleHandlerAsync(handler, options, @event, cancellationToken);
}
Log.Debug("Pipeline execution completed for phases: {0}", phases);
}
private List<IUiTransitionHandler> FilterAndSortHandlers(
UiTransitionEvent @event,
UITransitionPhases phases)
{
return _handlers
.Where(h => h.Phases.HasFlag(phases) && h.ShouldHandle(@event, phases))
.OrderBy(h => h.Priority)
.ToList();
}
private static async Task ExecuteSingleHandlerAsync(
IUiTransitionHandler handler,
UiTransitionHandlerOptions options,
UiTransitionEvent @event,
CancellationToken cancellationToken)
{
Log.Debug(
"Executing handler: {0}, Priority={1}",
handler.GetType().Name,
handler.Priority
);
try
{
using var timeoutCts = options.TimeoutMs > 0
? new CancellationTokenSource(options.TimeoutMs)
: null;
using var linkedCts = timeoutCts != null && cancellationToken.CanBeCanceled
? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)
: null;
await handler.HandleAsync(
@event,
linkedCts?.Token ?? cancellationToken
).ConfigureAwait(false);
Log.Debug("Handler completed: {0}", handler.GetType().Name);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
Log.Error(
"Handler timeout: {0}, TimeoutMs={1}",
handler.GetType().Name,
options.TimeoutMs
);
if (options.ContinueOnError) return;
Log.Error("Stopping pipeline due to timeout and ContinueOnError=false");
throw;
}
catch (OperationCanceledException)
{
Log.Debug("Handler cancelled: {0}", handler.GetType().Name);
throw;
}
catch (Exception ex)
{
Log.Error("Handler failed: {0}, Error: {1}", handler.GetType().Name, ex.Message);
if (options.ContinueOnError) return;
Log.Error("Stopping pipeline due to error and ContinueOnError=false");
throw;
}
}
}

View File

@ -0,0 +1,48 @@
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging;
using GFramework.Game.Abstractions.enums;
using GFramework.Game.Abstractions.ui;
namespace GFramework.Game.ui.handler;
/// <summary>
/// 日志UI切换处理器用于记录UI切换的详细信息
/// </summary>
public sealed class LoggingTransitionHandler : UiTransitionHandlerBase
{
/// <summary>
/// 日志记录器实例用于记录UI切换相关信息
/// </summary>
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("LoggingTransitionHandler");
/// <summary>
/// 获取处理器优先级,数值越大优先级越高
/// </summary>
public override int Priority => 999;
/// <summary>
/// 获取处理器处理的UI切换阶段处理所有阶段
/// </summary>
public override UITransitionPhases Phases => UITransitionPhases.All;
/// <summary>
/// 处理UI切换事件的异步方法
/// </summary>
/// <param name="event">UI切换事件对象包含切换的相关信息</param>
/// <param name="cancellationToken">取消令牌,用于控制异步操作的取消</param>
/// <returns>表示异步操作的任务</returns>
public override Task HandleAsync(UiTransitionEvent @event, CancellationToken cancellationToken)
{
// 记录UI切换的详细信息到日志
Log.Info(
"UI Transition: Phases={0}, Type={1}, From={2}, To={3}, Policy={4}",
@event.Get<string>("Phases", "Unknown"),
@event.TransitionType,
@event.FromUiKey,
@event.ToUiKey,
@event.Policy
);
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,33 @@
using GFramework.Game.Abstractions.enums;
using GFramework.Game.Abstractions.ui;
namespace GFramework.Game.ui.handler;
/// <summary>
/// UI切换处理器抽象基类提供一些默认实现
/// </summary>
public abstract class UiTransitionHandlerBase : IUiTransitionHandler
{
/// <summary>
/// 处理器适用的阶段,默认为所有阶段
/// </summary>
public virtual UITransitionPhases Phases => UITransitionPhases.All;
/// <summary>
/// 优先级,需要在子类中实现
/// </summary>
public abstract int Priority { get; }
/// <summary>
/// 判断是否应该处理当前事件默认返回true
/// </summary>
public virtual bool ShouldHandle(UiTransitionEvent @event, UITransitionPhases phases)
{
return true;
}
/// <summary>
/// 处理UI切换事件需要在子类中实现
/// </summary>
public abstract Task HandleAsync(UiTransitionEvent @event, CancellationToken cancellationToken);
}