killapp/Assets/Editor/UIAnimatorInspector.cs
“虞渠成” 3a335e60a2 chore: 重构DOTween插件路径并更新相关代码
1. 将DOTween从原Plugins/DOTween迁移到Plugins/Demigiant/DOTween
2. 更新版本号解析逻辑,适配设备端压缩编码格式
3. 新增UI动画组件UIAnimator及其编辑器脚本
4. 优化主页设备列表项滑动菜单功能
5. 移除无用的VSCode编辑器包配置
6. 更新多语言文案修正末尾标点
7. 调整主页锁定超时配置为可配置项
2026-08-19 09:47:44 +08:00

344 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}