From 9e42167e7285be012c90558a1be2a72fbc5fe315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Tue, 17 Mar 2026 17:15:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=8C=96UIAnimationTransitio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Runtime/UI/Constant/UIHolderFactory.cs | 10 +++ Runtime/UI/Manager/UIModule.Open.cs | 39 +++++++++- Runtime/UI/Other/IUITransitionPlayer.cs | 14 ++++ Runtime/UI/Other/IUITransitionPlayer.cs.meta | 11 +++ Runtime/UI/Other/UIAnimationFlowTransition.cs | 74 ++++++++++++++++++ .../Other/UIAnimationFlowTransition.cs.meta | 11 +++ Runtime/UI/UIBase/UIBase.cs | 77 ++++++++++++++++--- Runtime/UI/UIBase/UIHolderObjectBase.cs | 70 +++++++++++------ Runtime/UI/UIBase/UIState.cs | 2 + Runtime/UI/UIBase/UIStateMachine.cs | 19 +++-- 10 files changed, 287 insertions(+), 40 deletions(-) create mode 100644 Runtime/UI/Other/IUITransitionPlayer.cs create mode 100644 Runtime/UI/Other/IUITransitionPlayer.cs.meta create mode 100644 Runtime/UI/Other/UIAnimationFlowTransition.cs create mode 100644 Runtime/UI/Other/UIAnimationFlowTransition.cs.meta diff --git a/Runtime/UI/Constant/UIHolderFactory.cs b/Runtime/UI/Constant/UIHolderFactory.cs index 449f007..6153310 100644 --- a/Runtime/UI/Constant/UIHolderFactory.cs +++ b/Runtime/UI/Constant/UIHolderFactory.cs @@ -56,6 +56,16 @@ namespace AlicizaX.UI.Runtime { if (meta.State != UIState.CreatedUI) return; GameObject obj = await LoadUIResourcesAsync(meta.ResInfo, parent); + if (meta.CancellationToken.IsCancellationRequested || meta.View == null || meta.State != UIState.CreatedUI) + { + if (obj != null) + { + Object.Destroy(obj); + } + + return; + } + ValidateAndBind(meta, obj, owner); } diff --git a/Runtime/UI/Manager/UIModule.Open.cs b/Runtime/UI/Manager/UIModule.Open.cs index 203ff41..7de4a97 100644 --- a/Runtime/UI/Manager/UIModule.Open.cs +++ b/Runtime/UI/Manager/UIModule.Open.cs @@ -29,6 +29,11 @@ namespace AlicizaX.UI.Runtime { CreateMetaUI(metaInfo); await UIHolderFactory.CreateUIResourceAsync(metaInfo, UICacheLayer); + if (metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed) + { + return null; + } + FinalizeShow(metaInfo, userDatas); await UpdateVisualState(metaInfo, metaInfo.CancellationToken); return metaInfo.View; @@ -38,6 +43,11 @@ namespace AlicizaX.UI.Runtime { CreateMetaUI(metaInfo); UIHolderFactory.CreateUIResourceSync(metaInfo, UICacheLayer); + if (metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed) + { + return null; + } + FinalizeShow(metaInfo, userDatas); UpdateVisualState(metaInfo).Forget(); return metaInfo.View; @@ -45,13 +55,36 @@ namespace AlicizaX.UI.Runtime private async UniTask CloseUIImpl(UIMetadata meta, bool force) { - if (meta.State == UIState.Uninitialized || meta.State == UIState.CreatedUI) + if (meta.State == UIState.Uninitialized) { return; } + if (meta.State == UIState.CreatedUI) + { + meta.CancelAsyncOperations(); + meta.Dispose(); + return; + } + + if (meta.State == UIState.Loaded || meta.State == UIState.Initialized) + { + meta.CancelAsyncOperations(); + Pop(meta); + SortWindowVisible(meta.MetaInfo.UILayer); + SortWindowDepth(meta.MetaInfo.UILayer); + meta.View.Visible = false; + CacheWindow(meta, force); + return; + } + meta.CancelAsyncOperations(); await meta.View.InternalClose(); + if (meta.State != UIState.Closed) + { + return; + } + Pop(meta); SortWindowVisible(meta.MetaInfo.UILayer); SortWindowDepth(meta.MetaInfo.UILayer); @@ -85,6 +118,8 @@ namespace AlicizaX.UI.Runtime case UIState.Loaded: Push(meta); break; + case UIState.Opening: + case UIState.Closing: case UIState.Opened: MoveToTop(meta); break; @@ -186,7 +221,7 @@ namespace AlicizaX.UI.Runtime for (int i = count - 1; i >= 0; i--) { var meta = list[i]; - if (meta.MetaInfo.FullScreen && meta.State == UIState.Opened) + if (meta.MetaInfo.FullScreen && UIStateMachine.IsDisplayActive(meta.State)) { fullscreenIdx = i; break; diff --git a/Runtime/UI/Other/IUITransitionPlayer.cs b/Runtime/UI/Other/IUITransitionPlayer.cs new file mode 100644 index 0000000..e2b9e2b --- /dev/null +++ b/Runtime/UI/Other/IUITransitionPlayer.cs @@ -0,0 +1,14 @@ +using System.Threading; +using Cysharp.Threading.Tasks; + +namespace AlicizaX.UI.Runtime +{ + public interface IUITransitionPlayer + { + UniTask PlayOpenAsync(CancellationToken cancellationToken = default); + + UniTask PlayCloseAsync(CancellationToken cancellationToken = default); + + void Stop(); + } +} diff --git a/Runtime/UI/Other/IUITransitionPlayer.cs.meta b/Runtime/UI/Other/IUITransitionPlayer.cs.meta new file mode 100644 index 0000000..cc51acf --- /dev/null +++ b/Runtime/UI/Other/IUITransitionPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b746e052b3511314993f07f959473956 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UI/Other/UIAnimationFlowTransition.cs b/Runtime/UI/Other/UIAnimationFlowTransition.cs new file mode 100644 index 0000000..2cd3bc4 --- /dev/null +++ b/Runtime/UI/Other/UIAnimationFlowTransition.cs @@ -0,0 +1,74 @@ +using System.Threading; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace AlicizaX.UI.Runtime +{ + [DisallowMultipleComponent] + public sealed class UIAnimationFlowTransition : MonoBehaviour, IUITransitionPlayer + { +#if ALICIZAX_UI_ANIMATION_SUPPORT + [SerializeField] private AnimationFlow.Runtime.AnimationFlow animationFlow; + [SerializeField] private string openClip = "Open"; + [SerializeField] private string closeClip = "Close"; +#endif + + public UniTask PlayOpenAsync(CancellationToken cancellationToken = default) + { +#if ALICIZAX_UI_ANIMATION_SUPPORT + return PlayAsync(openClip, cancellationToken); +#else + return UniTask.CompletedTask; +#endif + } + + public UniTask PlayCloseAsync(CancellationToken cancellationToken = default) + { +#if ALICIZAX_UI_ANIMATION_SUPPORT + return PlayAsync(closeClip, cancellationToken); +#else + return UniTask.CompletedTask; +#endif + } + + public void Stop() + { +#if ALICIZAX_UI_ANIMATION_SUPPORT + ResolveAnimationFlow()?.Stop(); +#endif + } + +#if ALICIZAX_UI_ANIMATION_SUPPORT + private UniTask PlayAsync(string clipName, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested || string.IsNullOrWhiteSpace(clipName)) + { + return UniTask.CompletedTask; + } + + AnimationFlow.Runtime.AnimationFlow flow = ResolveAnimationFlow(); + return flow == null ? UniTask.CompletedTask : flow.PlayAsync(clipName); + } + + private AnimationFlow.Runtime.AnimationFlow ResolveAnimationFlow() + { + if (animationFlow == null) + { + animationFlow = GetComponent(); + } + + return animationFlow; + } + +#if UNITY_EDITOR + private void OnValidate() + { + if (animationFlow == null) + { + animationFlow = GetComponent(); + } + } +#endif +#endif + } +} diff --git a/Runtime/UI/Other/UIAnimationFlowTransition.cs.meta b/Runtime/UI/Other/UIAnimationFlowTransition.cs.meta new file mode 100644 index 0000000..bb0d317 --- /dev/null +++ b/Runtime/UI/Other/UIAnimationFlowTransition.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ad79303854072f4798edbea92187a26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UI/UIBase/UIBase.cs b/Runtime/UI/UIBase/UIBase.cs index eabaac6..b7ad8fe 100644 --- a/Runtime/UI/UIBase/UIBase.cs +++ b/Runtime/UI/UIBase/UIBase.cs @@ -22,6 +22,7 @@ namespace AlicizaX.UI.Runtime internal Canvas _canvas; internal GraphicRaycaster _raycaster; + private int _lifecycleVersion; internal UIState _state = UIState.Uninitialized; internal UIState State => _state; @@ -217,31 +218,67 @@ namespace AlicizaX.UI.Runtime internal async UniTask InternalOpen(CancellationToken cancellationToken = default) { - if (_state == UIState.Opened) - return; // Already open + if (_state == UIState.Opened || _state == UIState.Opening) + return; - if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Opened)) + if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Opening)) + return; + + int lifecycleVersion = BeginLifecycleTransition(); + _state = UIState.Opening; + Visible = true; + Holder.OnWindowBeforeShowEvent?.Invoke(); + try + { + await OnOpenAsync(cancellationToken); + if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening)) + return; + + cancellationToken.ThrowIfCancellationRequested(); + await Holder.PlayOpenTransitionAsync(cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + return; + } + + if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening)) return; _state = UIState.Opened; - Visible = true; - Holder.OnWindowBeforeShowEvent?.Invoke(); - await OnOpenAsync(cancellationToken); Holder.OnWindowAfterShowEvent?.Invoke(); } internal async UniTask InternalClose(CancellationToken cancellationToken = default) { - if (_state != UIState.Opened) + if (_state == UIState.Closed || _state == UIState.Closing) return; - if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Closed)) + if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Closing)) return; + int lifecycleVersion = BeginLifecycleTransition(); + _state = UIState.Closing; Holder.OnWindowBeforeClosedEvent?.Invoke(); - await OnCloseAsync(cancellationToken); - _state = UIState.Closed; + try + { + await OnCloseAsync(cancellationToken); + if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing)) + return; + + cancellationToken.ThrowIfCancellationRequested(); + await Holder.PlayCloseTransitionAsync(cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + return; + } + + if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing)) + return; + Visible = false; + _state = UIState.Closed; Holder.OnWindowAfterClosedEvent?.Invoke(); } @@ -257,8 +294,9 @@ namespace AlicizaX.UI.Runtime if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Destroying)) return; + InterruptLifecycleTransition(); _state = UIState.Destroying; - Holder.OnWindowDestroyEvent?.Invoke(); + Holder?.OnWindowDestroyEvent?.Invoke(); await DestroyAllChildren(); OnDestroy(); ReleaseEventListenerProxy(); @@ -271,6 +309,23 @@ namespace AlicizaX.UI.Runtime this._userDatas = userDatas; } + private int BeginLifecycleTransition() + { + InterruptLifecycleTransition(); + return _lifecycleVersion; + } + + private void InterruptLifecycleTransition() + { + _lifecycleVersion++; + Holder?.StopTransition(); + } + + private bool IsCurrentLifecycleTransition(int lifecycleVersion, UIState state) + { + return lifecycleVersion == _lifecycleVersion && _state == state; + } + #endregion } } diff --git a/Runtime/UI/UIBase/UIHolderObjectBase.cs b/Runtime/UI/UIBase/UIHolderObjectBase.cs index a1de9d5..efb3095 100644 --- a/Runtime/UI/UIBase/UIHolderObjectBase.cs +++ b/Runtime/UI/UIBase/UIHolderObjectBase.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; @@ -15,27 +16,14 @@ namespace AlicizaX.UI.Runtime public Action OnWindowDestroyEvent; -#if ALICIZAX_UI_ANIMATION_SUPPORT - public async UniTask PlayAnimtion(string name) - { - if (AnimationFlow == null) - { - AnimationFlow = transform.GetComponent(); - } - - await AnimationFlow.PlayAsync(name); - } -#endif - - private GameObject _target; + private IUITransitionPlayer _transitionPlayer; /// /// UI实例资源对象。 /// public GameObject Target => _target ??= gameObject; - private RectTransform _rectTransform; /// @@ -53,30 +41,68 @@ namespace AlicizaX.UI.Runtime internal set { _target.SetActive(value); } } -#if ALICIZAX_UI_ANIMATION_SUPPORT - private AnimationFlow.Runtime.AnimationFlow AnimationFlow; -#endif - public virtual void Awake() { _target = gameObject; -#if ALICIZAX_UI_ANIMATION_SUPPORT - AnimationFlow = GetComponent(); -#endif } private bool _isAlive = true; - public bool IsValid() { return this != null && _isAlive; } + internal UniTask PlayOpenTransitionAsync(CancellationToken cancellationToken = default) + { + return TryGetTransitionPlayer(out var transitionPlayer) + ? transitionPlayer.PlayOpenAsync(cancellationToken) + : UniTask.CompletedTask; + } + + internal UniTask PlayCloseTransitionAsync(CancellationToken cancellationToken = default) + { + return TryGetTransitionPlayer(out var transitionPlayer) + ? transitionPlayer.PlayCloseAsync(cancellationToken) + : UniTask.CompletedTask; + } + + internal void StopTransition() + { + if (TryGetTransitionPlayer(out var transitionPlayer)) + { + transitionPlayer.Stop(); + } + } + + private bool TryGetTransitionPlayer(out IUITransitionPlayer transitionPlayer) + { + if (_transitionPlayer != null) + { + transitionPlayer = _transitionPlayer; + return true; + } + + MonoBehaviour[] behaviours = GetComponents(); + for (int i = 0; i < behaviours.Length; i++) + { + if (behaviours[i] is IUITransitionPlayer player) + { + _transitionPlayer = player; + transitionPlayer = player; + return true; + } + } + + transitionPlayer = null; + return false; + } + private void OnDestroy() { _isAlive = false; + _transitionPlayer = null; } } } diff --git a/Runtime/UI/UIBase/UIState.cs b/Runtime/UI/UIBase/UIState.cs index da5b703..f3743e8 100644 --- a/Runtime/UI/UIBase/UIState.cs +++ b/Runtime/UI/UIBase/UIState.cs @@ -6,7 +6,9 @@ CreatedUI, Loaded, Initialized, + Opening, Opened, + Closing, Closed, Destroying, Destroyed, diff --git a/Runtime/UI/UIBase/UIStateMachine.cs b/Runtime/UI/UIBase/UIStateMachine.cs index 016963c..4fa6679 100644 --- a/Runtime/UI/UIBase/UIStateMachine.cs +++ b/Runtime/UI/UIBase/UIStateMachine.cs @@ -9,11 +9,13 @@ namespace AlicizaX.UI.Runtime private static readonly Dictionary> _validTransitions = new() { [UIState.Uninitialized] = new() { UIState.CreatedUI }, - [UIState.CreatedUI] = new() { UIState.Loaded }, - [UIState.Loaded] = new() { UIState.Initialized }, - [UIState.Initialized] = new() { UIState.Opened }, - [UIState.Opened] = new() { UIState.Closed, UIState.Destroying }, - [UIState.Closed] = new() { UIState.Opened, UIState.Destroying }, + [UIState.CreatedUI] = new() { UIState.Loaded, UIState.Destroying }, + [UIState.Loaded] = new() { UIState.Initialized, UIState.Destroying }, + [UIState.Initialized] = new() { UIState.Opening, UIState.Destroying }, + [UIState.Opening] = new() { UIState.Opened, UIState.Closing, UIState.Destroying }, + [UIState.Opened] = new() { UIState.Closing, UIState.Destroying }, + [UIState.Closing] = new() { UIState.Opening, UIState.Closed, UIState.Destroying }, + [UIState.Closed] = new() { UIState.Opening, UIState.Destroying }, [UIState.Destroying] = new() { UIState.Destroyed }, [UIState.Destroyed] = new() { } }; @@ -48,12 +50,19 @@ namespace AlicizaX.UI.Runtime UIState.CreatedUI => "UI logic created, awaiting resource load", UIState.Loaded => "Resources loaded, awaiting initialization", UIState.Initialized => "Initialized, ready to open", + UIState.Opening => "Opening transition is running", UIState.Opened => "Currently visible and active", + UIState.Closing => "Closing transition is running", UIState.Closed => "Hidden but cached", UIState.Destroying => "Being destroyed", UIState.Destroyed => "Fully destroyed", _ => "Unknown state" }; } + + public static bool IsDisplayActive(UIState state) + { + return state == UIState.Opening || state == UIState.Opened || state == UIState.Closing; + } } }