using System.Collections; using DG.Tweening; using UnityEngine; using UnityEngine.UI; namespace AibisDream.UI { /// /// 全局屏幕转场面板。In 决定遮罩颜色和盖入方式,Out 决定揭出方式。 /// public class ScreenTransitionPanel : MonoBehaviour, IUIPanel { private enum TransitionVisual { None, Overlay, Shutter } private const float DefaultWipeDuration = 0.6f; private const float DefaultShutterDuration = 0.6f; [SerializeField] private GameObject transitionVisualRoot; [SerializeField] private Image overlayImage; [SerializeField] private RectTransform shutterRoot; [SerializeField] private Image shutterTop; [SerializeField] private Image shutterBottom; private Color _coverColor = Color.black; private TransitionVisual _currentVisual = TransitionVisual.None; private bool _isCovered; private int _transitionVersion; public bool IsOpen => gameObject.activeSelf; public bool IsCloseable => false; private RectTransform OverlayRect => overlayImage.rectTransform; private void Awake() { ResetVisualsImmediate(); } private void OnDisable() { CancelActiveTransition(); } public IEnumerator FadeInAsync(float duration = 1f, bool useUnscaledTime = false) { return FadeInInternal(Color.black, duration, useUnscaledTime); } public IEnumerator FadeInWhiteAsync(float duration = 1f, bool useUnscaledTime = false) { return FadeInInternal(Color.white, duration, useUnscaledTime); } public IEnumerator FadeOutAsync(float duration = 1f, bool useUnscaledTime = false) { var version = BeginTransition(); PrepareOverlayForFadeOut(); duration = Mathf.Max(0f, duration); if (duration <= 0f) { if (version == _transitionVersion) { ResetVisualsImmediate(); } yield break; } var tween = overlayImage.DOFade(0f, duration) .SetUpdate(useUnscaledTime) .SetTarget(this); yield return tween.WaitForCompletion(); if (version == _transitionVersion) { ResetVisualsImmediate(); } } public void FadeInSync(float duration = 1f) { StartCoroutine(FadeInAsync(duration)); } public void FadeOutSync(float duration = 1f) { StartCoroutine(FadeOutAsync(duration)); } public IEnumerator FlashWhite(float duration = 0.2f, bool useUnscaledTime = false) { var version = BeginTransition(); PrepareVisibleOverlay(Color.white, 0f); _isCovered = false; duration = Mathf.Max(0f, duration); if (duration <= 0f) { if (version == _transitionVersion) { ResetVisualsImmediate(); } yield break; } var halfDuration = duration * 0.5f; var sequence = DOTween.Sequence() .Append(overlayImage.DOFade(1f, halfDuration)) .Append(overlayImage.DOFade(0f, halfDuration)) .SetUpdate(useUnscaledTime) .SetTarget(this); yield return sequence.WaitForCompletion(); if (version == _transitionVersion) { ResetVisualsImmediate(); } } public IEnumerator WipeInAsync(float duration = DefaultWipeDuration, bool useUnscaledTime = false) { var version = BeginTransition(); PrepareVisibleOverlay(Color.black, 1f); _isCovered = false; var width = GetTransitionWidth(); OverlayRect.anchoredPosition = new Vector2(-width, 0f); duration = Mathf.Max(0f, duration); if (duration <= 0f) { OverlayRect.anchoredPosition = Vector2.zero; CompleteCover(version); yield break; } var tween = OverlayRect.DOAnchorPosX(0f, duration) .SetEase(Ease.InOutQuad) .SetUpdate(useUnscaledTime) .SetTarget(this); yield return tween.WaitForCompletion(); CompleteCover(version); } public IEnumerator WipeOutAsync(float duration = DefaultWipeDuration, bool useUnscaledTime = false) { var version = BeginTransition(); PrepareOverlayCovered(); var width = GetTransitionWidth(); duration = Mathf.Max(0f, duration); if (duration <= 0f) { OverlayRect.anchoredPosition = new Vector2(width, 0f); if (version == _transitionVersion) { ResetVisualsImmediate(); } yield break; } var tween = OverlayRect.DOAnchorPosX(width, duration) .SetEase(Ease.InOutQuad) .SetUpdate(useUnscaledTime) .SetTarget(this); yield return tween.WaitForCompletion(); if (version == _transitionVersion) { ResetVisualsImmediate(); } } public IEnumerator ShutterInAsync(float duration = DefaultShutterDuration, bool useUnscaledTime = false) { var version = BeginTransition(); _coverColor = Color.black; PrepareShutter(_coverColor, 0f); _isCovered = false; duration = Mathf.Max(0f, duration); if (duration <= 0f) { SetShutterScale(1f); CompleteCover(version); yield break; } var sequence = DOTween.Sequence() .Join(shutterTop.rectTransform.DOScaleY(1f, duration)) .Join(shutterBottom.rectTransform.DOScaleY(1f, duration)) .SetEase(Ease.InOutQuad) .SetUpdate(useUnscaledTime) .SetTarget(this); yield return sequence.WaitForCompletion(); CompleteCover(version); } public IEnumerator ShutterOutAsync(float duration = DefaultShutterDuration, bool useUnscaledTime = false) { var version = BeginTransition(); if (!transitionVisualRoot.activeSelf) { _coverColor = Color.black; } PrepareShutter(_coverColor, 1f); duration = Mathf.Max(0f, duration); if (duration <= 0f) { SetShutterScale(0f); if (version == _transitionVersion) { ResetVisualsImmediate(); } yield break; } var sequence = DOTween.Sequence() .Join(shutterTop.rectTransform.DOScaleY(0f, duration)) .Join(shutterBottom.rectTransform.DOScaleY(0f, duration)) .SetEase(Ease.InOutQuad) .SetUpdate(useUnscaledTime) .SetTarget(this); yield return sequence.WaitForCompletion(); if (version == _transitionVersion) { ResetVisualsImmediate(); } } public bool CaptureScreenCovered() { return _isCovered; } public void ApplyScreenCovered(bool isCovered) { BeginTransition(); if (!isCovered) { ResetVisualsImmediate(); return; } PrepareVisibleOverlay(Color.black, 1f); _isCovered = true; } public void Show() { gameObject.SetActive(true); } public void Hide() { BeginTransition(); ResetVisualsImmediate(); gameObject.SetActive(false); } private IEnumerator FadeInInternal(Color color, float duration, bool useUnscaledTime) { var version = BeginTransition(); var startAlpha = transitionVisualRoot.activeSelf && _currentVisual == TransitionVisual.Overlay ? overlayImage.color.a : 0f; PrepareVisibleOverlay(color, startAlpha); _isCovered = false; duration = Mathf.Max(0f, duration); if (duration <= 0f) { SetImageAlpha(overlayImage, 1f); CompleteCover(version); yield break; } var tween = overlayImage.DOFade(1f, duration) .SetUpdate(useUnscaledTime) .SetTarget(this); yield return tween.WaitForCompletion(); CompleteCover(version); } private int BeginTransition() { _transitionVersion++; CancelActiveTransition(); return _transitionVersion; } private void CancelActiveTransition() { DOTween.Kill(this, false); overlayImage?.DOKill(false); if (overlayImage != null) { overlayImage.rectTransform.DOKill(false); } shutterTop?.rectTransform.DOKill(false); shutterBottom?.rectTransform.DOKill(false); } private void PrepareVisibleOverlay(Color color, float alpha) { transitionVisualRoot.SetActive(true); _coverColor = Opaque(color); _currentVisual = TransitionVisual.Overlay; shutterRoot.gameObject.SetActive(false); overlayImage.gameObject.SetActive(true); OverlayRect.anchoredPosition = Vector2.zero; overlayImage.color = WithAlpha(_coverColor, alpha); } private void PrepareOverlayForFadeOut() { if (!transitionVisualRoot.activeSelf) { PrepareVisibleOverlay(Color.black, 1f); return; } if (_currentVisual == TransitionVisual.Shutter) { PrepareVisibleOverlay(_coverColor, 1f); return; } var alpha = overlayImage.gameObject.activeSelf ? overlayImage.color.a : 1f; PrepareVisibleOverlay(_coverColor, alpha); } private void PrepareOverlayCovered() { if (!transitionVisualRoot.activeSelf) { _coverColor = Color.black; } PrepareVisibleOverlay(_coverColor, 1f); } private void PrepareShutter(Color color, float scaleY) { transitionVisualRoot.SetActive(true); _coverColor = Opaque(color); _currentVisual = TransitionVisual.Shutter; shutterRoot.gameObject.SetActive(true); shutterTop.gameObject.SetActive(true); shutterBottom.gameObject.SetActive(true); shutterTop.color = _coverColor; shutterBottom.color = _coverColor; SetShutterScale(scaleY); // 完全覆盖帧先准备目标节点,再关闭来源节点,避免跨模式切换时漏出背景。 overlayImage.gameObject.SetActive(false); } private void CompleteCover(int version) { if (version != _transitionVersion) { return; } _isCovered = true; } private float GetTransitionWidth() { Canvas.ForceUpdateCanvases(); var rectTransform = transitionVisualRoot.transform as RectTransform; return Mathf.Max(1f, rectTransform != null ? rectTransform.rect.width : Screen.width); } private void SetShutterScale(float scaleY) { var topScale = shutterTop.rectTransform.localScale; topScale.y = scaleY; shutterTop.rectTransform.localScale = topScale; var bottomScale = shutterBottom.rectTransform.localScale; bottomScale.y = scaleY; shutterBottom.rectTransform.localScale = bottomScale; } private void ResetVisualsImmediate() { CancelActiveTransition(); if (overlayImage != null) { overlayImage.gameObject.SetActive(true); OverlayRect.anchoredPosition = Vector2.zero; overlayImage.color = Color.clear; } if (shutterRoot != null) { SetShutterScale(0f); shutterRoot.gameObject.SetActive(false); } _coverColor = Color.black; _currentVisual = TransitionVisual.None; _isCovered = false; transitionVisualRoot?.SetActive(false); } private static Color Opaque(Color color) { color.a = 1f; return color; } private static Color WithAlpha(Color color, float alpha) { color.a = alpha; return color; } private static void SetImageAlpha(Graphic graphic, float alpha) { var color = graphic.color; color.a = alpha; graphic.color = color; } } }