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
+ };
+ }
+}