using DG.Tweening; using UnityEditor; using UnityEditorInternal; using UnityEngine; /// /// UIAnimator 可视化编辑Inspector: /// - ReorderableList 编排动画步骤(增删/拖拽排序) /// - 各步骤字段按类型动态显示 /// - 播放/反向/停止/重置 预览(编辑器内即时查看效果,不进入播放模式) /// [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() == 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(); } /// /// 按绝对时间(秒)插值应用步骤(编辑器预览用,与运行时DOTween缓动保持一致的核心曲线) /// 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; } } }