com.alicizax.unity.framework/Runtime/UI/Other/UIPresetTransition.cs

418 lines
13 KiB
C#
Raw Normal View History

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