551 lines
18 KiB
C#
551 lines
18 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Cysharp.Threading.Tasks;
|
||
using DG.Tweening;
|
||
using PhxhSDK;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
/// <summary>
|
||
/// 项目侧自定义UI显隐动画播放器
|
||
/// </summary>
|
||
public class UIAnimationPlayer : IUIAnimationPlayer
|
||
{
|
||
/// <summary>
|
||
/// 显示动画开播前的预热帧数:给 Canvas/TMP/Layout 一点时间完成 rebuild,避免首开吞动画
|
||
/// </summary>
|
||
private const int ShowWarmupFrames = 2;
|
||
|
||
/// <summary>
|
||
/// 缓存背景alpha
|
||
/// </summary>
|
||
private readonly Dictionary<Image, float> _bgImageOriginAlpha = new();
|
||
|
||
/// <summary>
|
||
/// 提前初始化动画目标(在 UI Init 时调用,避免首次显示卡顿)
|
||
/// </summary>
|
||
public void PreInitialize(UIWindow window)
|
||
{
|
||
if (window == null)
|
||
return;
|
||
|
||
var profile = GetAnimProfile(window.AnimPresetType);
|
||
if (profile == null)
|
||
return;
|
||
|
||
// 提前查找和缓存所有节点引用,避免首次显示时卡顿
|
||
EnsureAnimTargets(window);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 预设动画初始状态(在 SetActive 前调用,确保第一帧就是正确状态)
|
||
/// </summary>
|
||
public void PresetInitialState(UIWindow window, bool isShow)
|
||
{
|
||
if (window == null || !isShow)
|
||
return;
|
||
|
||
var profile = GetAnimProfile(window.AnimPresetType);
|
||
if (profile == null)
|
||
return;
|
||
|
||
EnsureAnimTargets(window);
|
||
// 避免截屏未就绪时白屏闪烁:预设阶段就强制 mask 透明(不隐藏节点)
|
||
EnsureMaskTextureReady(isShow, profile);
|
||
|
||
// 若缺少主体和背景,无需预设
|
||
if ((_bgImages == null || _bgImages.Count == 0) && _contentCg == null)
|
||
return;
|
||
|
||
// 设置动画初始状态(alpha=0, scale等)
|
||
ResetAnimInitialState(isShow, profile);
|
||
|
||
// 标记已预设,避免 PlayAnim 中重复设置
|
||
_isInitialStatePreset = true;
|
||
}
|
||
|
||
public async UniTask Play(UIWindow window, bool isShow, Action callback)
|
||
{
|
||
if (window == null)
|
||
{
|
||
callback?.Invoke();
|
||
return;
|
||
}
|
||
|
||
await PlayAnim(window, isShow, callback);
|
||
}
|
||
|
||
private async UniTask PlayAnim(UIWindow window, bool isShow, Action callBack = null)
|
||
{
|
||
// 隐藏动画不播放,直接执行回调
|
||
if (!isShow)
|
||
{
|
||
callBack?.Invoke();
|
||
return;
|
||
}
|
||
|
||
var profile = GetAnimProfile(window.AnimPresetType);
|
||
|
||
var message = isShow ? "显示" : "隐藏";
|
||
if (profile == null)
|
||
{
|
||
DebugUtil.LogG($"{window.name} - {window.AnimPresetType} - 使用默认方案 播放{message}动画");
|
||
// 使用默认播放器
|
||
await PlayAnimationDefault(window, isShow, callBack);
|
||
return;
|
||
}
|
||
|
||
DebugUtil.LogG($"{window.name} - {window.AnimPresetType} - 使用新方案 播放{message}动画");
|
||
EnsureAnimTargets(window);
|
||
|
||
// 若缺少主体和背景,降级为旧动画
|
||
if ((_bgImages == null || _bgImages.Count == 0) && _contentCg == null)
|
||
{
|
||
// 使用默认播放器
|
||
await PlayAnimationDefault(window, isShow, callBack);
|
||
return;
|
||
}
|
||
|
||
window.OpenAnimSeq?.Kill();
|
||
window.IsCloseCbCalled = false;
|
||
window.IsPlayingOpenAnim = true;
|
||
|
||
// ✅ 显示时等待若干帧,让 UI 布局稳定后再开播,避免首开卡顿导致动画跳帧
|
||
if (isShow && ShowWarmupFrames > 0)
|
||
{
|
||
await UniTask.DelayFrame(ShowWarmupFrames);
|
||
}
|
||
|
||
// 设置初始状态
|
||
// 显示动画:如果 PresetInitialState 已调用,则跳过(避免重复设置)
|
||
// 隐藏动画:必须在这里设置初始状态
|
||
if (!isShow || !_isInitialStatePreset)
|
||
{
|
||
ResetAnimInitialState(isShow, profile);
|
||
}
|
||
|
||
// 重置标记(用于下次动画)
|
||
_isInitialStatePreset = false;
|
||
|
||
window.OpenAnimSeq = DOTween.Sequence();
|
||
|
||
// 遮罩:useMask=false 时直接设为最终值,不播放动画
|
||
var canPlayMask = EnsureMaskTextureReady(isShow, profile);
|
||
if (canPlayMask && _maskCg != null && profile.maskFade.Duration > 0f)
|
||
{
|
||
var tween = _maskCg.DOFade(isShow ? 1f : 0f, profile.maskFade.Duration)
|
||
.SetDelay(profile.maskFade.start)
|
||
.SetEase(profile.alphaCurve);
|
||
window.OpenAnimSeq.Join(tween);
|
||
}
|
||
else if (_maskCg != null)
|
||
{
|
||
_maskCg.alpha = isShow ? 1f : 0f;
|
||
}
|
||
|
||
// 背景透明度
|
||
if (profile.bgFade.Duration > 0f && _bgImages.Count > 0)
|
||
{
|
||
foreach (var img in _bgImages)
|
||
{
|
||
if (img == null) continue;
|
||
|
||
float targetAlpha = 1f;
|
||
if (_bgImageOriginAlpha.TryGetValue(img, out var originAlpha))
|
||
targetAlpha = originAlpha;
|
||
|
||
var tween = img.DOFade(isShow ? targetAlpha : 0f, profile.bgFade.Duration)
|
||
.SetDelay(profile.bgFade.start)
|
||
.SetEase(profile.alphaCurve);
|
||
|
||
window.OpenAnimSeq.Join(tween);
|
||
}
|
||
}
|
||
|
||
// 背景缩放(作用在 BgRoot),隐藏时走反向(显示为 1.1->1,隐藏为 1->1.1)
|
||
if (_bgRoot != null && profile.bgScale.Duration > 0f)
|
||
{
|
||
var toScale = isShow ? profile.bgScaleTo : profile.bgScaleFrom;
|
||
var tween = _bgRoot.DOScale(toScale, profile.bgScale.Duration)
|
||
.SetDelay(profile.bgScale.start)
|
||
.SetEase(profile.scaleCurve);
|
||
window.OpenAnimSeq.Join(tween);
|
||
}
|
||
|
||
// 主体透明度
|
||
if (profile.contentFade.Duration > 0f && _contentCg != null)
|
||
{
|
||
var tween = _contentCg.DOFade(isShow ? 1f : 0f, profile.contentFade.Duration)
|
||
.SetDelay(profile.contentFade.start)
|
||
.SetEase(profile.alphaCurve);
|
||
window.OpenAnimSeq.Join(tween);
|
||
}
|
||
|
||
window.OpenAnimSeq.OnComplete(() =>
|
||
{
|
||
if (window.IsCloseCbCalled) return;
|
||
window.IsPlayingOpenAnim = false;
|
||
window.OpenAnimSeq = null;
|
||
window.IsCloseCbCalled = true;
|
||
callBack?.Invoke();
|
||
});
|
||
|
||
await UniTask.WaitUntil(() =>
|
||
{
|
||
if (window.OpenAnimSeq == null)
|
||
return true;
|
||
if (!window.OpenAnimSeq.IsActive())
|
||
return true;
|
||
return window.OpenAnimSeq.IsComplete();
|
||
});
|
||
|
||
// 补偿兜底
|
||
if (!window.IsCloseCbCalled)
|
||
{
|
||
window.IsPlayingOpenAnim = false;
|
||
window.OpenAnimSeq?.Kill();
|
||
window.OpenAnimSeq = null;
|
||
window.IsCloseCbCalled = true;
|
||
callBack?.Invoke();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取界面对应的配置,若资源未就绪则使用内置预设兜底,避免首次打开无动画
|
||
/// </summary>
|
||
/// <param name="preset"></param>
|
||
/// <returns></returns>
|
||
private UIAnimProfile GetAnimProfile(UIAnimPresetType preset)
|
||
{
|
||
if (preset == UIAnimPresetType.None)
|
||
return null;
|
||
|
||
var assetProfile = UIManager.Instance.AnimProfileAsset?.GetProfile(preset);
|
||
if (assetProfile != null)
|
||
return assetProfile;
|
||
|
||
return s_defaultProfiles.GetValueOrDefault(preset);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 高斯模糊节点命名
|
||
/// </summary>
|
||
private const string BlurMaskName = "GaussianBlurMask";
|
||
|
||
/// <summary>
|
||
/// 背景节点命名
|
||
/// </summary>
|
||
private const string BgRootName = "BgRoot";
|
||
|
||
/// <summary>
|
||
/// 界面内容主体节点命名
|
||
/// </summary>
|
||
private const string ContentRootName = "MainNode";
|
||
|
||
/// <summary>
|
||
/// 挂在界面上的动画节点
|
||
/// </summary>
|
||
private UIAnimationNode animationNode;
|
||
|
||
// 记录当前缓存的窗口,避免跨窗口复用动画目标
|
||
private UIWindow _cachedWindow;
|
||
|
||
private RectTransform _bgRoot;
|
||
private RectTransform _contentRoot;
|
||
private CanvasGroup _maskCg;
|
||
private RawImage _maskRawImage;
|
||
private CanvasGroup _contentCg;
|
||
|
||
/// <summary>
|
||
/// 背景图列表
|
||
/// </summary>
|
||
private readonly List<Image> _bgImages = new List<Image>();
|
||
|
||
/// <summary>
|
||
/// 标记初始状态是否已预设(避免重复设置)
|
||
/// </summary>
|
||
private bool _isInitialStatePreset = false;
|
||
|
||
/// <summary>
|
||
/// 内置预设兜底,资源未加载到时使用
|
||
/// </summary>
|
||
private static readonly Dictionary<UIAnimPresetType, UIAnimProfile> s_defaultProfiles =
|
||
new()
|
||
{
|
||
{
|
||
UIAnimPresetType.Level1,
|
||
new UIAnimProfile
|
||
{
|
||
totalDuration = 0.3f,
|
||
maskFade = new UIAnimRange(0f, 0.05f),
|
||
bgFade = new UIAnimRange(0.02f, 0.2f),
|
||
bgScale = new UIAnimRange(0.02f, 0.12f),
|
||
bgScaleFrom = 1.1f,
|
||
bgScaleTo = 1f,
|
||
contentFade = new UIAnimRange(0.15f, 0.3f),
|
||
}
|
||
},
|
||
{
|
||
UIAnimPresetType.Level2,
|
||
new UIAnimProfile
|
||
{
|
||
totalDuration = 0.3f,
|
||
maskFade = new UIAnimRange(0f, 0.05f),
|
||
bgFade = new UIAnimRange(0.02f, 0.2f),
|
||
bgScale = new UIAnimRange(0.02f, 0.12f),
|
||
bgScaleFrom = 1.1f,
|
||
bgScaleTo = 1f,
|
||
contentFade = new UIAnimRange(0.15f, 0.3f),
|
||
}
|
||
},
|
||
{
|
||
UIAnimPresetType.Level3,
|
||
new UIAnimProfile
|
||
{
|
||
totalDuration = 0.15f,
|
||
maskFade = new UIAnimRange(0f, 0.05f),
|
||
bgFade = new UIAnimRange(0f, 0.15f),
|
||
bgScale = new UIAnimRange(0.02f, 0.1f),
|
||
bgScaleFrom = 1.1f,
|
||
bgScaleTo = 1f,
|
||
contentFade = new UIAnimRange(0.11f, 0.15f),
|
||
}
|
||
},
|
||
};
|
||
|
||
/// <summary>
|
||
/// 初始化动画需要的节点和组件
|
||
/// </summary>
|
||
private void EnsureAnimTargets(UIWindow window)
|
||
{
|
||
if (window == null)
|
||
return;
|
||
|
||
if (_cachedWindow != window)
|
||
{
|
||
_cachedWindow = window;
|
||
animationNode = null;
|
||
_bgRoot = null;
|
||
_contentRoot = null;
|
||
_maskCg = null;
|
||
_maskRawImage = null;
|
||
_contentCg = null;
|
||
_bgImages.Clear();
|
||
_bgImageOriginAlpha.Clear();
|
||
_isInitialStatePreset = false;
|
||
}
|
||
|
||
if (animationNode == null)
|
||
{
|
||
animationNode = window.transform.GetComponent<UIAnimationNode>();
|
||
}
|
||
|
||
if (_maskCg == null)
|
||
{
|
||
var maskTrans = window.transform.Find(BlurMaskName);
|
||
if (maskTrans != null)
|
||
{
|
||
if (_maskRawImage == null)
|
||
{
|
||
_maskRawImage = maskTrans.GetComponent<RawImage>();
|
||
}
|
||
_maskCg = maskTrans.GetComponent<CanvasGroup>();
|
||
if (_maskCg == null)
|
||
{
|
||
_maskCg = maskTrans.gameObject.AddComponent<CanvasGroup>();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (_bgRoot == null)
|
||
{
|
||
if (animationNode != null && animationNode.BGRoot != null)
|
||
{
|
||
_bgRoot = animationNode.BGRoot.transform as RectTransform;
|
||
}
|
||
|
||
if (_bgRoot == null)
|
||
{
|
||
_bgRoot = window.transform.Find(BgRootName) as RectTransform;
|
||
}
|
||
}
|
||
|
||
if (_bgImages.Count == 0 && _bgRoot != null)
|
||
{
|
||
_bgImages.AddRange(_bgRoot.GetComponentsInChildren<Image>(true));
|
||
foreach (var img in _bgImages)
|
||
{
|
||
if (img == null) continue;
|
||
_bgImageOriginAlpha[img] = img.color.a;
|
||
}
|
||
}
|
||
|
||
if (_contentRoot == null)
|
||
{
|
||
if (animationNode != null && animationNode.ContentRoot != null)
|
||
{
|
||
_contentRoot = animationNode.ContentRoot.transform as RectTransform;
|
||
}
|
||
|
||
if (_contentRoot == null)
|
||
{
|
||
_contentRoot = window.transform.Find(ContentRootName) as RectTransform;
|
||
}
|
||
}
|
||
|
||
if (_contentCg == null && _contentRoot != null)
|
||
{
|
||
_contentCg = _contentRoot.GetComponent<CanvasGroup>();
|
||
if (_contentCg == null)
|
||
_contentCg = _contentRoot.gameObject.AddComponent<CanvasGroup>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置初始透明度/缩放
|
||
/// </summary>
|
||
private void ResetAnimInitialState(bool isShow, UIAnimProfile profile)
|
||
{
|
||
// 高斯模糊背景不修改
|
||
/*if (_maskCg != null)
|
||
{
|
||
_maskCg.alpha = isShow ? 0f : 1f;
|
||
}*/
|
||
|
||
if (_bgImages != null)
|
||
{
|
||
foreach (var img in _bgImages)
|
||
{
|
||
if (img == null) continue;
|
||
var c = img.color;
|
||
// c.a = isShow ? 0f : 1f;
|
||
if (_bgImageOriginAlpha.TryGetValue(img, out var originAlpha))
|
||
{
|
||
c.a = isShow ? 0f : originAlpha;
|
||
}
|
||
else
|
||
{
|
||
c.a = isShow ? 0f : img.color.a;
|
||
}
|
||
img.color = c;
|
||
}
|
||
}
|
||
|
||
if (_bgRoot != null)
|
||
{
|
||
_bgRoot.localScale = Vector3.one * (isShow ? profile.bgScaleFrom : profile.bgScaleTo);
|
||
}
|
||
|
||
if (_contentCg != null)
|
||
{
|
||
_contentCg.alpha = isShow ? 0f : 1f;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// useMask=true 时确保模糊遮罩纹理就绪,未就绪时跳过动画并强制透明(避免 RawImage 默认白屏闪烁)
|
||
/// </summary>
|
||
/// <returns>true=纹理就绪可播放动画; false=纹理未就绪已强制透明,跳过动画</returns>
|
||
private bool EnsureMaskTextureReady(bool isShow, UIAnimProfile profile)
|
||
{
|
||
if (!profile.useMask)
|
||
return false;
|
||
|
||
if (_maskRawImage == null)
|
||
return false;
|
||
|
||
// 截屏纹理未就绪:保持节点激活但强制透明(alpha=0),跳过动画,避免 RawImage 显示默认白色
|
||
if (_maskRawImage.texture == null)
|
||
{
|
||
_maskRawImage.gameObject.SetActive(true); // 保持激活(不隐藏节点)
|
||
if (_maskCg != null)
|
||
_maskCg.alpha = 0f; // 强制透明,避免白屏
|
||
return false; // 跳过fade动画
|
||
}
|
||
|
||
// 纹理就绪:确保节点可见(透明度由 CanvasGroup 动画控制)
|
||
_maskRawImage.gameObject.SetActive(true);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 播放窗口动画 默认方法
|
||
/// </summary>
|
||
private async UniTask PlayAnimationDefault(UIWindow uiWindow, bool isShow, Action callBack = null)
|
||
{
|
||
var canvasGroup = uiWindow.gameObject.GetComponent<CanvasGroup>();
|
||
if (!canvasGroup)
|
||
{
|
||
canvasGroup = uiWindow.gameObject.AddComponent<CanvasGroup>();
|
||
}
|
||
|
||
|
||
var time = 0.2f;
|
||
uiWindow.IsPlayingOpenAnim = true;
|
||
uiWindow.OpenAnimSeq = DOTween.Sequence();
|
||
|
||
if (isShow)
|
||
{
|
||
// 只有当前透明度较低时才做淡入,避免 active=true 后被强制拉成 0 导致闪烁
|
||
if (canvasGroup.alpha < 0.99f)
|
||
{
|
||
time = 0.3f;
|
||
var tw0 = canvasGroup.DOFade(1, time);
|
||
uiWindow.OpenAnimSeq.Append(tw0);
|
||
uiWindow.OpenAnimSeq.AppendCallback(() =>
|
||
{
|
||
uiWindow.IsPlayingOpenAnim = false;
|
||
uiWindow.OpenAnimSeq.Kill();
|
||
uiWindow.OpenAnimSeq = null;
|
||
callBack?.Invoke();
|
||
});
|
||
}
|
||
else
|
||
{
|
||
uiWindow.IsPlayingOpenAnim = false;
|
||
uiWindow.OpenAnimSeq.Kill();
|
||
uiWindow.OpenAnimSeq = null;
|
||
callBack?.Invoke();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
canvasGroup.alpha = 1;
|
||
var tw1 = canvasGroup.DOFade(0, time);
|
||
uiWindow.OpenAnimSeq.Append(tw1);
|
||
uiWindow.OpenAnimSeq.AppendCallback(() =>
|
||
{
|
||
if (!uiWindow.IsCloseCbCalled)
|
||
{
|
||
uiWindow.IsPlayingOpenAnim = false;
|
||
uiWindow.OpenAnimSeq.Kill();
|
||
uiWindow.OpenAnimSeq = null;
|
||
callBack?.Invoke();
|
||
uiWindow.IsCloseCbCalled = true;
|
||
}
|
||
});
|
||
|
||
await UniTask.WaitUntil(() =>
|
||
{
|
||
if (null == uiWindow.OpenAnimSeq)
|
||
return true;
|
||
|
||
if (!uiWindow.OpenAnimSeq.IsActive())
|
||
return true;
|
||
|
||
return uiWindow.OpenAnimSeq.IsComplete();
|
||
});
|
||
|
||
//保证销毁界面时全都销毁掉
|
||
//await UniTask.WaitForSeconds(time); // UniTask.wa((int)time * 1000 + 10);
|
||
if (!uiWindow.IsCloseCbCalled)
|
||
{
|
||
uiWindow.IsPlayingOpenAnim = false;
|
||
uiWindow.OpenAnimSeq?.Kill();
|
||
uiWindow.OpenAnimSeq = null;
|
||
callBack?.Invoke();
|
||
uiWindow.IsCloseCbCalled = true;
|
||
}
|
||
}
|
||
}
|
||
} |