2026-03-10 15:04:26 +08:00
|
|
|
|
// 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);
|
|
|
|
|
|
|
2026-04-16 19:26:22 +08:00
|
|
|
|
[Tooltip("不参与透明度渐变的根节点:自身及子物体上的 Graphic 均不随动画改 alpha,\n" +
|
|
|
|
|
|
"也不参与「初始透明」与 OnDisable 时的清零。")]
|
|
|
|
|
|
[SerializeField]
|
|
|
|
|
|
private List<GameObject> excludeAlphaRoots = new List<GameObject>();
|
|
|
|
|
|
|
2026-03-10 15:04:26 +08:00
|
|
|
|
// ── 内部状态 ──────────────────────────────────────────────────
|
2026-03-18 13:42:03 +08:00
|
|
|
|
public bool IsPlaying => _isPlaying;
|
2026-03-10 15:04:26 +08:00
|
|
|
|
private RectTransform _rectTransform;
|
|
|
|
|
|
private Vector2 _basePosition;
|
|
|
|
|
|
private float _elapsed;
|
|
|
|
|
|
private bool _isPlaying;
|
|
|
|
|
|
|
|
|
|
|
|
// 缓存子级所有 Graphic,避免每帧 GetComponentsInChildren
|
2026-03-11 19:22:01 +08:00
|
|
|
|
private readonly List<Graphic> _graphics = new List<Graphic>();
|
|
|
|
|
|
// 对应每个 Graphic 在 CacheGraphics 时记录的原始 alpha(渐变目标值)
|
|
|
|
|
|
private readonly List<float> _originalAlphas = new List<float>();
|
2026-04-16 19:26:22 +08:00
|
|
|
|
private readonly List<Graphic> _graphicsScanBuffer = new List<Graphic>();
|
2026-03-10 15:04:26 +08:00
|
|
|
|
|
|
|
|
|
|
// ── 生命周期 ──────────────────────────────────────────────────
|
|
|
|
|
|
private void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
_rectTransform = GetComponent<RectTransform>();
|
|
|
|
|
|
if (_rectTransform == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("[UIMotionTween] 未找到 RectTransform,位移功能将不生效。", this);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_basePosition = _rectTransform.anchoredPosition;
|
|
|
|
|
|
}
|
2026-03-18 19:44:20 +08:00
|
|
|
|
}
|
2026-03-10 15:04:26 +08:00
|
|
|
|
|
2026-03-18 19:44:20 +08:00
|
|
|
|
private void Start()
|
|
|
|
|
|
{
|
2026-03-10 15:04:26 +08:00
|
|
|
|
CacheGraphics();
|
2026-03-18 19:44:20 +08:00
|
|
|
|
SetAllTransparent();
|
2026-03-10 15:04:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (playOnEnable)
|
|
|
|
|
|
Play();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnDisable()
|
|
|
|
|
|
{
|
|
|
|
|
|
_isPlaying = false;
|
2026-03-18 19:44:20 +08:00
|
|
|
|
SetAllTransparent();
|
2026-03-10 15:04:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 公开接口 ──────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-05-26 20:02:52 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 更新初始位置 安全区节点在awake之后更新
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void UpdateBasePosition()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_rectTransform != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_basePosition = _rectTransform.anchoredPosition;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 15:04:26 +08:00
|
|
|
|
/// <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();
|
2026-03-11 19:22:01 +08:00
|
|
|
|
_originalAlphas.Clear();
|
2026-04-16 19:26:22 +08:00
|
|
|
|
_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;
|
2026-03-10 15:04:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 内部实现 ──────────────────────────────────────────────────
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-03-11 19:22:01 +08:00
|
|
|
|
float progress = fadeCurve.Evaluate(t);
|
|
|
|
|
|
for (int i = 0; i < _graphics.Count; i++)
|
2026-03-10 15:04:26 +08:00
|
|
|
|
{
|
2026-03-11 19:22:01 +08:00
|
|
|
|
var graphic = _graphics[i];
|
2026-03-10 15:04:26 +08:00
|
|
|
|
if (graphic == null) continue;
|
|
|
|
|
|
Color c = graphic.color;
|
2026-03-11 19:22:01 +08:00
|
|
|
|
c.a = progress * _originalAlphas[i];
|
2026-03-10 15:04:26 +08:00
|
|
|
|
graphic.color = c;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 19:44:20 +08:00
|
|
|
|
/// <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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 16:47:13 +08:00
|
|
|
|
// 判断该 Graphic 是否应跳过渐变:
|
|
|
|
|
|
// Image - 未设置 Sprite,或使用了 Unity 内置默认 Sprite(GUID 全零)
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 15:04:26 +08:00
|
|
|
|
#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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|