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 _layoutTypeNames = new List(); private int _selectedLayoutIndex; #endregion #region Serialized Properties - Scroller private SerializedProperty _scroll; private SerializedProperty _scroller; private SerializedProperty _scrollerTypeName; private List _scrollerTypes = new List(); private List _scrollerTypeNames = new List(); 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(); 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() .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(); 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(); 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(); 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(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(); 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(); 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().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().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 } }