From e972d926a76ba9478ca351d8b98902dc8b03abcb Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:36:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0UI=E8=BF=87?= =?UTF-8?q?=E6=B8=A1=E5=8A=A8=E7=94=BB=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加了完整的UI过渡动画功能,包括: - 新增UiTransitionAnimation枚举定义各种动画类型 - 扩展IUiRouter接口支持动画策略参数 - 新增IUiTransition接口定义动画播放契约 - 新增UiAnimationPolicy类配置动画行为 - 实现God --- .../enums/UiTransitionAnimation.cs | 48 +++++ GFramework.Game.Abstractions/ui/IUiRouter.cs | 14 +- .../ui/IUiTransition.cs | 24 +++ .../ui/UiAnimationPolicy.cs | 91 ++++++++++ GFramework.Game/ui/UiRouterBase.cs | 21 ++- GFramework.Godot/ui/GodotUiTransition.cs | 171 ++++++++++++++++++ 6 files changed, 360 insertions(+), 9 deletions(-) create mode 100644 GFramework.Game.Abstractions/enums/UiTransitionAnimation.cs create mode 100644 GFramework.Game.Abstractions/ui/IUiTransition.cs create mode 100644 GFramework.Game.Abstractions/ui/UiAnimationPolicy.cs create mode 100644 GFramework.Godot/ui/GodotUiTransition.cs diff --git a/GFramework.Game.Abstractions/enums/UiTransitionAnimation.cs b/GFramework.Game.Abstractions/enums/UiTransitionAnimation.cs new file mode 100644 index 0000000..15d2c08 --- /dev/null +++ b/GFramework.Game.Abstractions/enums/UiTransitionAnimation.cs @@ -0,0 +1,48 @@ +namespace GFramework.Game.Abstractions.enums; + +/// +/// UI过渡动画类型枚举 +/// 定义UI切换时支持的动画效果 +/// +public enum UiTransitionAnimation +{ + /// + /// 无动画 + /// + None, + + /// + /// 淡入淡出动画 + /// + Fade, + + /// + /// 从右侧滑入 + /// + SlideLeft, + + /// + /// 从左侧滑入 + /// + SlideRight, + + /// + /// 从下方滑入 + /// + SlideUp, + + /// + /// 从上方滑入 + /// + SlideDown, + + /// + /// 缩放动画 + /// + Scale, + + /// + /// 自定义动画(需要提供自定义的 IUiTransition 实现) + /// + Custom +} diff --git a/GFramework.Game.Abstractions/ui/IUiRouter.cs b/GFramework.Game.Abstractions/ui/IUiRouter.cs index fde8763..c12172c 100644 --- a/GFramework.Game.Abstractions/ui/IUiRouter.cs +++ b/GFramework.Game.Abstractions/ui/IUiRouter.cs @@ -26,8 +26,9 @@ public interface IUiRouter : ISystem /// 进入界面的参数,可为空 /// 界面切换策略,默认为Exclusive(独占) /// 实例管理策略,默认为Reuse(复用) + /// 动画策略,可为空 void Push(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive, - UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse); + UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse, UiAnimationPolicy? animationPolicy = null); /// @@ -37,8 +38,9 @@ public interface IUiRouter : ISystem /// 已创建的UI页面行为实例 /// 进入界面的参数,可为空 /// 界面切换策略,默认为Exclusive(独占) + /// 动画策略,可为空 void Push(IUiPageBehavior page, IUiPageEnterParam? param = null, - UiTransitionPolicy policy = UiTransitionPolicy.Exclusive); + UiTransitionPolicy policy = UiTransitionPolicy.Exclusive, UiAnimationPolicy? animationPolicy = null); /// @@ -55,12 +57,14 @@ public interface IUiRouter : ISystem /// 弹出页面时的销毁策略,默认为销毁 /// 推入页面时的过渡策略,默认为独占 /// 实例管理策略 + /// 动画策略,可为空 public void Replace( string uiKey, IUiPageEnterParam? param = null, UiPopPolicy popPolicy = UiPopPolicy.Destroy, UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive, - UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse); + UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse, + UiAnimationPolicy? animationPolicy = null); /// /// 替换当前所有页面为已存在的页面(基于实例) @@ -69,11 +73,13 @@ public interface IUiRouter : ISystem /// 页面进入参数,可为空 /// 弹出页面时的销毁策略,默认为销毁 /// 推入页面时的过渡策略,默认为独占 + /// 动画策略,可为空 public void Replace( IUiPageBehavior page, IUiPageEnterParam? param = null, UiPopPolicy popPolicy = UiPopPolicy.Destroy, - UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive); + UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive, + UiAnimationPolicy? animationPolicy = null); /// /// 清空所有UI界面,重置路由状态 /// diff --git a/GFramework.Game.Abstractions/ui/IUiTransition.cs b/GFramework.Game.Abstractions/ui/IUiTransition.cs new file mode 100644 index 0000000..1326171 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiTransition.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI过渡动画接口 +/// 定义UI进入和退出时的动画效果 +/// +public interface IUiTransition +{ + /// + /// 播放进入动画 + /// + /// UI页面 + /// 异步任务,动画完成后完成 + Task PlayEnterAsync(IUiPageBehavior page); + + /// + /// 播放退出动画 + /// + /// UI页面 + /// 异步任务,动画完成后完成 + Task PlayExitAsync(IUiPageBehavior page); +} diff --git a/GFramework.Game.Abstractions/ui/UiAnimationPolicy.cs b/GFramework.Game.Abstractions/ui/UiAnimationPolicy.cs new file mode 100644 index 0000000..3ce0713 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/UiAnimationPolicy.cs @@ -0,0 +1,91 @@ +using GFramework.Game.Abstractions.enums; + +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI动画策略配置 +/// 用于配置UI过渡动画的行为 +/// +public class UiAnimationPolicy +{ + /// + /// 动画类型 + /// + public UiTransitionAnimation Animation { get; set; } = UiTransitionAnimation.None; + + /// + /// 动画持续时间(秒) + /// + public float Duration { get; set; } = 0.3f; + + /// + /// 是否阻塞UI切换(等待动画完成) + /// + public bool BlockTransition { get; set; } = false; + + /// + /// 自定义动画实现(仅当 Animation 为 Custom 时使用) + /// + public IUiTransition? CustomTransition { get; set; } + + /// + /// 缓动函数(可选,用于调整动画曲线) + /// + public EasingFunction Easing { get; set; } = EasingFunction.EaseInOut; + + /// + /// 创建默认策略(无动画) + /// + public static UiAnimationPolicy None => new UiAnimationPolicy { Animation = UiTransitionAnimation.None }; + + /// + /// 创建淡入淡出策略 + /// + /// 持续时间 + /// 是否阻塞 + public static UiAnimationPolicy Fade(float duration = 0.3f, bool block = false) + => new UiAnimationPolicy { Animation = UiTransitionAnimation.Fade, Duration = duration, BlockTransition = block }; + + /// + /// 创建滑入策略 + /// + /// 滑动方向 + /// 持续时间 + /// 是否阻塞 + public static UiAnimationPolicy Slide(UiTransitionAnimation direction, float duration = 0.3f, bool block = false) + => new UiAnimationPolicy { Animation = direction, Duration = duration, BlockTransition = block }; + + /// + /// 创建缩放策略 + /// + /// 持续时间 + /// 是否阻塞 + public static UiAnimationPolicy Scale(float duration = 0.3f, bool block = false) + => new UiAnimationPolicy { Animation = UiTransitionAnimation.Scale, Duration = duration, BlockTransition = block }; +} + +/// +/// 缓动函数枚举 +/// +public enum EasingFunction +{ + /// + /// 线性 + /// + Linear, + + /// + /// 缓入 + /// + EaseIn, + + /// + /// 缓出 + /// + EaseOut, + + /// + /// 缓入缓出 + /// + EaseInOut +} diff --git a/GFramework.Game/ui/UiRouterBase.cs b/GFramework.Game/ui/UiRouterBase.cs index b6bffc8..4ffd2fe 100644 --- a/GFramework.Game/ui/UiRouterBase.cs +++ b/GFramework.Game/ui/UiRouterBase.cs @@ -73,8 +73,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// 进入界面的参数,可为空 /// 界面切换策略,默认为Exclusive(独占) /// 实例管理策略,默认为Reuse(复用) + /// 动画策略,可为空 public void Push(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive, - UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse) + UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse, UiAnimationPolicy? animationPolicy = null) { if (IsTop(uiKey)) { @@ -83,6 +84,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter } var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param); + @event.Set("AnimationPolicy", animationPolicy); Log.Debug( "Push UI Page: key={0}, policy={1}, instancePolicy={2}, stackBefore={3}", @@ -106,10 +108,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// 已创建的UI页面行为实例 /// 页面进入参数,可为空 /// 页面切换策略 + /// 动画策略,可为空 public void Push( IUiPageBehavior page, IUiPageEnterParam? param = null, - UiTransitionPolicy policy = UiTransitionPolicy.Exclusive + UiTransitionPolicy policy = UiTransitionPolicy.Exclusive, + UiAnimationPolicy? animationPolicy = null ) { var uiKey = page.View.GetType().Name; @@ -121,6 +125,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter } var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param); + @event.Set("AnimationPolicy", animationPolicy); Log.Debug( "Push existing UI Page: key={0}, policy={1}, stackBefore={2}", @@ -164,14 +169,17 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// 弹出页面时的销毁策略,默认为销毁 /// 推入页面时的过渡策略,默认为独占 /// 实例管理策略 + /// 动画策略,可为空 public void Replace( string uiKey, IUiPageEnterParam? param = null, UiPopPolicy popPolicy = UiPopPolicy.Destroy, UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive, - UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse) + UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse, + UiAnimationPolicy? animationPolicy = null) { var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param); + @event.Set("AnimationPolicy", animationPolicy); Log.Debug( "Replace UI Stack with page: key={0}, popPolicy={1}, pushPolicy={2}, instancePolicy={3}", @@ -186,7 +194,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter // 使用工厂的增强方法获取实例 var page = _factory.GetOrCreate(uiKey, instancePolicy); Log.Debug("Get/Create UI Page instance for Replace: {0}", page.GetType().Name); - + DoPushPageInternal(page, param, pushPolicy); AfterChange(@event); @@ -198,14 +206,17 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// 页面进入参数,可为空 /// 弹出页面时的销毁策略,默认为销毁 /// 推入页面时的过渡策略,默认为独占 + /// 动画策略,可为空 public void Replace( IUiPageBehavior page, IUiPageEnterParam? param = null, UiPopPolicy popPolicy = UiPopPolicy.Destroy, - UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive) + UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive, + UiAnimationPolicy? animationPolicy = null) { var uiKey = page.Key; var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param); + @event.Set("AnimationPolicy", animationPolicy); Log.Debug( "Replace UI Stack with existing page: key={0}, popPolicy={1}, pushPolicy={2}", diff --git a/GFramework.Godot/ui/GodotUiTransition.cs b/GFramework.Godot/ui/GodotUiTransition.cs new file mode 100644 index 0000000..85323cf --- /dev/null +++ b/GFramework.Godot/ui/GodotUiTransition.cs @@ -0,0 +1,171 @@ +using System; +using System.Threading.Tasks; +using GFramework.Game.Abstractions.enums; +using GFramework.Game.Abstractions.ui; +using Godot; + +namespace GFramework.Godot.ui; + +/// +/// Godot平台的UI过渡动画实现 +/// 支持多种预定义动画效果 +/// +public class GodotUiTransition : IUiTransition +{ + private readonly UiTransitionAnimation _animation; + + /// + /// 创建过渡动画实例 + /// + /// 动画类型 + public GodotUiTransition(UiTransitionAnimation animation) + { + _animation = animation; + } + + /// + /// 播放进入动画 + /// + public async Task PlayEnterAsync(IUiPageBehavior page) + { + var node = page.View as Node; + if (node == null) + return; + + switch (_animation) + { + case UiTransitionAnimation.Fade: + await PlayFadeEnterAsync(node); + break; + case UiTransitionAnimation.Scale: + await PlayScaleEnterAsync(node); + break; + case UiTransitionAnimation.SlideLeft: + case UiTransitionAnimation.SlideRight: + case UiTransitionAnimation.SlideUp: + case UiTransitionAnimation.SlideDown: + await PlaySlideEnterAsync(node, _animation); + break; + case UiTransitionAnimation.None: + default: + break; + } + } + + /// + /// 播放退出动画 + /// + public async Task PlayExitAsync(IUiPageBehavior page) + { + var node = page.View as Node; + if (node == null) + return; + + switch (_animation) + { + case UiTransitionAnimation.Fade: + await PlayFadeExitAsync(node); + break; + case UiTransitionAnimation.Scale: + await PlayScaleExitAsync(node); + break; + case UiTransitionAnimation.SlideLeft: + case UiTransitionAnimation.SlideRight: + case UiTransitionAnimation.SlideUp: + case UiTransitionAnimation.SlideDown: + await PlaySlideExitAsync(node, _animation); + break; + case UiTransitionAnimation.None: + default: + break; + } + } + + private static async Task PlayFadeEnterAsync(Node node) + { + if (node is CanvasItem canvasItem) + { + canvasItem.Modulate = new Color(1,1, 1, 0); + canvasItem.Visible = true; + await Task.Delay(300); + canvasItem.Modulate = new Color(1,1, 1, 1); + } + } + + private static async Task PlayFadeExitAsync(Node node) + { + if (node is CanvasItem canvasItem) + { + await Task.Delay(300); + canvasItem.Modulate = new Color(1,1, 1, 0); + } + } + + private static async Task PlayScaleEnterAsync(Node node) + { + if (node is Control control) + { + control.Scale = Vector2.Zero; + control.PivotOffset = control.Size / 2; + await Task.Delay(300); + control.Scale = Vector2.One; + } + } + + private static async Task PlayScaleExitAsync(Node node) + { + if (node is Control control) + { + control.PivotOffset = control.Size / 2; + await Task.Delay(300); + control.Scale = Vector2.Zero; + } + } + + private static async Task PlaySlideEnterAsync(Node node, UiTransitionAnimation direction) + { + if (node is Control control) + { + var screenPos = control.GetViewportRect().Size; + var offset = GetSlideOffset(direction, screenPos); + control.Position += offset; + await Task.Delay(300); + control.Position -= offset; + } + } + + private static async Task PlaySlideExitAsync(Node node, UiTransitionAnimation direction) + { + if (node is Control control) + { + var screenPos = control.GetViewportRect().Size; + var offset = GetSlideExitOffset(direction, screenPos); + await Task.Delay(300); + control.Position += offset; + } + } + + private static Vector2 GetSlideOffset(UiTransitionAnimation direction, Vector2 screenPos) + { + return direction switch + { + UiTransitionAnimation.SlideLeft => Vector2.Right * screenPos.X, + UiTransitionAnimation.SlideRight => Vector2.Left * screenPos.X, + UiTransitionAnimation.SlideUp => Vector2.Down * screenPos.Y, + UiTransitionAnimation.SlideDown => Vector2.Up * screenPos.Y, + _ => Vector2.Zero + }; + } + + private static Vector2 GetSlideExitOffset(UiTransitionAnimation direction, Vector2 screenPos) + { + return direction switch + { + UiTransitionAnimation.SlideLeft => Vector2.Left * screenPos.X, + UiTransitionAnimation.SlideRight => Vector2.Right * screenPos.X, + UiTransitionAnimation.SlideUp => Vector2.Up * screenPos.Y, + UiTransitionAnimation.SlideDown => Vector2.Down * screenPos.Y, + _ => Vector2.Zero + }; + } +}