From 8fd7e2e952dd67fc4ec9c5bfcfb1a97d221825b0 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 15 Jan 2026 08:44:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0UI=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E5=92=8C=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E7=AE=A1=E7=90=86=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=8F=8A=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 定义了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实例的注册和获取功能 --- .../enums/UITransitionPhases.cs | 27 ++ .../enums/UiTransitionPolicy.cs | 32 ++ .../enums/UiTransitionType.cs | 27 ++ .../ui/IPageBehavior.cs | 49 +++ GFramework.Game.Abstractions/ui/IUiFactory.cs | 16 + GFramework.Game.Abstractions/ui/IUiPage.cs | 39 +++ .../ui/IUiPageEnterParam.cs | 7 + .../ui/IUiPageProvider.cs | 13 + .../ui/IUiRegistry.cs | 17 + GFramework.Game.Abstractions/ui/IUiRoot.cs | 19 ++ GFramework.Game.Abstractions/ui/IUiRouter.cs | 64 ++++ .../ui/IUiTransitionHandler.cs | 39 +++ .../ui/IWritableUiRegistry.cs | 16 + .../ui/UiPopPolicy.cs | 17 + .../ui/UiTransitionEvent.cs | 105 ++++++ .../ui/UiTransitionHandlerOptions.cs | 6 + GFramework.Game/ui/UiRouterBase.cs | 319 ++++++++++++++++++ GFramework.Game/ui/UiTransitionPipeline.cs | 168 +++++++++ .../ui/handler/LoggingTransitionHandler.cs | 48 +++ .../ui/handler/UiTransitionHandlerBase.cs | 33 ++ 20 files changed, 1061 insertions(+) create mode 100644 GFramework.Game.Abstractions/enums/UITransitionPhases.cs create mode 100644 GFramework.Game.Abstractions/enums/UiTransitionPolicy.cs create mode 100644 GFramework.Game.Abstractions/enums/UiTransitionType.cs create mode 100644 GFramework.Game.Abstractions/ui/IPageBehavior.cs create mode 100644 GFramework.Game.Abstractions/ui/IUiFactory.cs create mode 100644 GFramework.Game.Abstractions/ui/IUiPage.cs create mode 100644 GFramework.Game.Abstractions/ui/IUiPageEnterParam.cs create mode 100644 GFramework.Game.Abstractions/ui/IUiPageProvider.cs create mode 100644 GFramework.Game.Abstractions/ui/IUiRegistry.cs create mode 100644 GFramework.Game.Abstractions/ui/IUiRoot.cs create mode 100644 GFramework.Game.Abstractions/ui/IUiRouter.cs create mode 100644 GFramework.Game.Abstractions/ui/IUiTransitionHandler.cs create mode 100644 GFramework.Game.Abstractions/ui/IWritableUiRegistry.cs create mode 100644 GFramework.Game.Abstractions/ui/UiPopPolicy.cs create mode 100644 GFramework.Game.Abstractions/ui/UiTransitionEvent.cs create mode 100644 GFramework.Game.Abstractions/ui/UiTransitionHandlerOptions.cs create mode 100644 GFramework.Game/ui/UiRouterBase.cs create mode 100644 GFramework.Game/ui/UiTransitionPipeline.cs create mode 100644 GFramework.Game/ui/handler/LoggingTransitionHandler.cs create mode 100644 GFramework.Game/ui/handler/UiTransitionHandlerBase.cs diff --git a/GFramework.Game.Abstractions/enums/UITransitionPhases.cs b/GFramework.Game.Abstractions/enums/UITransitionPhases.cs new file mode 100644 index 0000000..df6843f --- /dev/null +++ b/GFramework.Game.Abstractions/enums/UITransitionPhases.cs @@ -0,0 +1,27 @@ +using System; + +namespace GFramework.Game.Abstractions.enums; + +/// +/// UI切换阶段枚举,定义UI切换过程中的不同阶段 +/// +[Flags] +public enum UITransitionPhases +{ + /// + /// UI切换前阶段,在此阶段执行的Handler可以阻塞UI切换流程 + /// 适用于:淡入淡出动画、用户确认对话框、数据预加载等需要等待完成的操作 + /// + BeforeChange = 1, + + /// + /// UI切换后阶段,在此阶段执行的Handler不阻塞UI切换流程 + /// 适用于:播放音效、日志记录、统计数据收集等后台操作 + /// + AfterChange = 2, + + /// + /// 所有阶段,Handler将在BeforeChange和AfterChange阶段都执行 + /// + All = BeforeChange | AfterChange +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/enums/UiTransitionPolicy.cs b/GFramework.Game.Abstractions/enums/UiTransitionPolicy.cs new file mode 100644 index 0000000..8bc837e --- /dev/null +++ b/GFramework.Game.Abstractions/enums/UiTransitionPolicy.cs @@ -0,0 +1,32 @@ +namespace GFramework.Game.Abstractions.enums; + +/// +/// UI页面过渡策略枚举 +/// 定义了UI页面在出栈时的不同处理方式 +/// +public enum UiTransitionPolicy +{ + /// + /// 出栈即销毁(一次性页面) + /// 页面从栈中移除时会完全销毁实例 + /// + Destroy, + + /// + /// 出栈隐藏,保留实例 + /// 页面从栈中移除时仅隐藏显示,保留实例以便后续重用 + /// + Hide, + + /// + /// 覆盖显示(不影响下层页面) + /// 当前页面覆盖在其他页面之上显示,不影响下层页面的状态 + /// + Overlay, + + /// + /// 独占显示(下层页面 Pause + Hide) + /// 当前页面独占显示区域,下层页面会被暂停并隐藏 + /// + Exclusive +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/enums/UiTransitionType.cs b/GFramework.Game.Abstractions/enums/UiTransitionType.cs new file mode 100644 index 0000000..0d0f067 --- /dev/null +++ b/GFramework.Game.Abstractions/enums/UiTransitionType.cs @@ -0,0 +1,27 @@ +namespace GFramework.Game.Abstractions.enums; + +/// +/// UI切换类型枚举,定义不同的UI切换操作类型 +/// +public enum UiTransitionType +{ + /// + /// 压入新页面到栈顶 + /// + Push, + + /// + /// 弹出栈顶页面 + /// + Pop, + + /// + /// 替换当前页面 + /// + Replace, + + /// + /// 清空所有页面 + /// + Clear, +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IPageBehavior.cs b/GFramework.Game.Abstractions/ui/IPageBehavior.cs new file mode 100644 index 0000000..7307cd6 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IPageBehavior.cs @@ -0,0 +1,49 @@ +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI页面接口,定义了UI页面的生命周期方法 +/// +public interface IPageBehavior +{ + /// + /// 获取页面视图对象 + /// + /// 页面视图实例 + object View { get; } + + /// + /// 获取页面是否处于活动状态 + /// + bool IsAlive { get; } + + /// + /// 页面进入时调用的方法 + /// + /// 页面进入时传递的参数,可为空 + void OnEnter(IUiPageEnterParam? param); + + /// + /// 页面退出时调用的方法 + /// + void OnExit(); + + /// + /// 页面暂停时调用的方法 + /// + void OnPause(); + + /// + /// 页面恢复时调用的方法 + /// + void OnResume(); + + /// + /// 页面被覆盖时调用(不销毁) + /// + void OnHide(); + + /// + /// 页面重新显示时调用的方法 + /// + void OnShow(); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiFactory.cs b/GFramework.Game.Abstractions/ui/IUiFactory.cs new file mode 100644 index 0000000..fc8a715 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiFactory.cs @@ -0,0 +1,16 @@ +using GFramework.Core.Abstractions.utility; + +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI工厂接口,用于创建UI页面实例 +/// +public interface IUiFactory : IContextUtility +{ + /// + /// 根据UI键值创建对应的UI页面实例 + /// + /// UI标识键,用于确定要创建的具体UI页面类型 + /// 创建的UI页面实例,实现IUiPage接口 + IPageBehavior Create(string uiKey); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiPage.cs b/GFramework.Game.Abstractions/ui/IUiPage.cs new file mode 100644 index 0000000..4a3d9c8 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiPage.cs @@ -0,0 +1,39 @@ +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI页面生命周期接口 +/// 定义了UI页面的各种状态转换方法,用于管理UI页面的进入、退出、暂停、恢复、显示和隐藏等生命周期事件 +/// +public interface IUiPage +{ + /// + /// 页面进入时调用的方法 + /// + /// 页面进入参数,可能为空 + void OnEnter(IUiPageEnterParam? param); + + /// + /// 页面退出时调用的方法 + /// + void OnExit(); + + /// + /// 页面暂停时调用的方法 + /// + void OnPause(); + + /// + /// 页面恢复时调用的方法 + /// + void OnResume(); + + /// + /// 页面显示时调用的方法 + /// + void OnShow(); + + /// + /// 页面隐藏时调用的方法 + /// + void OnHide(); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiPageEnterParam.cs b/GFramework.Game.Abstractions/ui/IUiPageEnterParam.cs new file mode 100644 index 0000000..5b4a17d --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiPageEnterParam.cs @@ -0,0 +1,7 @@ +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI页面进入参数接口 +/// 该接口用于定义UI页面跳转时传递的参数数据结构 +/// +public interface IUiPageEnterParam; \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiPageProvider.cs b/GFramework.Game.Abstractions/ui/IUiPageProvider.cs new file mode 100644 index 0000000..d38892b --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiPageProvider.cs @@ -0,0 +1,13 @@ +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI页面提供者接口,用于创建UI页面实例 +/// +public interface IUiPageProvider +{ + /// + /// 获取UI页面实例 + /// + /// UI页面实例 + IPageBehavior GetPage(); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiRegistry.cs b/GFramework.Game.Abstractions/ui/IUiRegistry.cs new file mode 100644 index 0000000..8d66633 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiRegistry.cs @@ -0,0 +1,17 @@ +using GFramework.Core.Abstractions.utility; + +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI注册表接口,用于根据UI键获取对应的UI实例 +/// +/// UI实例的类型参数,使用协变修饰符out +public interface IUiRegistry : IUtility +{ + /// + /// 根据指定的UI键获取对应的UI实例 + /// + /// UI的唯一标识键 + /// 与指定键关联的UI实例 + T Get(string uiKey); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiRoot.cs b/GFramework.Game.Abstractions/ui/IUiRoot.cs new file mode 100644 index 0000000..6785d72 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiRoot.cs @@ -0,0 +1,19 @@ +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI根节点接口,定义了UI页面容器的基本操作 +/// +public interface IUiRoot +{ + /// + /// 向UI根节点添加子页面 + /// + /// 要添加的UI页面子节点 + void AddUiPage(IPageBehavior child); + + /// + /// 从UI根节点移除子页面 + /// + /// 要移除的UI页面子节点 + void RemoveUiPage(IPageBehavior child); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiRouter.cs b/GFramework.Game.Abstractions/ui/IUiRouter.cs new file mode 100644 index 0000000..e1bad8e --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiRouter.cs @@ -0,0 +1,64 @@ +using GFramework.Core.Abstractions.system; +using GFramework.Game.Abstractions.enums; + +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI路由管理器接口,用于管理UI界面的导航和切换操作 +/// +public interface IUiRouter : ISystem +{ + /// + /// 绑定UI根节点 + /// + /// UI根节点接口实例 + void BindRoot(IUiRoot root); + + /// + /// 将指定的UI界面压入路由栈,显示新的UI界面 + /// + /// UI界面的唯一标识符 + /// 进入界面的参数,可为空 + /// 界面切换策略,默认为Exclusive(独占) + void Push(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive); + + + /// + /// 弹出路由栈顶的UI界面,返回到上一个界面 + /// + /// 界面弹出策略,默认为Destroy(销毁) + void Pop(UiPopPolicy policy = UiPopPolicy.Destroy); + + /// + /// 替换当前UI界面为指定的新界面 + /// + /// 新UI界面的唯一标识符 + /// 进入界面的参数,可为空 + /// 界面弹出策略,默认为销毁当前界面 + /// 界面过渡策略,默认为独占模式 + void Replace( + string uiKey, + IUiPageEnterParam? param = null, + UiPopPolicy popPolicy = UiPopPolicy.Destroy, + UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive + ); + + + /// + /// 清空所有UI界面,重置路由状态 + /// + void Clear(); + + /// + /// 注册UI切换处理器 + /// + /// 处理器实例 + /// 执行选项 + void RegisterHandler(IUiTransitionHandler handler, UiTransitionHandlerOptions? options = null); + + /// + /// 注销UI切换处理器 + /// + /// 处理器实例 + void UnregisterHandler(IUiTransitionHandler handler); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiTransitionHandler.cs b/GFramework.Game.Abstractions/ui/IUiTransitionHandler.cs new file mode 100644 index 0000000..393e021 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiTransitionHandler.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; +using GFramework.Game.Abstractions.enums; + +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI切换处理器接口,定义UI切换扩展点的处理逻辑 +/// +public interface IUiTransitionHandler +{ + /// + /// 处理器优先级,数值越小越先执行 + /// + int Priority { get; } + + /// + /// 处理器适用的阶段,默认为所有阶段 + /// 可以使用Flags枚举指定多个阶段 + /// + UITransitionPhases Phases { get; } + + /// + /// 判断是否应该处理当前事件 + /// 可以根据事件类型、UI key等信息进行条件过滤 + /// + /// UI切换事件 + /// 当前阶段 + /// 是否处理 + bool ShouldHandle(UiTransitionEvent @event, UITransitionPhases phases); + + /// + /// 处理UI切换事件 + /// + /// UI切换事件 + /// 取消令牌 + /// 异步任务 + Task HandleAsync(UiTransitionEvent @event, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IWritableUiRegistry.cs b/GFramework.Game.Abstractions/ui/IWritableUiRegistry.cs new file mode 100644 index 0000000..c50669f --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IWritableUiRegistry.cs @@ -0,0 +1,16 @@ +namespace GFramework.Game.Abstractions.ui; + +/// +/// 可写的UI注册表接口 +/// +/// UI实例的类型参数 +public interface IWritableUiRegistry : IUiRegistry where T : class +{ + /// + /// 注册UI实例到注册表 + /// + /// UI的唯一标识键 + /// 要注册的UI实例 + /// 当前注册表实例 + IWritableUiRegistry Register(string key, T scene); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/UiPopPolicy.cs b/GFramework.Game.Abstractions/ui/UiPopPolicy.cs new file mode 100644 index 0000000..34607db --- /dev/null +++ b/GFramework.Game.Abstractions/ui/UiPopPolicy.cs @@ -0,0 +1,17 @@ +namespace GFramework.Game.Abstractions.ui; + +/// +/// 定义UI弹窗的关闭策略枚举 +/// +public enum UiPopPolicy +{ + /// + /// 销毁模式:关闭时完全销毁UI对象 + /// + Destroy, + + /// + /// 隐藏模式:关闭时仅隐藏UI对象,保留实例 + /// + Hide +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/UiTransitionEvent.cs b/GFramework.Game.Abstractions/ui/UiTransitionEvent.cs new file mode 100644 index 0000000..7aa9bac --- /dev/null +++ b/GFramework.Game.Abstractions/ui/UiTransitionEvent.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using GFramework.Game.Abstractions.enums; + +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI切换事件,包含UI切换过程中的上下文信息 +/// +public sealed class UiTransitionEvent +{ + /// + /// 用户自定义数据字典,用于Handler之间传递数据 + /// + private readonly Dictionary _context = new(StringComparer.Ordinal); + + /// + /// 源UI的标识符,切换前的UI key + /// + public string FromUiKey { get; init; } = string.Empty; + + /// + /// 目标UI的标识符,切换后的UI key + /// + public string ToUiKey { get; init; } = string.Empty; + + /// + /// UI切换类型 + /// + public UiTransitionType TransitionType { get; init; } + + /// + /// UI切换策略 + /// + public UiTransitionPolicy Policy { get; init; } + + /// + /// UI进入参数 + /// + public IUiPageEnterParam? EnterParam { get; init; } + + /// + /// 获取用户自定义数据 + /// + /// 数据类型 + /// 数据键 + /// 默认值(当键不存在或类型不匹配时返回) + /// 用户数据 + public T Get(string key, T defaultValue = default!) + { + if (_context.TryGetValue(key, out var obj) && obj is T value) + return value; + return defaultValue; + } + + /// + /// 尝试获取用户自定义数据 + /// + /// 数据类型 + /// 数据键 + /// 输出值 + /// 是否成功获取 + public bool TryGet(string key, out T value) + { + if (_context.TryGetValue(key, out var obj) && obj is T t) + { + value = t; + return true; + } + + value = default!; + return false; + } + + /// + /// 设置用户自定义数据 + /// + /// 数据类型 + /// 数据键 + /// 数据值 + public void Set(string key, T value) + { + _context[key] = value!; + } + + /// + /// 检查是否存在指定的用户数据键 + /// + /// 数据键 + /// 是否存在 + public bool Has(string key) + { + return _context.ContainsKey(key); + } + + /// + /// 移除指定的用户数据 + /// + /// 数据键 + /// 是否成功移除 + public bool Remove(string key) + { + return _context.Remove(key); + } +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/UiTransitionHandlerOptions.cs b/GFramework.Game.Abstractions/ui/UiTransitionHandlerOptions.cs new file mode 100644 index 0000000..058ebc6 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/UiTransitionHandlerOptions.cs @@ -0,0 +1,6 @@ +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI切换处理器执行选项 +/// +public record UiTransitionHandlerOptions(int TimeoutMs = 0, bool ContinueOnError = true); \ No newline at end of file diff --git a/GFramework.Game/ui/UiRouterBase.cs b/GFramework.Game/ui/UiRouterBase.cs new file mode 100644 index 0000000..cb7678f --- /dev/null +++ b/GFramework.Game/ui/UiRouterBase.cs @@ -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; + +/// +/// UI路由类,提供页面栈管理功能 +/// +public abstract class UiRouterBase : AbstractSystem, IUiRouter +{ + private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("UiRouterBase"); + + /// + /// UI切换处理器管道 + /// + private readonly UiTransitionPipeline _pipeline = new(); + + /// + /// 页面栈,用于管理UI页面的显示顺序 + /// + private readonly Stack _stack = new(); + + /// + /// UI工厂实例,用于创建UI相关的对象 + /// + private IUiFactory _factory = null!; + + private IUiRoot _uiRoot = null!; + + /// + /// 注册UI切换处理器 + /// + /// 处理器实例 + /// 执行选项 + public void RegisterHandler(IUiTransitionHandler handler, UiTransitionHandlerOptions? options = null) + { + _pipeline.RegisterHandler(handler, options); + } + + /// + /// 注销UI切换处理器 + /// + /// 处理器实例 + public void UnregisterHandler(IUiTransitionHandler handler) + { + _pipeline.UnregisterHandler(handler); + } + + /// + /// 绑定UI根节点 + /// + public void BindRoot(IUiRoot root) + { + _uiRoot = root; + Log.Debug("Bind UI Root: {0}", root.GetType().Name); + } + + /// + /// 将指定UI页面压入栈顶并显示 + /// + /// UI页面标识符 + /// 页面进入参数,可为空 + /// 页面切换策略 + 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); + } + + + /// + /// 弹出栈顶页面并根据策略处理页面 + /// + /// 弹出策略,默认为销毁策略 + 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); + } + + /// + /// 替换当前所有页面为新页面 + /// + /// 新UI页面标识符 + /// 页面进入参数,可为空 + /// 弹出页面时的销毁策略,默认为销毁 + /// 推入页面时的过渡策略,默认为独占 + 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); + } + + /// + /// 清空所有页面栈中的页面 + /// + 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); + } + + /// + /// 初始化方法,在页面初始化时获取UI工厂实例 + /// + protected override void OnInit() + { + _factory = this.GetUtility()!; + Log.Debug("UiRouterBase initialized. Factory={0}", _factory.GetType().Name); + RegisterHandlers(); + } + + /// + /// 注册默认的UI切换处理器 + /// + protected abstract void RegisterHandlers(); + + /// + /// 获取当前栈顶UI的key + /// + /// 当前UI key,如果栈为空则返回空字符串 + private string GetCurrentUiKey() + { + if (_stack.Count == 0) + return string.Empty; + var page = _stack.Peek(); + return page.View.GetType().Name; + } + + /// + /// 创建UI切换事件 + /// + 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 + }; + } + + /// + /// 执行UI切换前的Handler(阻塞) + /// + 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); + } + + /// + /// 执行UI切换后的Handler(不阻塞) + /// + 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); + } + }); + } + + /// + /// 执行Push的核心逻辑(不触发Pipeline) + /// + 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(); + } + + /// + /// 执行Pop的核心逻辑(不触发Pipeline) + /// + 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"); + } + } + + /// + /// 执行Clear的核心逻辑(不触发Pipeline) + /// + private void DoClearInternal(UiPopPolicy policy) + { + Log.Debug("Clear UI Stack internal, count={0}", _stack.Count); + while (_stack.Count > 0) + DoPopInternal(policy); + } +} \ No newline at end of file diff --git a/GFramework.Game/ui/UiTransitionPipeline.cs b/GFramework.Game/ui/UiTransitionPipeline.cs new file mode 100644 index 0000000..c8a8d01 --- /dev/null +++ b/GFramework.Game/ui/UiTransitionPipeline.cs @@ -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; + +/// +/// UI切换处理器管道,负责管理和执行UI切换扩展点 +/// +public class UiTransitionPipeline +{ + private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("UiTransitionPipeline"); + private readonly List _handlers = new(); + private readonly Dictionary _options = new(); + + /// + /// 注册UI切换处理器 + /// + /// 处理器实例 + /// 执行选项 + 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 + ); + } + + /// + /// 注销UI切换处理器 + /// + /// 处理器实例 + public void UnregisterHandler(IUiTransitionHandler handler) + { + ArgumentNullException.ThrowIfNull(handler); + + if (!_handlers.Remove(handler)) return; + _options.Remove(handler); + Log.Debug("Handler unregistered: {0}", handler.GetType().Name); + } + + /// + /// 执行指定阶段的所有Handler + /// + /// UI切换事件 + /// 执行阶段 + /// 取消令牌 + /// 异步任务 + 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 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; + } + } +} \ No newline at end of file diff --git a/GFramework.Game/ui/handler/LoggingTransitionHandler.cs b/GFramework.Game/ui/handler/LoggingTransitionHandler.cs new file mode 100644 index 0000000..da3a4f0 --- /dev/null +++ b/GFramework.Game/ui/handler/LoggingTransitionHandler.cs @@ -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; + +/// +/// 日志UI切换处理器,用于记录UI切换的详细信息 +/// +public sealed class LoggingTransitionHandler : UiTransitionHandlerBase +{ + /// + /// 日志记录器实例,用于记录UI切换相关信息 + /// + private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("LoggingTransitionHandler"); + + /// + /// 获取处理器优先级,数值越大优先级越高 + /// + public override int Priority => 999; + + /// + /// 获取处理器处理的UI切换阶段,处理所有阶段 + /// + public override UITransitionPhases Phases => UITransitionPhases.All; + + /// + /// 处理UI切换事件的异步方法 + /// + /// UI切换事件对象,包含切换的相关信息 + /// 取消令牌,用于控制异步操作的取消 + /// 表示异步操作的任务 + 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("Phases", "Unknown"), + @event.TransitionType, + @event.FromUiKey, + @event.ToUiKey, + @event.Policy + ); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/GFramework.Game/ui/handler/UiTransitionHandlerBase.cs b/GFramework.Game/ui/handler/UiTransitionHandlerBase.cs new file mode 100644 index 0000000..cf8be83 --- /dev/null +++ b/GFramework.Game/ui/handler/UiTransitionHandlerBase.cs @@ -0,0 +1,33 @@ +using GFramework.Game.Abstractions.enums; +using GFramework.Game.Abstractions.ui; + +namespace GFramework.Game.ui.handler; + +/// +/// UI切换处理器抽象基类,提供一些默认实现 +/// +public abstract class UiTransitionHandlerBase : IUiTransitionHandler +{ + /// + /// 处理器适用的阶段,默认为所有阶段 + /// + public virtual UITransitionPhases Phases => UITransitionPhases.All; + + /// + /// 优先级,需要在子类中实现 + /// + public abstract int Priority { get; } + + /// + /// 判断是否应该处理当前事件,默认返回true + /// + public virtual bool ShouldHandle(UiTransitionEvent @event, UITransitionPhases phases) + { + return true; + } + + /// + /// 处理UI切换事件,需要在子类中实现 + /// + public abstract Task HandleAsync(UiTransitionEvent @event, CancellationToken cancellationToken); +} \ No newline at end of file