增加通用UITransitionPreset 优化部分逻辑结构

This commit is contained in:
陈思海 2026-03-17 17:48:43 +08:00
parent 9e42167e72
commit b9c79e16a5
7 changed files with 596 additions and 7 deletions

View File

@ -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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a0487242afa85324b988eb4c7a15a671
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -5,6 +5,8 @@ namespace AlicizaX.UI.Runtime
{ {
public interface IUITransitionPlayer public interface IUITransitionPlayer
{ {
int Priority { get; }
UniTask PlayOpenAsync(CancellationToken cancellationToken = default); UniTask PlayOpenAsync(CancellationToken cancellationToken = default);
UniTask PlayCloseAsync(CancellationToken cancellationToken = default); UniTask PlayCloseAsync(CancellationToken cancellationToken = default);

View File

@ -7,6 +7,8 @@ namespace AlicizaX.UI.Runtime
[DisallowMultipleComponent] [DisallowMultipleComponent]
public sealed class UIAnimationFlowTransition : MonoBehaviour, IUITransitionPlayer public sealed class UIAnimationFlowTransition : MonoBehaviour, IUITransitionPlayer
{ {
public int Priority => 0;
#if ALICIZAX_UI_ANIMATION_SUPPORT #if ALICIZAX_UI_ANIMATION_SUPPORT
[SerializeField] private AnimationFlow.Runtime.AnimationFlow animationFlow; [SerializeField] private AnimationFlow.Runtime.AnimationFlow animationFlow;
[SerializeField] private string openClip = "Open"; [SerializeField] private string openClip = "Open";

View File

@ -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<CanvasGroup>();
if (canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
}
_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<CanvasGroup>();
}
}
#endif
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b880940ee89d20e4fa6f2630d7a81a14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -15,7 +15,6 @@ namespace AlicizaX.UI.Runtime
public Action OnWindowAfterClosedEvent; public Action OnWindowAfterClosedEvent;
public Action OnWindowDestroyEvent; public Action OnWindowDestroyEvent;
private GameObject _target; private GameObject _target;
private IUITransitionPlayer _transitionPlayer; private IUITransitionPlayer _transitionPlayer;
@ -78,25 +77,32 @@ namespace AlicizaX.UI.Runtime
private bool TryGetTransitionPlayer(out IUITransitionPlayer transitionPlayer) private bool TryGetTransitionPlayer(out IUITransitionPlayer transitionPlayer)
{ {
if (_transitionPlayer != null) if (_transitionPlayer is Behaviour cachedBehaviour && cachedBehaviour.isActiveAndEnabled)
{ {
transitionPlayer = _transitionPlayer; transitionPlayer = _transitionPlayer;
return true; return true;
} }
_transitionPlayer = null;
int bestPriority = int.MinValue;
MonoBehaviour[] behaviours = GetComponents<MonoBehaviour>(); MonoBehaviour[] behaviours = GetComponents<MonoBehaviour>();
for (int i = 0; i < behaviours.Length; i++) 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;
transitionPlayer = player; bestPriority = player.Priority;
return true;
} }
} }
transitionPlayer = null; transitionPlayer = _transitionPlayer;
return false; return transitionPlayer != null;
} }
private void OnDestroy() private void OnDestroy()