From b9c79e16a50080ba10b08a9b70bb513b36f51d1e 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:48:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=80=9A=E7=94=A8UITransitio?= =?UTF-8?q?nPreset=20=E4=BC=98=E5=8C=96=E9=83=A8=E5=88=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Inspector/UIPresetTransitionInspector.cs | 140 ++++++ .../UIPresetTransitionInspector.cs.meta | 11 + Runtime/UI/Other/IUITransitionPlayer.cs | 2 + Runtime/UI/Other/UIAnimationFlowTransition.cs | 2 + Runtime/UI/Other/UIPresetTransition.cs | 417 ++++++++++++++++++ Runtime/UI/Other/UIPresetTransition.cs.meta | 11 + Runtime/UI/UIBase/UIHolderObjectBase.cs | 20 +- 7 files changed, 596 insertions(+), 7 deletions(-) create mode 100644 Editor/UI/Inspector/UIPresetTransitionInspector.cs create mode 100644 Editor/UI/Inspector/UIPresetTransitionInspector.cs.meta create mode 100644 Runtime/UI/Other/UIPresetTransition.cs create mode 100644 Runtime/UI/Other/UIPresetTransition.cs.meta diff --git a/Editor/UI/Inspector/UIPresetTransitionInspector.cs b/Editor/UI/Inspector/UIPresetTransitionInspector.cs new file mode 100644 index 0000000..35a847c --- /dev/null +++ b/Editor/UI/Inspector/UIPresetTransitionInspector.cs @@ -0,0 +1,140 @@ +using AlicizaX.UI.Runtime; +using UnityEditor; +using UnityEngine; + +namespace AlicizaX.UI.Editor +{ + [CustomEditor(typeof(UIPresetTransition))] + internal sealed class UIPresetTransitionInspector : UnityEditor.Editor + { + private enum PreviewMode + { + Open, + Close, + } + + private UIPresetTransition _transition; + private PreviewMode _previewMode = PreviewMode.Open; + private float _previewProgress = 1f; + + private void OnEnable() + { + _transition = (UIPresetTransition)target; + } + + private void OnDisable() + { + StopPreview(); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + EditorGUI.BeginChangeCheck(); + DrawDefaultInspector(); + bool inspectorChanged = EditorGUI.EndChangeCheck(); + serializedObject.ApplyModifiedProperties(); + + EditorGUILayout.Space(); + DrawPreviewGUI(); + + if (inspectorChanged && _transition != null && _transition.EditorHasActivePreview()) + { + ApplyPreview(); + } + } + + private void DrawPreviewGUI() + { + EditorGUILayout.LabelField("Editor Preview", EditorStyles.boldLabel); + + if (targets.Length != 1) + { + EditorGUILayout.HelpBox("Preview is available when a single UIPresetTransition is selected.", MessageType.Info); + return; + } + + if (EditorApplication.isPlayingOrWillChangePlaymode) + { + EditorGUILayout.HelpBox("Preset preview is only available in edit mode.", MessageType.Info); + return; + } + + EditorGUILayout.HelpBox("Preview scrubs the selected transition in edit mode and restores automatically when this inspector closes.", MessageType.None); + + EditorGUI.BeginChangeCheck(); + _previewMode = (PreviewMode)EditorGUILayout.EnumPopup("Transition", _previewMode); + _previewProgress = EditorGUILayout.Slider("Progress", _previewProgress, 0f, 1f); + if (EditorGUI.EndChangeCheck()) + { + ApplyPreview(); + } + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("At Start")) + { + _previewProgress = 0f; + ApplyPreview(); + } + + if (GUILayout.Button("At End")) + { + _previewProgress = 1f; + ApplyPreview(); + } + + if (GUILayout.Button("Restore")) + { + StopPreview(); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Preview Open")) + { + _previewMode = PreviewMode.Open; + _previewProgress = 1f; + ApplyPreview(); + } + + if (GUILayout.Button("Preview Close")) + { + _previewMode = PreviewMode.Close; + _previewProgress = 1f; + ApplyPreview(); + } + EditorGUILayout.EndHorizontal(); + } + + private void ApplyPreview() + { + if (_transition == null) + { + return; + } + + if (_previewMode == PreviewMode.Open) + { + _transition.EditorPreviewOpen(_previewProgress); + } + else + { + _transition.EditorPreviewClose(_previewProgress); + } + + SceneView.RepaintAll(); + Repaint(); + } + + private void StopPreview() + { + if (_transition == null) + { + return; + } + + _transition.EditorStopPreview(); + SceneView.RepaintAll(); + } + } +} diff --git a/Editor/UI/Inspector/UIPresetTransitionInspector.cs.meta b/Editor/UI/Inspector/UIPresetTransitionInspector.cs.meta new file mode 100644 index 0000000..c1f09cb --- /dev/null +++ b/Editor/UI/Inspector/UIPresetTransitionInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a0487242afa85324b988eb4c7a15a671 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UI/Other/IUITransitionPlayer.cs b/Runtime/UI/Other/IUITransitionPlayer.cs index e2b9e2b..bf12163 100644 --- a/Runtime/UI/Other/IUITransitionPlayer.cs +++ b/Runtime/UI/Other/IUITransitionPlayer.cs @@ -5,6 +5,8 @@ namespace AlicizaX.UI.Runtime { public interface IUITransitionPlayer { + int Priority { get; } + UniTask PlayOpenAsync(CancellationToken cancellationToken = default); UniTask PlayCloseAsync(CancellationToken cancellationToken = default); diff --git a/Runtime/UI/Other/UIAnimationFlowTransition.cs b/Runtime/UI/Other/UIAnimationFlowTransition.cs index 2cd3bc4..389e651 100644 --- a/Runtime/UI/Other/UIAnimationFlowTransition.cs +++ b/Runtime/UI/Other/UIAnimationFlowTransition.cs @@ -7,6 +7,8 @@ namespace AlicizaX.UI.Runtime [DisallowMultipleComponent] public sealed class UIAnimationFlowTransition : MonoBehaviour, IUITransitionPlayer { + public int Priority => 0; + #if ALICIZAX_UI_ANIMATION_SUPPORT [SerializeField] private AnimationFlow.Runtime.AnimationFlow animationFlow; [SerializeField] private string openClip = "Open"; diff --git a/Runtime/UI/Other/UIPresetTransition.cs b/Runtime/UI/Other/UIPresetTransition.cs new file mode 100644 index 0000000..3a2abad --- /dev/null +++ b/Runtime/UI/Other/UIPresetTransition.cs @@ -0,0 +1,417 @@ +using System.Threading; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace AlicizaX.UI.Runtime +{ + public enum UITransitionPreset + { + None, + Fade, + Scale, + FadeScale, + SlideFromBottom, + SlideFromTop, + SlideFromLeft, + SlideFromRight, + Toast, + } + + public enum UITransitionEase + { + Linear, + InQuad, + OutQuad, + InCubic, + OutCubic, + InOutCubic, + OutBack, + } + + [DisallowMultipleComponent] + public sealed class UIPresetTransition : MonoBehaviour, IUITransitionPlayer + { + private struct VisualState + { + public Vector2 AnchoredPosition; + public Vector3 Scale; + public float Alpha; + } + + public int Priority => 100; + + [SerializeField] private UITransitionPreset openPreset = UITransitionPreset.FadeScale; + [SerializeField] private UITransitionPreset closePreset = UITransitionPreset.FadeScale; + [SerializeField] private UITransitionEase openEase = UITransitionEase.OutCubic; + [SerializeField] private UITransitionEase closeEase = UITransitionEase.InCubic; + [SerializeField] private RectTransform targetRect; + [SerializeField] private CanvasGroup canvasGroup; + [SerializeField] private bool useUnscaledTime = true; + [SerializeField] private bool initializeAsClosed = true; + [SerializeField] private bool disableInteractionWhilePlaying = true; + [SerializeField] [Min(0f)] private float openDuration = 0.22f; + [SerializeField] [Min(0f)] private float closeDuration = 0.18f; + [SerializeField] [Min(0f)] private float slideDistance = 120f; + [SerializeField] [Min(0f)] private float toastDistance = 40f; + [SerializeField] [Range(0.5f, 1f)] private float closedScale = 0.94f; + + private bool _initialized; + private int _playVersion; + private VisualState _openState; + +#if UNITY_EDITOR + private bool _editorPreviewActive; + private VisualState _editorPreviewRestoreState; + private bool _editorPreviewRestoreInteractable; + private bool _editorPreviewRestoreBlocksRaycasts; +#endif + + private void Awake() + { + EnsureInitialized(initializeAsClosed); + } + + private void OnDisable() + { + Stop(); +#if UNITY_EDITOR + EditorStopPreview(); +#endif + } + + public UniTask PlayOpenAsync(CancellationToken cancellationToken = default) + { + EnsureInitialized(false); + return PlayAsync(_openState, openDuration, openEase, true, cancellationToken); + } + + public UniTask PlayCloseAsync(CancellationToken cancellationToken = default) + { + EnsureInitialized(false); + return PlayAsync(BuildClosedState(closePreset), closeDuration, closeEase, false, cancellationToken); + } + + public void Stop() + { + _playVersion++; + RestoreInteractionState(true); + } + + private async UniTask PlayAsync( + VisualState targetState, + float duration, + UITransitionEase ease, + bool isOpening, + CancellationToken cancellationToken) + { + int playVersion = ++_playVersion; + RestoreInteractionState(!disableInteractionWhilePlaying); + + VisualState currentState = CaptureCurrentState(); + if (duration <= 0f) + { + ApplyVisualState(targetState); + RestoreInteractionState(isOpening); + return; + } + + float elapsed = 0f; + while (elapsed < duration) + { + if (playVersion != _playVersion || cancellationToken.IsCancellationRequested) + { + return; + } + + elapsed = Mathf.Min(elapsed + GetDeltaTime(), duration); + float t = Evaluate(ease, elapsed / duration); + ApplyVisualState(Lerp(currentState, targetState, t)); + await UniTask.Yield(PlayerLoopTiming.Update); + } + + if (playVersion != _playVersion || cancellationToken.IsCancellationRequested) + { + return; + } + + ApplyVisualState(targetState); + RestoreInteractionState(isOpening); + } + + private void EnsureInitialized(bool applyClosedState) + { + if (_initialized) + { + return; + } + + if (targetRect == null) + { + targetRect = transform as RectTransform; + } + + if (RequiresCanvasGroup() && canvasGroup == null) + { + canvasGroup = GetComponent(); + if (canvasGroup == null) + { + canvasGroup = gameObject.AddComponent(); + } + } + + _openState = CaptureCurrentState(); + _initialized = true; + + if (applyClosedState) + { + UITransitionPreset initialPreset = openPreset != UITransitionPreset.None ? openPreset : closePreset; + if (initialPreset != UITransitionPreset.None) + { + ApplyVisualState(BuildClosedState(initialPreset)); + RestoreInteractionState(false); + } + } + } + + private bool RequiresCanvasGroup() + { + return disableInteractionWhilePlaying || UsesAlpha(openPreset) || UsesAlpha(closePreset); + } + + private bool UsesAlpha(UITransitionPreset preset) + { + switch (preset) + { + case UITransitionPreset.Fade: + case UITransitionPreset.FadeScale: + case UITransitionPreset.SlideFromBottom: + case UITransitionPreset.SlideFromTop: + case UITransitionPreset.SlideFromLeft: + case UITransitionPreset.SlideFromRight: + case UITransitionPreset.Toast: + return true; + default: + return false; + } + } + + private VisualState BuildClosedState(UITransitionPreset preset) + { + VisualState state = _openState; + + switch (preset) + { + case UITransitionPreset.None: + return state; + case UITransitionPreset.Fade: + state.Alpha = 0f; + break; + case UITransitionPreset.Scale: + state.Scale = Vector3.Scale(_openState.Scale, Vector3.one * closedScale); + break; + case UITransitionPreset.FadeScale: + state.Alpha = 0f; + state.Scale = Vector3.Scale(_openState.Scale, Vector3.one * closedScale); + break; + case UITransitionPreset.SlideFromBottom: + state.Alpha = 0f; + state.AnchoredPosition = _openState.AnchoredPosition + new Vector2(0f, -slideDistance); + break; + case UITransitionPreset.SlideFromTop: + state.Alpha = 0f; + state.AnchoredPosition = _openState.AnchoredPosition + new Vector2(0f, slideDistance); + break; + case UITransitionPreset.SlideFromLeft: + state.Alpha = 0f; + state.AnchoredPosition = _openState.AnchoredPosition + new Vector2(-slideDistance, 0f); + break; + case UITransitionPreset.SlideFromRight: + state.Alpha = 0f; + state.AnchoredPosition = _openState.AnchoredPosition + new Vector2(slideDistance, 0f); + break; + case UITransitionPreset.Toast: + state.Alpha = 0f; + state.Scale = Vector3.Scale(_openState.Scale, Vector3.one * 0.98f); + state.AnchoredPosition = _openState.AnchoredPosition + new Vector2(0f, -toastDistance); + break; + } + + return state; + } + + private VisualState CaptureCurrentState() + { + return new VisualState + { + AnchoredPosition = targetRect != null ? targetRect.anchoredPosition : Vector2.zero, + Scale = targetRect != null ? targetRect.localScale : transform.localScale, + Alpha = canvasGroup != null ? canvasGroup.alpha : 1f, + }; + } + + private void ApplyVisualState(VisualState state) + { + if (targetRect != null) + { + targetRect.anchoredPosition = state.AnchoredPosition; + targetRect.localScale = state.Scale; + } + else + { + transform.localScale = state.Scale; + } + + if (canvasGroup != null) + { + canvasGroup.alpha = state.Alpha; + } + } + + private void RestoreInteractionState(bool enabled) + { + if (!disableInteractionWhilePlaying || canvasGroup == null) + { + return; + } + + canvasGroup.interactable = enabled; + canvasGroup.blocksRaycasts = enabled; + } + + private float GetDeltaTime() + { + return useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; + } + + private VisualState Lerp(VisualState from, VisualState to, float t) + { + return new VisualState + { + AnchoredPosition = Vector2.LerpUnclamped(from.AnchoredPosition, to.AnchoredPosition, t), + Scale = Vector3.LerpUnclamped(from.Scale, to.Scale, t), + Alpha = Mathf.LerpUnclamped(from.Alpha, to.Alpha, t), + }; + } + + private float Evaluate(UITransitionEase ease, float t) + { + t = Mathf.Clamp01(t); + switch (ease) + { + case UITransitionEase.InQuad: + return t * t; + case UITransitionEase.OutQuad: + return 1f - (1f - t) * (1f - t); + case UITransitionEase.InCubic: + return t * t * t; + case UITransitionEase.OutCubic: + { + float inv = 1f - t; + return 1f - inv * inv * inv; + } + case UITransitionEase.InOutCubic: + return t < 0.5f + ? 4f * t * t * t + : 1f - Mathf.Pow(-2f * t + 2f, 3f) * 0.5f; + case UITransitionEase.OutBack: + { + const float c1 = 1.70158f; + const float c3 = c1 + 1f; + float inv = t - 1f; + return 1f + c3 * inv * inv * inv + c1 * inv * inv; + } + default: + return t; + } + } + +#if UNITY_EDITOR + internal void EditorPreviewOpen(float progress) + { + EditorPreview(true, progress); + } + + internal void EditorPreviewClose(float progress) + { + EditorPreview(false, progress); + } + + internal void EditorStopPreview() + { + if (!_editorPreviewActive) + { + return; + } + + ApplyVisualState(_editorPreviewRestoreState); + if (canvasGroup != null) + { + canvasGroup.interactable = _editorPreviewRestoreInteractable; + canvasGroup.blocksRaycasts = _editorPreviewRestoreBlocksRaycasts; + } + + _editorPreviewActive = false; + } + + internal bool EditorHasActivePreview() + { + return _editorPreviewActive; + } + + private void EditorPreview(bool isOpening, float progress) + { + EnsureInitialized(false); + BeginEditorPreview(); + + progress = Mathf.Clamp01(progress); + UITransitionPreset preset = isOpening ? openPreset : closePreset; + UITransitionEase ease = isOpening ? openEase : closeEase; + VisualState closedState = BuildClosedState(preset); + float easedProgress = Evaluate(ease, progress); + + ApplyVisualState(isOpening + ? Lerp(closedState, _openState, easedProgress) + : Lerp(_openState, closedState, easedProgress)); + + if (canvasGroup != null && disableInteractionWhilePlaying) + { + bool enabled = isOpening ? progress >= 0.999f : progress <= 0.001f; + canvasGroup.interactable = enabled; + canvasGroup.blocksRaycasts = enabled; + } + } + + private void BeginEditorPreview() + { + if (_editorPreviewActive) + { + return; + } + + _editorPreviewRestoreState = CaptureCurrentState(); + _openState = _editorPreviewRestoreState; + + if (canvasGroup != null) + { + _editorPreviewRestoreInteractable = canvasGroup.interactable; + _editorPreviewRestoreBlocksRaycasts = canvasGroup.blocksRaycasts; + } + + _editorPreviewActive = true; + } + + private void OnValidate() + { + if (targetRect == null) + { + targetRect = transform as RectTransform; + } + + if (canvasGroup == null) + { + canvasGroup = GetComponent(); + } + } +#endif + } +} diff --git a/Runtime/UI/Other/UIPresetTransition.cs.meta b/Runtime/UI/Other/UIPresetTransition.cs.meta new file mode 100644 index 0000000..394cedf --- /dev/null +++ b/Runtime/UI/Other/UIPresetTransition.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b880940ee89d20e4fa6f2630d7a81a14 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UI/UIBase/UIHolderObjectBase.cs b/Runtime/UI/UIBase/UIHolderObjectBase.cs index efb3095..4a1a0ba 100644 --- a/Runtime/UI/UIBase/UIHolderObjectBase.cs +++ b/Runtime/UI/UIBase/UIHolderObjectBase.cs @@ -15,7 +15,6 @@ namespace AlicizaX.UI.Runtime public Action OnWindowAfterClosedEvent; public Action OnWindowDestroyEvent; - private GameObject _target; private IUITransitionPlayer _transitionPlayer; @@ -78,25 +77,32 @@ namespace AlicizaX.UI.Runtime private bool TryGetTransitionPlayer(out IUITransitionPlayer transitionPlayer) { - if (_transitionPlayer != null) + if (_transitionPlayer is Behaviour cachedBehaviour && cachedBehaviour.isActiveAndEnabled) { transitionPlayer = _transitionPlayer; return true; } + _transitionPlayer = null; + int bestPriority = int.MinValue; MonoBehaviour[] behaviours = GetComponents(); for (int i = 0; i < behaviours.Length; i++) { - if (behaviours[i] is IUITransitionPlayer player) + MonoBehaviour behaviour = behaviours[i]; + if (!behaviour.isActiveAndEnabled || behaviour is not IUITransitionPlayer player) + { + continue; + } + + if (player.Priority > bestPriority) { _transitionPlayer = player; - transitionPlayer = player; - return true; + bestPriority = player.Priority; } } - transitionPlayer = null; - return false; + transitionPlayer = _transitionPlayer; + return transitionPlayer != null; } private void OnDestroy()