// 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 excludeAlphaRoots = new List(); // ── 内部状态 ────────────────────────────────────────────────── public bool IsPlaying => _isPlaying; private RectTransform _rectTransform; private Vector2 _basePosition; private float _elapsed; private bool _isPlaying; // 缓存子级所有 Graphic,避免每帧 GetComponentsInChildren private readonly List _graphics = new List(); // 对应每个 Graphic 在 CacheGraphics 时记录的原始 alpha(渐变目标值) private readonly List _originalAlphas = new List(); private readonly List _graphicsScanBuffer = new List(); // ── 生命周期 ────────────────────────────────────────────────── private void Awake() { _rectTransform = GetComponent(); 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; } } // ── 公开接口 ────────────────────────────────────────────────── /// /// 更新初始位置 安全区节点在awake之后更新 /// public void UpdateBasePosition() { if (_rectTransform != null) { _basePosition = _rectTransform.anchoredPosition; } } /// 从头开始播放动画。 public void Play() { _elapsed = 0f; _isPlaying = true; enabled = true; } /// 从头开始反向播放(终点→起点)。 public void PlayReverse() { _elapsed = duration; _isPlaying = true; enabled = true; } /// 暂停动画,保持当前状态。 public void Pause() => _isPlaying = false; /// 继续播放(从暂停处恢复)。 public void Resume() => _isPlaying = true; /// 停止并重置到起始状态(t=0)。 public void Stop() { _isPlaying = false; _elapsed = 0f; ApplyMove(0f); ApplyFade(0f); } /// 手动跳转到指定归一化时间(0-1)。 public void SetProgress(float normalizedTime) { _elapsed = Mathf.Clamp01(normalizedTime) * duration; float t = Mathf.Clamp01(normalizedTime); ApplyMove(t); ApplyFade(t); } /// 重新扫描子级 Graphic 列表(动态增删子节点后调用)。 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); } } /// /// 是否位于「排除 alpha」根节点之下(含根节点自身上的 Graphic)。 /// 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; } } /// 将所有 Graphic 设置为完全透明。 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 内置默认 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; } #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 } }