com.alicizax.unity.ui.exten.../Editor/RecyclerView/RecyclerViewEditor.cs
2025-12-26 14:22:46 +08:00

842 lines
28 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 System;
using System.Collections.Generic;
using System.Linq;
using AlicizaX.UI;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace AlicizaX.UI.Editor
{
[CustomEditor(typeof(RecyclerView))]
public class RecyclerViewEditor : UnityEditor.Editor
{
#region Constants
private const string NoneOptionName = "None";
private const string VerticalScrollbarPath = "Packages/com.alicizax.unity.ui.extension/Editor/RecyclerView/Res/vertical.prefab";
private const string HorizontalScrollbarPath = "Packages/com.alicizax.unity.ui.extension/Editor/RecyclerView/Res/horizontal.prefab";
#endregion
#region Serialized Properties - Layout Manager
private SerializedProperty _layoutManagerTypeName;
private SerializedProperty _layoutManager;
private List<string> _layoutTypeNames = new List<string>();
private int _selectedLayoutIndex;
#endregion
#region Serialized Properties - Scroller
private SerializedProperty _scroll;
private SerializedProperty _scroller;
private SerializedProperty _scrollerTypeName;
private List<Type> _scrollerTypes = new List<Type>();
private List<string> _scrollerTypeNames = new List<string>();
private int _selectedScrollerIndex;
#endregion
#region Serialized Properties - Base Settings
private SerializedProperty _direction;
private SerializedProperty _alignment;
private SerializedProperty _content;
private SerializedProperty _spacing;
private SerializedProperty _padding;
private SerializedProperty _snap;
private SerializedProperty _scrollSpeed;
private SerializedProperty _wheelSpeed;
#endregion
#region Serialized Properties - Templates & Scrollbar
private SerializedProperty _templates;
private SerializedProperty _showScrollBar;
private SerializedProperty _scrollbar;
#endregion
#region Unity Lifecycle
private void OnEnable()
{
// 先绑定所有 SerializedProperty
InitializeLayoutManagerProperties();
InitializeScrollerProperties();
InitializeBaseProperties();
InitializeTemplateProperties();
// 确保序列化对象是最新的
serializedObject.Update();
// 如果 layoutManager 的 managedReferenceValue 丢失但有记录的 typeName则尝试恢复实例
RestoreLayoutManagerFromTypeNameIfMissing();
// 如果 scroller 组件丢失但有记录的 typeName则尝试恢复组件到目标 GameObject 上
RestoreScrollerFromTypeNameIfMissing();
// 应用修改(若有)
serializedObject.ApplyModifiedProperties();
}
#endregion
#region Initialization
private void InitializeLayoutManagerProperties()
{
_layoutManagerTypeName = serializedObject.FindProperty("_layoutManagerTypeName");
_layoutManager = serializedObject.FindProperty("layoutManager");
RefreshLayoutTypes();
}
private void InitializeScrollerProperties()
{
_scroll = serializedObject.FindProperty("scroll");
_scroller = serializedObject.FindProperty("scroller");
_scrollerTypeName = serializedObject.FindProperty("_scrollerTypeName");
RefreshScrollerTypes();
SyncExistingScroller();
}
private void InitializeBaseProperties()
{
_direction = serializedObject.FindProperty("direction");
_alignment = serializedObject.FindProperty("alignment");
_content = serializedObject.FindProperty("content");
_spacing = serializedObject.FindProperty("spacing");
_padding = serializedObject.FindProperty("padding");
_snap = serializedObject.FindProperty("snap");
_scrollSpeed = serializedObject.FindProperty("scrollSpeed");
_wheelSpeed = serializedObject.FindProperty("wheelSpeed");
}
private void InitializeTemplateProperties()
{
_templates = serializedObject.FindProperty("templates");
_showScrollBar = serializedObject.FindProperty("showScrollBar");
_scrollbar = serializedObject.FindProperty("scrollbar");
}
#endregion
#region Inspector GUI
public override void OnInspectorGUI()
{
serializedObject.Update();
bool isPlaying = Application.isPlaying;
DrawLayoutManagerSection(isPlaying);
DrawBaseSettingsSection(isPlaying);
DrawScrollerSection(isPlaying);
DrawTemplatesSection();
serializedObject.ApplyModifiedProperties();
}
#endregion
#region Layout Manager Section
private void DrawLayoutManagerSection(bool isPlaying)
{
EditorGUILayout.BeginVertical("box");
{
EditorGUILayout.LabelField("Layout Manager", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(isPlaying))
{
int newIndex = EditorGUILayout.Popup("Layout Type", _selectedLayoutIndex, _layoutTypeNames.ToArray());
if (newIndex != _selectedLayoutIndex)
{
_selectedLayoutIndex = newIndex;
UpdateLayoutManager(newIndex);
serializedObject.ApplyModifiedProperties();
}
}
if (_layoutManager.managedReferenceValue != null)
{
EditorGUILayout.Space(3);
DrawManagedReferenceProperties(_layoutManager);
}
else
{
EditorGUILayout.HelpBox("Please select a Layout Manager", MessageType.Error);
}
}
EditorGUILayout.EndVertical();
}
private void RefreshLayoutTypes()
{
_layoutTypeNames.Clear();
_layoutTypeNames.Add(NoneOptionName);
var types = AlicizaX.Utility.Assembly.GetRuntimeTypes(typeof(ILayoutManager));
foreach (var type in types)
{
if (!typeof(MonoBehaviour).IsAssignableFrom(type))
{
_layoutTypeNames.Add(type.FullName);
}
}
_selectedLayoutIndex = Mathf.Clamp(
_layoutTypeNames.IndexOf(_layoutManagerTypeName.stringValue),
0,
_layoutTypeNames.Count - 1
);
}
private void UpdateLayoutManager(int selectedIndex)
{
try
{
if (selectedIndex == 0)
{
ClearLayoutManager();
return;
}
if (!IsValidLayoutIndex(selectedIndex))
{
Debug.LogError($"Invalid layout index: {selectedIndex}");
ClearLayoutManager();
return;
}
string typeName = _layoutTypeNames[selectedIndex];
Type type = AlicizaX.Utility.Assembly.GetType(typeName);
if (type != null && typeof(ILayoutManager).IsAssignableFrom(type))
{
_layoutManager.managedReferenceValue = Activator.CreateInstance(type);
_layoutManagerTypeName.stringValue = typeName;
_selectedLayoutIndex = selectedIndex;
}
else
{
Debug.LogError($"Invalid layout type: {typeName}");
ClearLayoutManager();
}
}
catch (Exception e)
{
Debug.LogError($"Layout Manager Error: {e.Message}");
ClearLayoutManager();
}
}
private void ClearLayoutManager()
{
_layoutManager.managedReferenceValue = null;
_layoutManagerTypeName.stringValue = "";
_selectedLayoutIndex = 0;
}
private bool IsValidLayoutIndex(int index)
{
return index >= 0 && index < _layoutTypeNames.Count;
}
#endregion
#region Base Settings Section
private void DrawBaseSettingsSection(bool isPlaying)
{
EditorGUILayout.BeginVertical("box");
{
EditorGUILayout.LabelField("Base Settings", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(isPlaying))
{
EditorGUILayout.PropertyField(_direction);
EditorGUILayout.PropertyField(_alignment);
EditorGUILayout.PropertyField(_content);
}
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Spacing", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_spacing);
EditorGUILayout.PropertyField(_padding);
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Scrolling", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(isPlaying))
{
bool previousScrollValue = _scroll.boolValue;
EditorGUILayout.PropertyField(_scroll);
if (_scroll.boolValue != previousScrollValue)
{
HandleScrollToggle();
}
}
if (_scroll.boolValue)
{
DrawScrollBarSettings(isPlaying);
}
EditorGUILayout.PropertyField(_snap);
}
EditorGUILayout.EndVertical();
}
private void DrawScrollBarSettings(bool isPlaying)
{
using (new EditorGUI.DisabledScope(isPlaying))
{
bool previousShowScrollBarValue = _showScrollBar.boolValue;
EditorGUILayout.PropertyField(_showScrollBar);
if (_showScrollBar.boolValue != previousShowScrollBarValue)
{
HandleScrollBarToggle();
}
}
}
#endregion
#region Scroller Section
private void DrawScrollerSection(bool isPlaying)
{
if (!_scroll.boolValue) return;
EditorGUILayout.BeginVertical("box");
{
EditorGUILayout.LabelField("Scroller Settings", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(isPlaying))
{
int newIndex = EditorGUILayout.Popup("Scroller Type", _selectedScrollerIndex, _scrollerTypeNames.ToArray());
if (newIndex != _selectedScrollerIndex)
{
UpdateScroller(newIndex);
serializedObject.ApplyModifiedProperties();
}
}
DrawScrollerComponentProperties();
DrawScrollerSpeedSettings();
}
EditorGUILayout.EndVertical();
}
private void DrawScrollerComponentProperties()
{
var recyclerView = target as RecyclerView;
if (recyclerView == null) return;
var scrollerComponent = recyclerView.GetComponent<IScroller>();
if (scrollerComponent != null)
{
DrawComponentProperties(scrollerComponent as MonoBehaviour, "Scroller Properties");
}
else
{
EditorGUILayout.HelpBox("Please select a Scroller type", MessageType.Error);
}
}
private void DrawScrollerSpeedSettings()
{
EditorGUILayout.Space(3);
EditorGUILayout.PropertyField(_scrollSpeed);
EditorGUILayout.PropertyField(_wheelSpeed);
}
private void RefreshScrollerTypes()
{
_scrollerTypes = TypeCache.GetTypesDerivedFrom<IScroller>()
.Where(t => t.IsSubclassOf(typeof(MonoBehaviour)))
.ToList();
_scrollerTypeNames = _scrollerTypes
.Select(t => t.FullName)
.Prepend(NoneOptionName)
.ToList();
}
private void SyncExistingScroller()
{
var recyclerView = target as RecyclerView;
if (recyclerView == null) return;
var existingScroller = recyclerView.GetComponent<IScroller>();
if (existingScroller != null)
{
_scroller.objectReferenceValue = existingScroller as MonoBehaviour;
_scrollerTypeName.stringValue = existingScroller.GetType().FullName;
_selectedScrollerIndex = _scrollerTypeNames.IndexOf(_scrollerTypeName.stringValue);
}
else
{
// 如果组件不存在,但属性里存了类型名,这里不清理 typeName恢复逻辑会处理
_selectedScrollerIndex = Mathf.Clamp(_scrollerTypeNames.IndexOf(_scrollerTypeName.stringValue), 0, _scrollerTypeNames.Count - 1);
}
}
private void UpdateScroller(int selectedIndex)
{
try
{
var recyclerView = target as RecyclerView;
if (recyclerView == null) return;
Undo.RecordObjects(new Object[] { recyclerView, this }, "Update Scroller");
RemoveExistingScroller(recyclerView);
if (selectedIndex == 0)
{
ClearScrollerReferences();
return;
}
AddNewScroller(recyclerView, selectedIndex);
}
catch (Exception e)
{
Debug.LogError($"Scroller Error: {e}");
ClearScrollerReferences();
}
}
private void RemoveExistingScroller(RecyclerView recyclerView)
{
var oldScroller = recyclerView.GetComponent<IScroller>();
if (oldScroller != null)
{
Undo.DestroyObjectImmediate(oldScroller as MonoBehaviour);
}
}
private void AddNewScroller(RecyclerView recyclerView, int selectedIndex)
{
Type selectedType = _scrollerTypes[selectedIndex - 1];
var newScroller = Undo.AddComponent(recyclerView.gameObject, selectedType) as IScroller;
_scroller.objectReferenceValue = newScroller as MonoBehaviour;
_scrollerTypeName.stringValue = selectedType.FullName;
_selectedScrollerIndex = selectedIndex;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(recyclerView);
}
private void ClearScrollerReferences()
{
_scroller.objectReferenceValue = null;
_scrollerTypeName.stringValue = "";
_selectedScrollerIndex = 0;
}
private void HandleScrollToggle()
{
if (!_scroll.boolValue)
{
RemoveScrollerComponent();
ClearScrollBar();
}
}
private void RemoveScrollerComponent()
{
var recyclerView = target as RecyclerView;
if (recyclerView == null) return;
var scrollerComponent = recyclerView.GetComponent<IScroller>();
if (scrollerComponent != null)
{
Undo.DestroyObjectImmediate(scrollerComponent as MonoBehaviour);
}
ClearScrollerReferences();
}
#endregion
#region Scrollbar Handling
private void HandleScrollBarToggle()
{
if (_showScrollBar.boolValue)
{
CreateScrollBar();
}
else
{
ClearScrollBar();
}
}
private void CreateScrollBar()
{
var recyclerView = target as RecyclerView;
if (recyclerView == null) return;
Direction direction = (Direction)_direction.enumValueIndex;
string prefabPath = GetScrollbarPrefabPath(direction);
if (!string.IsNullOrEmpty(prefabPath))
{
InstantiateScrollBar(prefabPath, recyclerView.transform);
}
}
private string GetScrollbarPrefabPath(Direction direction)
{
return direction switch
{
Direction.Vertical => VerticalScrollbarPath,
Direction.Horizontal => HorizontalScrollbarPath,
_ => null
};
}
private void InstantiateScrollBar(string path, Transform parent)
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab == null)
{
Debug.LogError($"Scrollbar prefab not found at path: {path}");
return;
}
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
PrefabUtility.UnpackPrefabInstance(instance, PrefabUnpackMode.Completely, InteractionMode.UserAction);
_scrollbar.objectReferenceValue = instance.GetComponent<Scrollbar>();
serializedObject.ApplyModifiedProperties();
}
private void ClearScrollBar()
{
_showScrollBar.boolValue = false;
if (_scrollbar.objectReferenceValue != null)
{
Scrollbar scrollbarComponent = _scrollbar.objectReferenceValue as Scrollbar;
_scrollbar.objectReferenceValue = null;
if (scrollbarComponent != null)
{
Object.DestroyImmediate(scrollbarComponent.gameObject);
}
}
serializedObject.ApplyModifiedProperties();
}
#endregion
#region Templates Section
private void DrawTemplatesSection()
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Item Templates", EditorStyles.boldLabel);
DrawTemplatesList();
DrawDragAndDropArea();
}
private void DrawTemplatesList()
{
if (_templates == null || !_templates.isArray) return;
for (int i = 0; i < _templates.arraySize; i++)
{
DrawTemplateItem(i);
}
}
private void DrawTemplateItem(int index)
{
if (index < 0 || index >= _templates.arraySize) return;
SerializedProperty item = _templates.GetArrayElementAtIndex(index);
EditorGUILayout.BeginHorizontal();
{
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.PropertyField(item, GUIContent.none, true);
}
if (GUILayout.Button("×", GUILayout.Width(20), GUILayout.Height(EditorGUIUtility.singleLineHeight)))
{
RemoveTemplateItem(index);
}
}
EditorGUILayout.EndHorizontal();
}
private void RemoveTemplateItem(int index)
{
if (_templates == null || index < 0 || index >= _templates.arraySize) return;
_templates.DeleteArrayElementAtIndex(index);
serializedObject.ApplyModifiedProperties();
GUIUtility.ExitGUI();
}
private void DrawDragAndDropArea()
{
Rect dropArea = GUILayoutUtility.GetRect(0f, 50f, GUILayout.ExpandWidth(true));
GUI.Box(dropArea, "Drag ViewHolder Templates Here", EditorStyles.helpBox);
HandleDragAndDrop(dropArea);
}
private void HandleDragAndDrop(Rect dropArea)
{
Event currentEvent = Event.current;
switch (currentEvent.type)
{
case EventType.DragUpdated:
case EventType.DragPerform:
if (!dropArea.Contains(currentEvent.mousePosition))
return;
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (currentEvent.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
ProcessDraggedTemplates();
currentEvent.Use();
}
break;
}
}
private void ProcessDraggedTemplates()
{
foreach (Object draggedObject in DragAndDrop.objectReferences)
{
if (draggedObject is GameObject gameObject)
{
ProcessDraggedGameObject(gameObject);
}
}
}
private void ProcessDraggedGameObject(GameObject gameObject)
{
ViewHolder viewHolder = gameObject.GetComponent<ViewHolder>();
if (viewHolder != null)
{
AddTemplate(gameObject);
}
else
{
Debug.LogWarning($"GameObject '{gameObject.name}' must have a ViewHolder component!");
}
}
private void AddTemplate(GameObject templatePrefab)
{
if (_templates == null || templatePrefab == null) return;
if (IsTemplateDuplicate(templatePrefab))
{
Debug.LogWarning($"Template '{templatePrefab.name}' already exists in the list!");
return;
}
_templates.arraySize++;
SerializedProperty newItem = _templates.GetArrayElementAtIndex(_templates.arraySize - 1);
newItem.objectReferenceValue = templatePrefab;
serializedObject.ApplyModifiedProperties();
}
private bool IsTemplateDuplicate(GameObject templatePrefab)
{
Type templateType = templatePrefab.GetComponent<ViewHolder>().GetType();
for (int i = 0; i < _templates.arraySize; i++)
{
SerializedProperty existingItem = _templates.GetArrayElementAtIndex(i);
var existingViewHolder = existingItem.objectReferenceValue as GameObject;
if (existingViewHolder != null)
{
var existingType = existingViewHolder.GetComponent<ViewHolder>().GetType();
if (existingType == templateType)
{
return true;
}
}
}
return false;
}
#endregion
#region Helper Methods
private void DrawManagedReferenceProperties(SerializedProperty property)
{
SerializedProperty iterator = property.Copy();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false;
if (iterator.name == "m_Script") continue;
EditorGUILayout.PropertyField(iterator, true);
}
}
private void DrawComponentProperties(MonoBehaviour component, string header = null)
{
if (component == null) return;
EditorGUILayout.Space(3);
if (!string.IsNullOrEmpty(header))
{
EditorGUILayout.LabelField(header, EditorStyles.boldLabel);
}
SerializedObject componentSerializedObject = new SerializedObject(component);
componentSerializedObject.Update();
SerializedProperty property = componentSerializedObject.GetIterator();
bool enterChildren = true;
while (property.NextVisible(enterChildren))
{
enterChildren = false;
if (property.name == "m_Script") continue;
EditorGUILayout.PropertyField(property, true);
}
componentSerializedObject.ApplyModifiedProperties();
}
#endregion
#region Restore Helpers ()
private void RestoreLayoutManagerFromTypeNameIfMissing()
{
try
{
if (_layoutManager == null || _layoutManagerTypeName == null) return;
// 如果 managedReferenceValue 已经存在就不必恢复
if (_layoutManager.managedReferenceValue != null) return;
string typeName = _layoutManagerTypeName.stringValue;
if (string.IsNullOrEmpty(typeName)) return;
Type type = AlicizaX.Utility.Assembly.GetType(typeName);
if (type == null)
{
Debug.LogWarning($"LayoutManager type '{typeName}' not found. Cannot restore layout manager.");
return;
}
if (!typeof(ILayoutManager).IsAssignableFrom(type))
{
Debug.LogWarning($"Type '{typeName}' does not implement ILayoutManager. Cannot restore layout manager.");
_layoutManagerTypeName.stringValue = "";
return;
}
// 实例化并赋值
var instance = Activator.CreateInstance(type);
_layoutManager.managedReferenceValue = instance;
// 尝试刷新下拉列表并更新选择索引
RefreshLayoutTypes();
_selectedLayoutIndex = Mathf.Clamp(_layoutTypeNames.IndexOf(typeName), 0, _layoutTypeNames.Count - 1);
Debug.Log($"LayoutManager restored from type name '{typeName}'.");
}
catch (Exception e)
{
Debug.LogError($"Error restoring LayoutManager: {e}");
_layoutManager.managedReferenceValue = null;
_layoutManagerTypeName.stringValue = "";
}
}
private void RestoreScrollerFromTypeNameIfMissing()
{
try
{
if (_scroller == null || _scrollerTypeName == null) return;
// 如果 objectReferenceValue 已经存在就不必恢复
if (_scroller.objectReferenceValue != null) return;
string typeName = _scrollerTypeName.stringValue;
if (string.IsNullOrEmpty(typeName)) return;
Type type = AlicizaX.Utility.Assembly.GetType(typeName) ?? Type.GetType(typeName);
if (type == null)
{
Debug.LogWarning($"Scroller type '{typeName}' not found. Cannot restore scroller component.");
_scrollerTypeName.stringValue = "";
return;
}
if (!typeof(MonoBehaviour).IsAssignableFrom(type) || !typeof(IScroller).IsAssignableFrom(type))
{
Debug.LogWarning($"Type '{typeName}' is not a MonoBehaviour implementing IScroller. Cannot restore scroller.");
_scrollerTypeName.stringValue = "";
return;
}
var recyclerView = target as RecyclerView;
if (recyclerView == null) return;
// 给目标 GameObject 添加组件(使用 Undo 以支持撤销)
var newComp = Undo.AddComponent(recyclerView.gameObject, type) as MonoBehaviour;
if (newComp == null)
{
Debug.LogError($"Failed to add scroller component of type '{typeName}' to GameObject '{recyclerView.gameObject.name}'.");
_scrollerTypeName.stringValue = "";
return;
}
_scroller.objectReferenceValue = newComp;
// 刷新 scroller 类型列表并更新索引(如果存在)
RefreshScrollerTypes();
_selectedScrollerIndex = Mathf.Clamp(_scrollerTypeNames.IndexOf(typeName), 0, _scrollerTypeNames.Count - 1);
EditorUtility.SetDirty(recyclerView);
Debug.Log($"Scroller component of type '{typeName}' restored and attached to GameObject '{recyclerView.gameObject.name}'.");
}
catch (Exception e)
{
Debug.LogError($"Error restoring Scroller: {e}");
_scroller.objectReferenceValue = null;
_scrollerTypeName.stringValue = "";
}
}
#endregion
}
}