344 lines
14 KiB
C#
344 lines
14 KiB
C#
|
|
using DG.Tweening;
|
|||
|
|
using UnityEditor;
|
|||
|
|
using UnityEditorInternal;
|
|||
|
|
using UnityEngine;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// UIAnimator 可视化编辑Inspector:
|
|||
|
|
/// - ReorderableList 编排动画步骤(增删/拖拽排序)
|
|||
|
|
/// - 各步骤字段按类型动态显示
|
|||
|
|
/// - 播放/反向/停止/重置 预览(编辑器内即时查看效果,不进入播放模式)
|
|||
|
|
/// </summary>
|
|||
|
|
[CustomEditor(typeof(UIAnimator))]
|
|||
|
|
public class UIAnimatorInspector : Editor
|
|||
|
|
{
|
|||
|
|
private ReorderableList stepList;
|
|||
|
|
private SerializedProperty stepsProp;
|
|||
|
|
|
|||
|
|
// 预览状态
|
|||
|
|
private bool previewing;
|
|||
|
|
private bool previewForward = true;
|
|||
|
|
private float previewElapsed;
|
|||
|
|
private float previewTotal;
|
|||
|
|
private double lastPreviewTime;
|
|||
|
|
|
|||
|
|
private const float RowHeight = 18f;
|
|||
|
|
private const float RowGap = 2f;
|
|||
|
|
|
|||
|
|
private void OnEnable()
|
|||
|
|
{
|
|||
|
|
stepsProp = serializedObject.FindProperty("steps");
|
|||
|
|
stepList = new ReorderableList(serializedObject, stepsProp, true, true, true, true)
|
|||
|
|
{
|
|||
|
|
drawHeaderCallback = rect => EditorGUI.LabelField(rect, "动画步骤"),
|
|||
|
|
drawElementCallback = DrawStepElement,
|
|||
|
|
elementHeightCallback = index => RowHeight * 3 + RowGap * 4,
|
|||
|
|
onAddCallback = OnAddStep
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnDisable()
|
|||
|
|
{
|
|||
|
|
StopPreview();
|
|||
|
|
EditorApplication.update -= OnPreviewUpdate;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public override void OnInspectorGUI()
|
|||
|
|
{
|
|||
|
|
serializedObject.Update();
|
|||
|
|
|
|||
|
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("playOnAwake"));
|
|||
|
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("autoAddCanvasGroup"));
|
|||
|
|
|
|||
|
|
// 总时长提示
|
|||
|
|
EditorGUILayout.HelpBox($"动画总时长: {((UIAnimator)target).TotalDuration:F2}s", MessageType.None);
|
|||
|
|
|
|||
|
|
stepList.DoLayoutList();
|
|||
|
|
|
|||
|
|
EditorGUILayout.Space();
|
|||
|
|
DrawPreviewButtons();
|
|||
|
|
|
|||
|
|
serializedObject.ApplyModifiedProperties();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 步骤绘制 ----------
|
|||
|
|
|
|||
|
|
private void DrawStepElement(Rect rect, int index, bool isActive, bool isFocused)
|
|||
|
|
{
|
|||
|
|
SerializedProperty prop = stepsProp.GetArrayElementAtIndex(index);
|
|||
|
|
SerializedProperty typeProp = prop.FindPropertyRelative("type");
|
|||
|
|
UIAnimator.UIAnimationType type = (UIAnimator.UIAnimationType)typeProp.enumValueIndex;
|
|||
|
|
|
|||
|
|
rect.y += RowGap;
|
|||
|
|
float y = rect.y;
|
|||
|
|
|
|||
|
|
// 第1行:类型
|
|||
|
|
typeProp.enumValueIndex = (int)(UIAnimator.UIAnimationType)EditorGUI.EnumPopup(
|
|||
|
|
new Rect(rect.x, y, rect.width, RowHeight), GUIContent.none, type);
|
|||
|
|
y += RowHeight + RowGap;
|
|||
|
|
|
|||
|
|
// 第2行:延迟 + 时长
|
|||
|
|
float half = (rect.width - 6) * 0.5f;
|
|||
|
|
EditorGUIUtility.labelWidth = 38f;
|
|||
|
|
EditorGUI.PropertyField(new Rect(rect.x, y, half, RowHeight), prop.FindPropertyRelative("delay"));
|
|||
|
|
EditorGUI.PropertyField(new Rect(rect.x + half + 6, y, half, RowHeight), prop.FindPropertyRelative("duration"));
|
|||
|
|
EditorGUIUtility.labelWidth = 0f;
|
|||
|
|
y += RowHeight + RowGap;
|
|||
|
|
|
|||
|
|
// 第3行:缓动 + 类型专属字段
|
|||
|
|
float easeW = half;
|
|||
|
|
EditorGUI.LabelField(new Rect(rect.x, y, easeW, RowHeight), "缓动");
|
|||
|
|
SerializedProperty easeProp = prop.FindPropertyRelative("ease");
|
|||
|
|
easeProp.enumValueIndex = (int)(Ease)EditorGUI.EnumPopup(
|
|||
|
|
new Rect(rect.x + 34f, y, easeW - 34f, RowHeight), GUIContent.none, (Ease)easeProp.enumValueIndex);
|
|||
|
|
|
|||
|
|
Rect valueRect = new Rect(rect.x + half + 6, y, half, RowHeight);
|
|||
|
|
EditorGUIUtility.labelWidth = 42f;
|
|||
|
|
switch (type)
|
|||
|
|
{
|
|||
|
|
case UIAnimator.UIAnimationType.Move:
|
|||
|
|
EditorGUI.PropertyField(valueRect, prop.FindPropertyRelative("moveOffset"));
|
|||
|
|
break;
|
|||
|
|
case UIAnimator.UIAnimationType.Scale:
|
|||
|
|
EditorGUI.PropertyField(valueRect, prop.FindPropertyRelative("targetScale"));
|
|||
|
|
break;
|
|||
|
|
case UIAnimator.UIAnimationType.Rotate:
|
|||
|
|
EditorGUI.PropertyField(valueRect, prop.FindPropertyRelative("targetRotation"));
|
|||
|
|
break;
|
|||
|
|
case UIAnimator.UIAnimationType.Alpha:
|
|||
|
|
EditorGUI.PropertyField(valueRect, prop.FindPropertyRelative("targetAlpha"));
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
EditorGUIUtility.labelWidth = 0f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnAddStep(ReorderableList list)
|
|||
|
|
{
|
|||
|
|
stepsProp.arraySize++;
|
|||
|
|
SerializedProperty newElem = stepsProp.GetArrayElementAtIndex(stepsProp.arraySize - 1);
|
|||
|
|
// 给新步骤赋默认值
|
|||
|
|
newElem.FindPropertyRelative("type").enumValueIndex = 0;
|
|||
|
|
newElem.FindPropertyRelative("delay").floatValue = 0f;
|
|||
|
|
newElem.FindPropertyRelative("duration").floatValue = 0.5f;
|
|||
|
|
newElem.FindPropertyRelative("ease").enumValueIndex = (int)Ease.OutQuad;
|
|||
|
|
newElem.FindPropertyRelative("moveOffset").vector3Value = Vector3.zero;
|
|||
|
|
newElem.FindPropertyRelative("targetScale").vector3Value = Vector3.one;
|
|||
|
|
newElem.FindPropertyRelative("targetRotation").floatValue = 0f;
|
|||
|
|
newElem.FindPropertyRelative("targetAlpha").floatValue = 1f;
|
|||
|
|
serializedObject.ApplyModifiedProperties();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 预览按钮 ----------
|
|||
|
|
|
|||
|
|
private void DrawPreviewButtons()
|
|||
|
|
{
|
|||
|
|
UIAnimator animator = (UIAnimator)target;
|
|||
|
|
if (animator.steps == null || animator.steps.Count == 0)
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.HelpBox("请先添加动画步骤", MessageType.Warning);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EditorGUILayout.BeginHorizontal();
|
|||
|
|
if (GUILayout.Button("▶ 播放", GUILayout.Height(26)))
|
|||
|
|
StartPreview(animator, true);
|
|||
|
|
if (GUILayout.Button("◀ 反向", GUILayout.Height(26)))
|
|||
|
|
StartPreview(animator, false);
|
|||
|
|
if (GUILayout.Button("■ 停止", GUILayout.Height(26)))
|
|||
|
|
StopPreview();
|
|||
|
|
if (GUILayout.Button("⟲ 重置", GUILayout.Height(26)))
|
|||
|
|
ResetToInitial(animator);
|
|||
|
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
if (previewing)
|
|||
|
|
EditorGUILayout.HelpBox("预览中… 停止后恢复初始状态", MessageType.Info);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 预览驱动 ----------
|
|||
|
|
|
|||
|
|
private void StartPreview(UIAnimator animator, bool forward)
|
|||
|
|
{
|
|||
|
|
StopPreview();
|
|||
|
|
if (animator.GetComponent<RectTransform>() == null)
|
|||
|
|
{
|
|||
|
|
EditorUtility.DisplayDialog("UIAnimator", "需要挂载在带有RectTransform的UI对象上", "确定");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
animator.SaveInitialState();
|
|||
|
|
previewForward = forward;
|
|||
|
|
previewing = true;
|
|||
|
|
previewElapsed = 0f;
|
|||
|
|
previewTotal = Mathf.Max(animator.TotalDuration, 0.001f);
|
|||
|
|
lastPreviewTime = EditorApplication.timeSinceStartup;
|
|||
|
|
|
|||
|
|
EditorApplication.update -= OnPreviewUpdate;
|
|||
|
|
EditorApplication.update += OnPreviewUpdate;
|
|||
|
|
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void StopPreview()
|
|||
|
|
{
|
|||
|
|
if (!previewing) return;
|
|||
|
|
previewing = false;
|
|||
|
|
EditorApplication.update -= OnPreviewUpdate;
|
|||
|
|
RestoreInitialState();
|
|||
|
|
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ResetToInitial(UIAnimator animator)
|
|||
|
|
{
|
|||
|
|
StopPreview();
|
|||
|
|
animator.SaveInitialState();
|
|||
|
|
RestoreInitialState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnPreviewUpdate()
|
|||
|
|
{
|
|||
|
|
if (!previewing) return;
|
|||
|
|
UIAnimator animator = (UIAnimator)target;
|
|||
|
|
if (animator == null) { StopPreview(); return; }
|
|||
|
|
|
|||
|
|
double now = EditorApplication.timeSinceStartup;
|
|||
|
|
previewElapsed += (float)(now - lastPreviewTime);
|
|||
|
|
lastPreviewTime = now;
|
|||
|
|
|
|||
|
|
// 每帧重算总时长,预览中修改步骤字段可即时生效
|
|||
|
|
previewTotal = Mathf.Max(animator.TotalDuration, 0.001f);
|
|||
|
|
|
|||
|
|
// 直接使用绝对时间(秒)驱动,避免归一化比例与步骤时间轴错位
|
|||
|
|
float elapsedTime = previewForward
|
|||
|
|
? Mathf.Min(previewElapsed, previewTotal)
|
|||
|
|
: Mathf.Max(previewTotal - previewElapsed, 0f);
|
|||
|
|
ApplyPreview(animator, elapsedTime);
|
|||
|
|
|
|||
|
|
if (previewElapsed >= previewTotal)
|
|||
|
|
{
|
|||
|
|
previewing = false;
|
|||
|
|
EditorApplication.update -= OnPreviewUpdate;
|
|||
|
|
}
|
|||
|
|
Repaint();
|
|||
|
|
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 按绝对时间(秒)插值应用步骤(编辑器预览用,与运行时DOTween缓动保持一致的核心曲线)
|
|||
|
|
/// </summary>
|
|||
|
|
private void ApplyPreview(UIAnimator animator, float time)
|
|||
|
|
{
|
|||
|
|
RectTransform rt = animator.RectTransform;
|
|||
|
|
CanvasGroup cg = animator.CanvasGroup;
|
|||
|
|
|
|||
|
|
Vector3 curPos = animator.InitialAnchoredPosition3D;
|
|||
|
|
Vector3 curScale = animator.InitialLocalScale;
|
|||
|
|
float curRotZ = animator.InitialRotationZ;
|
|||
|
|
float curAlpha = animator.InitialAlpha;
|
|||
|
|
|
|||
|
|
float t = 0f;
|
|||
|
|
foreach (var step in animator.steps)
|
|||
|
|
{
|
|||
|
|
float stepStart = t + step.delay;
|
|||
|
|
float stepEnd = stepStart + step.duration;
|
|||
|
|
|
|||
|
|
if (time >= stepStart)
|
|||
|
|
{
|
|||
|
|
float local = Mathf.Clamp01((time - stepStart) / Mathf.Max(step.duration, 0.0001f));
|
|||
|
|
float e = EvaluateEase(step.ease, local);
|
|||
|
|
|
|||
|
|
// 记录本步骤起始值(=上一步结束后的值),串行执行时从该值插值到目标值,
|
|||
|
|
// 与运行时 Sequence.Append 的语义一致,避免多步骤"同时执行"
|
|||
|
|
Vector3 startPos = curPos;
|
|||
|
|
Vector3 startScale = curScale;
|
|||
|
|
float startRotZ = curRotZ;
|
|||
|
|
float startAlpha = curAlpha;
|
|||
|
|
|
|||
|
|
switch (step.type)
|
|||
|
|
{
|
|||
|
|
case UIAnimator.UIAnimationType.Move:
|
|||
|
|
curPos = startPos + step.moveOffset * e;
|
|||
|
|
break;
|
|||
|
|
case UIAnimator.UIAnimationType.Scale:
|
|||
|
|
curScale = Vector3.LerpUnclamped(startScale, step.targetScale, e);
|
|||
|
|
break;
|
|||
|
|
case UIAnimator.UIAnimationType.Rotate:
|
|||
|
|
curRotZ = Mathf.LerpUnclamped(startRotZ, step.targetRotation, e);
|
|||
|
|
break;
|
|||
|
|
case UIAnimator.UIAnimationType.Alpha:
|
|||
|
|
if (cg != null)
|
|||
|
|
curAlpha = Mathf.LerpUnclamped(startAlpha, step.targetAlpha, e);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
t = stepEnd;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
rt.anchoredPosition3D = curPos;
|
|||
|
|
rt.localScale = curScale;
|
|||
|
|
Vector3 rot = rt.localEulerAngles;
|
|||
|
|
rot.z = curRotZ;
|
|||
|
|
rt.localEulerAngles = rot;
|
|||
|
|
if (cg != null) cg.alpha = curAlpha;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void RestoreInitialState()
|
|||
|
|
{
|
|||
|
|
UIAnimator animator = (UIAnimator)target;
|
|||
|
|
if (animator == null || animator.RectTransform == null) return;
|
|||
|
|
animator.RectTransform.anchoredPosition3D = animator.InitialAnchoredPosition3D;
|
|||
|
|
animator.RectTransform.localScale = animator.InitialLocalScale;
|
|||
|
|
Vector3 rot = animator.RectTransform.localEulerAngles;
|
|||
|
|
rot.z = animator.InitialRotationZ;
|
|||
|
|
animator.RectTransform.localEulerAngles = rot;
|
|||
|
|
if (animator.CanvasGroup != null)
|
|||
|
|
animator.CanvasGroup.alpha = animator.InitialAlpha;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 预览缓动公式(与DOTween常用缓动一致) ----------
|
|||
|
|
|
|||
|
|
private static float EvaluateEase(Ease ease, float t)
|
|||
|
|
{
|
|||
|
|
switch (ease)
|
|||
|
|
{
|
|||
|
|
case Ease.Linear: return t;
|
|||
|
|
case Ease.InQuad: return t * t;
|
|||
|
|
case Ease.OutQuad: return t * (2f - t);
|
|||
|
|
case Ease.InOutQuad: return t < 0.5f ? 2f * t * t : -1f + (4f - 2f * t) * t;
|
|||
|
|
case Ease.InCubic: return t * t * t;
|
|||
|
|
case Ease.OutCubic: return 1f + (--t) * t * t;
|
|||
|
|
case Ease.InOutCubic: return t < 0.5f ? 4f * t * t * t : (t - 1f) * (2f * t - 2f) * (2f * t - 2f) + 1f;
|
|||
|
|
case Ease.InQuart: return t * t * t * t;
|
|||
|
|
case Ease.OutQuart: return 1f - (--t) * t * t * t;
|
|||
|
|
case Ease.InQuint: return t * t * t * t * t;
|
|||
|
|
case Ease.OutQuint: return 1f + (--t) * t * t * t * t;
|
|||
|
|
case Ease.InSine: return 1f - Mathf.Cos(t * Mathf.PI * 0.5f);
|
|||
|
|
case Ease.OutSine: return Mathf.Sin(t * Mathf.PI * 0.5f);
|
|||
|
|
case Ease.InOutSine: return -0.5f * (Mathf.Cos(Mathf.PI * t) - 1f);
|
|||
|
|
case Ease.InBack: { float c1 = 1.70158f, c3 = c1 + 1f; return c3 * t * t * t - c1 * t * t; }
|
|||
|
|
case Ease.OutBack: { float c1 = 1.70158f, c3 = c1 + 1f; t -= 1f; return 1f + c3 * t * t * t + c1 * t * t; }
|
|||
|
|
case Ease.InOutBack:
|
|||
|
|
{
|
|||
|
|
float c1 = 1.70158f, c2 = c1 * 1.525f;
|
|||
|
|
return t < 0.5f
|
|||
|
|
? (2f * t) * (2f * t) * ((c2 + 1f) * 2f * t - c2) / 2f
|
|||
|
|
: ((2f * t - 2f) * (2f * t - 2f) * ((c2 + 1f) * (t * 2f - 2f) + c2) + 2f) / 2f;
|
|||
|
|
}
|
|||
|
|
case Ease.OutElastic:
|
|||
|
|
{
|
|||
|
|
float c4 = (2f * Mathf.PI) / 3f;
|
|||
|
|
if (t == 0f) return 0f;
|
|||
|
|
if (t == 1f) return 1f;
|
|||
|
|
return Mathf.Pow(2f, -10f * t) * Mathf.Sin((t * 10f - 0.75f) * c4) + 1f;
|
|||
|
|
}
|
|||
|
|
case Ease.OutBounce:
|
|||
|
|
{
|
|||
|
|
const float n1 = 7.5625f, d1 = 2.75f;
|
|||
|
|
if (t < 1f / d1) return n1 * t * t;
|
|||
|
|
if (t < 2f / d1) { t -= 1.5f / d1; return n1 * t * t + 0.75f; }
|
|||
|
|
if (t < 2.5f / d1) { t -= 2.25f / d1; return n1 * t * t + 0.9375f; }
|
|||
|
|
t -= 2.625f / d1; return n1 * t * t + 0.984375f;
|
|||
|
|
}
|
|||
|
|
default: return t;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|