418 lines
13 KiB
C#
418 lines
13 KiB
C#
|
|
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
|
||
|
|
}
|
||
|
|
}
|