NLDClient-yudde/ProjectNLD/Assets/Code/Scripts/UI/Tween/UIMotionTween.cs

280 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// UIMotionTween.cs
// 控制 UI 的位移动画和透明度渐变动画。
// - 位移:在两个锚点之间按 AnimationCurve 插值(支持本地坐标偏移)
// - 渐变:另一条 AnimationCurve 驱动当前 GameObject 下所有 Graphic 的 alpha
// 两条动画可独立启用,共用同一个时长和播放控制。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace NLD.UI
{
public class UIMotionTween : MonoBehaviour
{
// ── 通用设置 ──────────────────────────────────────────────────
[Header("通用")]
[Tooltip("动画总时长(秒)")]
public float duration = 0.5f;
[Tooltip("启动时自动播放")]
public bool playOnEnable = true;
[Tooltip("播放结束后是否循环")]
public bool loop = false;
// ── 位移设置 ──────────────────────────────────────────────────
[Header("位移")]
[Tooltip("是否启用位移动画")]
public bool enableMove = true;
[Tooltip("起点(本地坐标偏移,相对于 RectTransform 的初始 anchoredPosition")]
public Vector2 fromOffset = new Vector2(0f, -50f);
[Tooltip("终点(本地坐标偏移,相对于 RectTransform 的初始 anchoredPosition")]
public Vector2 toOffset = Vector2.zero;
[Tooltip("位移曲线X 轴为归一化时间(0-1)Y 轴为插值进度(0-1)。\n" +
"用曲线形态控制运动快慢/弹性等动态效果。")]
public AnimationCurve moveCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
// ── 渐变设置 ──────────────────────────────────────────────────
[Header("渐变")]
[Tooltip("是否启用透明度渐变")]
public bool enableFade = true;
[Tooltip("渐变曲线X 轴为归一化时间(0-1)Y 轴为 alpha 值(0-1)。\n" +
"影响此 GameObject 下所有 Graphic 组件的 color.a。")]
public AnimationCurve fadeCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
[Tooltip("不参与透明度渐变的根节点:自身及子物体上的 Graphic 均不随动画改 alpha\n" +
"也不参与「初始透明」与 OnDisable 时的清零。")]
[SerializeField]
private List<GameObject> excludeAlphaRoots = new List<GameObject>();
// ── 内部状态 ──────────────────────────────────────────────────
public bool IsPlaying => _isPlaying;
private RectTransform _rectTransform;
private Vector2 _basePosition;
private float _elapsed;
private bool _isPlaying;
// 缓存子级所有 Graphic避免每帧 GetComponentsInChildren
private readonly List<Graphic> _graphics = new List<Graphic>();
// 对应每个 Graphic 在 CacheGraphics 时记录的原始 alpha渐变目标值
private readonly List<float> _originalAlphas = new List<float>();
private readonly List<Graphic> _graphicsScanBuffer = new List<Graphic>();
// ── 生命周期 ──────────────────────────────────────────────────
private void Awake()
{
_rectTransform = GetComponent<RectTransform>();
if (_rectTransform == null)
{
Debug.LogWarning("[UIMotionTween] 未找到 RectTransform位移功能将不生效。", this);
}
else
{
_basePosition = _rectTransform.anchoredPosition;
}
}
private void Start()
{
CacheGraphics();
SetAllTransparent();
}
private void OnEnable()
{
if (playOnEnable)
Play();
}
private void OnDisable()
{
_isPlaying = false;
SetAllTransparent();
}
private void Update()
{
if (!_isPlaying) return;
_elapsed += Time.deltaTime;
float t = duration > 0f ? Mathf.Clamp01(_elapsed / duration) : 1f;
ApplyMove(t);
ApplyFade(t);
if (t >= 1f)
{
if (loop)
_elapsed = 0f;
else
_isPlaying = false;
}
}
// ── 公开接口 ──────────────────────────────────────────────────
/// <summary>
/// 更新初始位置 安全区节点在awake之后更新
/// </summary>
public void UpdateBasePosition()
{
if (_rectTransform != null)
{
_basePosition = _rectTransform.anchoredPosition;
}
}
/// <summary>从头开始播放动画。</summary>
public void Play()
{
_elapsed = 0f;
_isPlaying = true;
enabled = true;
}
/// <summary>从头开始反向播放(终点→起点)。</summary>
public void PlayReverse()
{
_elapsed = duration;
_isPlaying = true;
enabled = true;
}
/// <summary>暂停动画,保持当前状态。</summary>
public void Pause() => _isPlaying = false;
/// <summary>继续播放(从暂停处恢复)。</summary>
public void Resume() => _isPlaying = true;
/// <summary>停止并重置到起始状态t=0。</summary>
public void Stop()
{
_isPlaying = false;
_elapsed = 0f;
ApplyMove(0f);
ApplyFade(0f);
}
/// <summary>手动跳转到指定归一化时间0-1。</summary>
public void SetProgress(float normalizedTime)
{
_elapsed = Mathf.Clamp01(normalizedTime) * duration;
float t = Mathf.Clamp01(normalizedTime);
ApplyMove(t);
ApplyFade(t);
}
/// <summary>重新扫描子级 Graphic 列表(动态增删子节点后调用)。</summary>
public void CacheGraphics()
{
_graphics.Clear();
_originalAlphas.Clear();
_graphicsScanBuffer.Clear();
GetComponentsInChildren(true, _graphicsScanBuffer);
foreach (var g in _graphicsScanBuffer)
{
if (g == null) continue;
if (IsGraphicUnderAlphaExclude(g)) continue;
_graphics.Add(g);
_originalAlphas.Add(g.color.a);
}
}
/// <summary>
/// 是否位于「排除 alpha」根节点之下含根节点自身上的 Graphic
/// </summary>
private bool IsGraphicUnderAlphaExclude(Graphic graphic)
{
if (excludeAlphaRoots == null || excludeAlphaRoots.Count == 0) return false;
var t = graphic.transform;
foreach (var root in excludeAlphaRoots)
{
if (root == null) continue;
var r = root.transform;
if (t == r || t.IsChildOf(r)) return true;
}
return false;
}
// ── 内部实现 ──────────────────────────────────────────────────
private void ApplyMove(float t)
{
if (!enableMove || _rectTransform == null) return;
float curveValue = moveCurve.Evaluate(t);
_rectTransform.anchoredPosition = _basePosition + Vector2.LerpUnclamped(fromOffset, toOffset, curveValue);
}
private void ApplyFade(float t)
{
if (!enableFade) return;
float progress = fadeCurve.Evaluate(t);
for (int i = 0; i < _graphics.Count; i++)
{
var graphic = _graphics[i];
if (graphic == null) continue;
Color c = graphic.color;
c.a = progress * _originalAlphas[i];
graphic.color = c;
}
}
/// <summary>将所有 Graphic 设置为完全透明。</summary>
private void SetAllTransparent()
{
for (int i = 0; i < _graphics.Count; i++)
{
var graphic = _graphics[i];
if (graphic == null) continue;
Color c = graphic.color;
c.a = 0f;
graphic.color = c;
}
}
// 判断该 Graphic 是否应跳过渐变:
// Image - 未设置 Sprite或使用了 Unity 内置默认 SpriteGUID 全零)
// RawImage - 未设置 Texture
private static bool ShouldSkipFade(Graphic graphic)
{
if (graphic is Image img)
{
if (img.sprite == null) return true;
// Unity 内置 Sprite 的名称(来自 Resources/unity_builtin_extra
string n = img.sprite.name;
return n == "UISprite" ||
n == "Background" ||
n == "InputFieldBackground" ||
n == "Knob" ||
n == "Checkmark" ||
n == "DropdownArrow" ||
n == "UIMask";
}
if (graphic is RawImage raw)
{
return raw.texture == null;
}
return false;
}
#if UNITY_EDITOR
private void OnValidate()
{
// Inspector 修改参数时实时预览Edit Mode
if (!Application.isPlaying) return;
if (_rectTransform == null) return;
float t = duration > 0f ? Mathf.Clamp01(_elapsed / duration) : 1f;
ApplyMove(t);
ApplyFade(t);
}
#endif
}
}