From cbae052e0f402bf0a99c57e5481b8ff76f89d2d5 Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Thu, 9 Jul 2026 17:10:40 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=A0=B7=E5=BC=8F=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E6=A0=B7=E5=BC=8F=E8=B7=AF=E5=BE=84=E7=9A=84=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=80=BC=E3=80=82=20=E9=92=88=E5=AF=B9=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E7=94=9F=E6=88=90=E9=A2=84=E5=88=B6=E4=BD=93=E7=9A=84?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E8=BF=9B=E8=A1=8C=E4=BC=98=E5=8C=96=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Editor/SuperStyleSheet.meta | 8 - Editor/SuperStyleSheet/StyleFieldDrawer.cs | 275 ------------------ Editor/XTable/XericUIActionTableEditor.cs | 38 +++ Runtime/Core/StyleSheetUI/StyleSheetState.cs | 49 ++-- Runtime/XTable/Core/XTableCellData.cs | 77 ++--- .../Rendering/Component/TableRenderer.cs | 87 ++---- .../Rendering/Component/XericUIActionTable.cs | 164 ++++++----- .../Rendering/Elements/PrefabPrototypePool.cs | 186 ++++++++++++ .../Elements/PrefabPrototypePool.cs.meta | 2 +- 9 files changed, 412 insertions(+), 474 deletions(-) delete mode 100644 Editor/SuperStyleSheet.meta delete mode 100644 Editor/SuperStyleSheet/StyleFieldDrawer.cs create mode 100644 Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs rename Editor/SuperStyleSheet/StyleFieldDrawer.cs.meta => Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs.meta (83%) diff --git a/Editor/SuperStyleSheet.meta b/Editor/SuperStyleSheet.meta deleted file mode 100644 index 668124b..0000000 --- a/Editor/SuperStyleSheet.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 44c3980d2307e514a869459f8153cda2 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Editor/SuperStyleSheet/StyleFieldDrawer.cs b/Editor/SuperStyleSheet/StyleFieldDrawer.cs deleted file mode 100644 index f5f1ecc..0000000 --- a/Editor/SuperStyleSheet/StyleFieldDrawer.cs +++ /dev/null @@ -1,275 +0,0 @@ -using UnityEditor; -using UnityEngine; -using XericLibrary.Runtime.SuperStyleSheet; - -namespace XericUIEditor -{ - /// - /// StyleField 的自定义 Inspector 属性绘制器。 - /// - /// 默认折叠状态:仅显示样式路径下拉框。 - /// 展开状态:显示命名空间、路径、资源值类型(有值时)、注解描述(有注解时)。 - /// 无值/无注解的行不显示。 - /// - [CustomPropertyDrawer(typeof(StyleField))] - public class StyleFieldDrawer : PropertyDrawer - { - private const float LINE_HEIGHT = 18f; - private const float PADDING = 2f; - private const float FOLD_BUTTON_WIDTH = 14f; - - private const string PROP_NAMESPACE = "_namespace"; - private const string PROP_STYLEPATH = "_stylePath"; - - /// 折叠状态(按 property path 缓存) - private static readonly System.Collections.Generic.Dictionary s_FoldoutStates - = new System.Collections.Generic.Dictionary(); - - public override float GetPropertyHeight(SerializedProperty property, GUIContent label) - { - var nsProp = property.FindPropertyRelative(PROP_NAMESPACE); - var pathProp = property.FindPropertyRelative(PROP_STYLEPATH); - if (nsProp == null || pathProp == null) - return PADDING + (LINE_HEIGHT + PADDING); - - string key = GetFoldKey(property); - bool expanded = s_FoldoutStates.TryGetValue(key, out bool f) && f; - - if (!expanded) - return PADDING + (LINE_HEIGHT + PADDING); - - // 基础:命名空间 + 路径 = 2 行 - int rowCount = 2; - - // 有值才显示值类型行 - string ns = GetNamespace(property); - string path = pathProp.stringValue; - StyleValue val = StyleManager.GetValue(ns, path); - if (val != null && val.ValueType != null) - rowCount++; - - // 有注解才显示注解行 - var annotations = StyleManager.GetAnnotations(ns, path); - if (annotations != null && annotations.Count > 0) - rowCount++; - - return PADDING + (1 + rowCount) * (LINE_HEIGHT + PADDING); - } - - public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) - { - string key = GetFoldKey(property); - bool expanded = s_FoldoutStates.TryGetValue(key, out bool fold) && fold; - - // 获取子属性 - SerializedProperty nsProp = property.FindPropertyRelative(PROP_NAMESPACE); - SerializedProperty pathProp = property.FindPropertyRelative(PROP_STYLEPATH); - - if (nsProp == null || pathProp == null) - { - EditorGUI.LabelField(position, label != null ? label.text : "Style Field", " (未初始化)"); - return; - } - - // === 标题行:折叠箭头 + 标签 + 路径下拉 === - Rect headerRect = new Rect(position.x, position.y + PADDING, position.width, LINE_HEIGHT); - - // 折叠按钮 - Rect foldRect = new Rect(headerRect.x, headerRect.y, FOLD_BUTTON_WIDTH, LINE_HEIGHT); - bool newFold = EditorGUI.Foldout(foldRect, expanded, GUIContent.none, true); - if (newFold != expanded) - { - s_FoldoutStates[key] = newFold; - expanded = newFold; - } - - // 标签 - Rect labelRect = new Rect(headerRect.x + FOLD_BUTTON_WIDTH, headerRect.y, - EditorGUIUtility.labelWidth - FOLD_BUTTON_WIDTH, LINE_HEIGHT); - EditorGUI.LabelField(labelRect, label); - - // 路径下拉框(始终可见) - Rect pathRect = new Rect(headerRect.x + EditorGUIUtility.labelWidth + 4f, headerRect.y, - headerRect.width - EditorGUIUtility.labelWidth - 8f, LINE_HEIGHT); - - string currentNs = GetNamespace(property); - - // 首次绘制时 xsss 可能尚未加载,当前命名空间若无效则自动切换 - EnsureValidNamespace(nsProp, ref currentNs); - - DrawPathDropdown(pathRect, pathProp, currentNs); - - if (!expanded) - return; - - // === 展开区域 === - float y = headerRect.y + LINE_HEIGHT + PADDING; - float indent = 16f; - float fieldWidth = position.width - indent; - - // --- 命名空间 --- - Rect nsLabelRect = new Rect(position.x + indent, y, 60f, LINE_HEIGHT); - Rect nsFieldRect = new Rect(position.x + indent + 64f, y, fieldWidth - 64f, LINE_HEIGHT); - EditorGUI.LabelField(nsLabelRect, "命名空间"); - DrawNamespaceDropdown(nsFieldRect, nsProp); - y += LINE_HEIGHT + PADDING; - - // --- 路径 --- - Rect pathLabelRect = new Rect(position.x + indent, y, 60f, LINE_HEIGHT); - Rect pathFieldRect = new Rect(position.x + indent + 64f, y, fieldWidth - 64f, LINE_HEIGHT); - EditorGUI.LabelField(pathLabelRect, "路径"); - DrawPathDropdown(pathFieldRect, pathProp, currentNs); - y += LINE_HEIGHT + PADDING; - - // --- 资源值类型(仅在有值时显示) --- - StyleValue currentValue = StyleManager.GetValue(currentNs, pathProp.stringValue); - if (currentValue != null && currentValue.ValueType != null) - { - Rect valLabelRect = new Rect(position.x + indent, y, 80f, LINE_HEIGHT); - Rect valFieldRect = new Rect(position.x + indent + 84f, y, fieldWidth - 84f, LINE_HEIGHT); - - EditorGUI.LabelField(valLabelRect, "值类型"); - - string typeDisplay = GetShortTypeName(currentValue.ValueType); - string valuePreview = currentValue.RawValue != null - ? currentValue.RawValue.ToString() - : ""; - - EditorGUI.LabelField(valFieldRect, $"{typeDisplay} = {valuePreview}"); - y += LINE_HEIGHT + PADDING; - } - - // --- 注解描述(仅在有注解时显示) --- - var annotations = StyleManager.GetAnnotations(currentNs, pathProp.stringValue); - if (annotations != null && annotations.Count > 0) - { - Rect annoLabelRect = new Rect(position.x + indent, y, 80f, LINE_HEIGHT); - Rect annoFieldRect = new Rect(position.x + indent + 84f, y, fieldWidth - 84f, LINE_HEIGHT); - - EditorGUI.LabelField(annoLabelRect, "注解"); - - // 优先显示 @Desc - annotations.TryGetValue("Desc", out string desc); - if (!string.IsNullOrEmpty(desc)) - EditorGUI.LabelField(annoFieldRect, desc); - else - { - foreach (var a in annotations) - { - EditorGUI.LabelField(annoFieldRect, $"@{a.Key}: {a.Value}"); - break; - } - } - } - - // 变更检测 - if (GUI.changed) - { - property.serializedObject.ApplyModifiedProperties(); - } - } - - #region 下拉框绘制 - - private void DrawNamespaceDropdown(Rect rect, SerializedProperty nsProp) - { - var namespaces = StyleManager.GetAvailableNamespaces(); - if (namespaces.Count == 0) - namespaces.Add("default"); - - int currentIndex = namespaces.IndexOf(nsProp.stringValue); - if (currentIndex < 0) currentIndex = 0; - - int newIndex = EditorGUI.Popup(rect, currentIndex, namespaces.ToArray()); - if (newIndex >= 0 && newIndex < namespaces.Count) - { - nsProp.stringValue = namespaces[newIndex]; - } - } - - private void DrawPathDropdown(Rect rect, SerializedProperty pathProp, string namespace_) - { - var paths = StyleManager.GetPathsForNamespace(namespace_); - - var displayOptions = new System.Collections.Generic.List(); - if (paths.Count == 0) - { - displayOptions.Add("(无可用路径)"); - } - else - { - foreach (var p in paths) - { - StyleValue val = StyleManager.GetValue(namespace_, p); - string typeStr = val?.ValueType != null ? GetShortTypeName(val.ValueType) : "?"; - string valStr = val?.RawValue != null ? val.RawValue.ToString() : ""; - if (valStr.Length > 20) valStr = valStr.Substring(0, 17) + "..."; - displayOptions.Add($"{p} [{typeStr}] {valStr}"); - } - } - - int currentIndex = System.Math.Max(0, paths.IndexOf(pathProp.stringValue)); - - int newIndex = EditorGUI.Popup(rect, currentIndex, displayOptions.ToArray()); - if (newIndex >= 0 && newIndex < paths.Count) - { - pathProp.stringValue = paths[newIndex]; - } - } - - #endregion - - #region 工具方法 - - private static string GetFoldKey(SerializedProperty property) - { - return $"{property.serializedObject.targetObject.GetInstanceID()}_{property.propertyPath}"; - } - - private static string GetNamespace(SerializedProperty property) - { - var nsProp = property.FindPropertyRelative(PROP_NAMESPACE); - string ns = nsProp?.stringValue; - return string.IsNullOrEmpty(ns) ? "default" : ns; - } - - private static string GetShortTypeName(System.Type type) - { - if (type == null) return "?"; - switch (type.Name) - { - case "Single": return "float"; - case "Int32": return "int"; - case "String": return "string"; - case "Boolean": return "bool"; - default: return type.Name; - } - } - - /// - /// 若当前命名空间无可用路径(xsss 尚未加载),自动切换至首个有路径的命名空间。 - /// - private static void EnsureValidNamespace(SerializedProperty nsProp, ref string currentNs) - { - var paths = StyleManager.GetPathsForNamespace(currentNs); - if (paths.Count > 0) - return; - - // 当前命名空间不可用,尝试其他命名空间 - var allNs = StyleManager.GetAvailableNamespaces(); - foreach (var ns in allNs) - { - if (ns == currentNs) continue; - var altPaths = StyleManager.GetPathsForNamespace(ns); - if (altPaths.Count > 0) - { - currentNs = ns; - nsProp.stringValue = ns; - return; - } - } - } - - #endregion - } -} diff --git a/Editor/XTable/XericUIActionTableEditor.cs b/Editor/XTable/XericUIActionTableEditor.cs index 1d2d89f..af00346 100644 --- a/Editor/XTable/XericUIActionTableEditor.cs +++ b/Editor/XTable/XericUIActionTableEditor.cs @@ -177,6 +177,44 @@ namespace XericUIEditor.XTable if (GUILayout.Button("手动刷新视图", GUILayout.Height(22))) EditorApplication.delayCall += () => m_Table.RefreshView(); + // ── 预制体配置 ── + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("单元格预制体配置", EditorStyles.boldLabel); + int selRow = m_Table.SelectedRow; + int selCol = m_Table.SelectedCol; + if (selRow < 0 || selCol < 0) + { + EditorGUILayout.HelpBox("请先在 Scene View 中点击选中一个单元格", MessageType.Info); + } + else + { + var data = m_Table.TableData?.GetCell(selRow, selCol); + GameObject currentPrefab = data?.Prefab; + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.TextField("目标单元格", $"({selRow}, {selCol})"); + EditorGUI.EndDisabledGroup(); + + GameObject newPrefab = (GameObject)EditorGUILayout.ObjectField( + "预制体", currentPrefab, typeof(GameObject), false); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("应用预制体", GUILayout.Height(22))) + { + Undo.RecordObject(m_Table, "Set Cell Prefab"); + m_Table.SetCellPrefab(selRow, selCol, newPrefab); + EditorUtility.SetDirty(m_Table); + Repaint(); + } + if (GUILayout.Button("清除预制体", GUILayout.Height(22))) + { + Undo.RecordObject(m_Table, "Clear Cell Prefab"); + m_Table.ClearCellPrefab(selRow, selCol); + EditorUtility.SetDirty(m_Table); + Repaint(); + } + EditorGUILayout.EndHorizontal(); + } + EditorGUI.indentLevel--; } diff --git a/Runtime/Core/StyleSheetUI/StyleSheetState.cs b/Runtime/Core/StyleSheetUI/StyleSheetState.cs index 30d0ef1..840e61e 100644 --- a/Runtime/Core/StyleSheetUI/StyleSheetState.cs +++ b/Runtime/Core/StyleSheetUI/StyleSheetState.cs @@ -1,7 +1,7 @@ using System; using UnityEngine; - +using UnityEngine.UI; using XericLibrary.Runtime.SuperStyleSheet; namespace XericUI.Core.StyleSheetUI @@ -13,31 +13,31 @@ namespace XericUI.Core.StyleSheetUI [Serializable] public class StyleSheetState { - private const string k_DefaultColorBasePath = "/button/TargetGraphic/ColorTint"; - private const string k_DefaultTextureBasePath = "/button/TargetGraphic/SpriteState"; + private const string k_DefaultColorBasePath = "/TargetGraphic/ColorTint"; + private const string k_DefaultTextureBasePath = "/TargetGraphic/SpriteState"; private const string k_DefaultNamespace = "default"; #region 序列化字段 [SerializeField] [Tooltip("正常状态")] - private StyleField m_Normal = new StyleField { Namespace = k_DefaultNamespace, StylePath = k_DefaultColorBasePath + "/NormalColor" }; + private StyleField m_Normal; [SerializeField] [Tooltip("高亮状态")] - private StyleField m_Highlighted = new StyleField { Namespace = k_DefaultNamespace, StylePath = k_DefaultColorBasePath + "/HighlightedColor" }; + private StyleField m_Highlighted; [SerializeField] [Tooltip("按下状态")] - private StyleField m_Pressed = new StyleField { Namespace = k_DefaultNamespace, StylePath = k_DefaultColorBasePath + "/PressedColor" }; + private StyleField m_Pressed; [SerializeField] [Tooltip("选中状态")] - private StyleField m_Selected = new StyleField { Namespace = k_DefaultNamespace, StylePath = k_DefaultColorBasePath + "/SelectedColor" }; + private StyleField m_Selected; [SerializeField] [Tooltip("禁用状态")] - private StyleField m_Disabled = new StyleField { Namespace = k_DefaultNamespace, StylePath = k_DefaultColorBasePath + "/DisabledColor" }; + private StyleField m_Disabled; [SerializeField] [Range(0.001f, 1f)] @@ -46,6 +46,22 @@ namespace XericUI.Core.StyleSheetUI #endregion + public StyleSheetState(string stylePathRoot, Selectable.Transition mode) + { + string state = mode switch + { + Selectable.Transition.ColorTint => "ColorTint", + Selectable.Transition.SpriteSwap => "SpriteState", + Selectable.Transition.Animation => "Animation", + _ => "None" + }; + m_Normal = new StyleField($"{stylePathRoot}/TargetGraphic/{state}", k_DefaultNamespace); + m_Highlighted = new StyleField($"{stylePathRoot}/TargetGraphic/{state}", k_DefaultNamespace); + m_Pressed = new StyleField($"{stylePathRoot}/TargetGraphic/{state}", k_DefaultNamespace); + m_Selected = new StyleField($"{stylePathRoot}/TargetGraphic/{state}", k_DefaultNamespace); + m_Disabled = new StyleField($"{stylePathRoot}/TargetGraphic/{state}", k_DefaultNamespace); + } + #region 公开属性 public StyleField Normal => m_Normal; @@ -67,22 +83,7 @@ namespace XericUI.Core.StyleSheetUI }; /// 默认实例(五态默认匹配 ColorTint 颜色块) - public static StyleSheetState Default => new StyleSheetState(); - - /// - /// 创建纹理五态样式块,StylePath 默认匹配 SpriteState 块。 - /// - public static StyleSheetState CreateTexture(string ns = k_DefaultNamespace) - { - var state = new StyleSheetState(); - state.Normal.StylePath = k_DefaultTextureBasePath + "/NormalSprite"; - state.Highlighted.StylePath = k_DefaultTextureBasePath + "/HighlightedSprite"; - state.Pressed.StylePath = k_DefaultTextureBasePath + "/PressedSprite"; - state.Selected.StylePath = k_DefaultTextureBasePath + "/SelectedSprite"; - state.Disabled.StylePath = k_DefaultTextureBasePath + "/DisabledSprite"; - foreach (var f in state.AllFields) f.Namespace = ns; - return state; - } + public static StyleSheetState Default => new StyleSheetState("button", Selectable.Transition.ColorTint); #endregion diff --git a/Runtime/XTable/Core/XTableCellData.cs b/Runtime/XTable/Core/XTableCellData.cs index 00dc7db..a5257fb 100644 --- a/Runtime/XTable/Core/XTableCellData.cs +++ b/Runtime/XTable/Core/XTableCellData.cs @@ -1,48 +1,53 @@ using System; - using UnityEngine; namespace XericUI.XTable.Core { - /// - /// 单元格数据模型——仅保存数据内容,不保存自身尺寸。 - /// 尺寸由渲染阶段的行列标题(行高/列宽)决定。 - /// 样式通过 StyleNamespace 字符串引用 StyleManager 中的命名空间。 - /// - [Serializable] - public class XTableCellData - { - /// 文本内容 - public string Text; + /// + /// 单元格数据模型——仅保存数据内容,不保存自身尺寸。 + /// 尺寸由渲染阶段的行列标题(行高/列宽)决定。 + /// 样式通过 StyleNamespace 字符串引用 StyleManager 中的命名空间。 + /// + [Serializable] + public class XTableCellData + { + /// 文本内容 + public string Text; - /// 图片纹理 - public Texture2D Image; + /// 图片纹理 + public Texture2D Image; - /// 预制体对象(用于嵌入复杂 UI 元素) - public GameObject Prefab; + /// 预制体对象(用于嵌入复杂 UI 元素) + public GameObject Prefab; - /// - /// 样式命名空间——对应 StyleManager 中的命名空间。 - /// 为空或 "xeric_table_default" 时使用默认表格样式。 - /// 渲染时从此命名空间读取 /cell/bg/color、/cell/font/size 等路径。 - /// - public string StyleNamespace; + /// + /// 样式命名空间——对应 StyleManager 中的命名空间。 + /// 为空或 "xeric_table_default" 时使用默认表格样式。 + /// 渲染时从此命名空间读取 /cell/bg/color、/cell/font/size 等路径。 + /// + public string StyleNamespace; - public XTableCellData() { } + /// 预制体实例从对象池取出时回调——外部可在此初始化实例内容 + [NonSerialized] public System.Action OnPrefabAcquire; - public XTableCellData(string text) - { - Text = text; - } + /// 预制体实例归还对象池前回调——外部可在此清理引用 + [NonSerialized] public System.Action OnPrefabRelease; - public XTableCellData(string text, string styleNamespace = null) : this(text) - { - StyleNamespace = styleNamespace; - } + public XTableCellData() + { } - public XTableCellData(string text, Texture2D image, string styleNamespace = null) : this(text, styleNamespace) - { - Image = image; - } - } -} + public XTableCellData(string text) => Text = text; + + public XTableCellData(string text, string styleNamespace = null) : this(text) => + StyleNamespace = styleNamespace; + + public XTableCellData(string text, Texture2D image, string styleNamespace = null) : + this(text, styleNamespace) => Image = image; + + public void ClearEvent() + { + OnPrefabAcquire = null; + OnPrefabRelease = null; + } + } +} \ No newline at end of file diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs b/Runtime/XTable/Rendering/Component/TableRenderer.cs index 315300c..35a7c03 100644 --- a/Runtime/XTable/Rendering/Component/TableRenderer.cs +++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs @@ -17,11 +17,14 @@ namespace XericUI.XTable.Rendering.Component #region 内部字段 private Transform m_Parent; - private CellAssembler m_Assembler; - private GameObject m_BackgroundGo; + private CellAssembler m_Assembler; + private GameObject m_BackgroundGo; - /// 当前渲染器中所有预制体实例的缓存 - private Dictionary<(int, int), GameObject> m_PrefabInstances; + /// 共享预制体对象池引用(由 XericUIActionTable 传入) + public PrefabPrototypePool PrefabPool; + + /// 共享活跃预制体映射表(由 XericUIActionTable 传入) + public Dictionary<(int, int), GameObject> ActivePrefabMap; #endregion @@ -88,7 +91,6 @@ namespace XericUI.XTable.Rendering.Component public void DestroyAll() { m_Assembler?.DestroyAll(); - ClearPrefabInstances(); if (m_BackgroundGo != null) { @@ -262,8 +264,16 @@ namespace XericUI.XTable.Rendering.Component XericUIActionTable.STYLE_CELL_FONT_ASSET) ?? txt.Text.font; } - if (data.Prefab != null) - PositionPrefabInstance(r, c, data.Prefab, cellX, cellY, cellW, cellH); + if (data.Prefab != null && PrefabPool != null) + { + GameObject instance = PrefabPool.Acquire(data.Prefab, r, c); + RectTransform prt = instance.GetComponent(); + prt.anchoredPosition = new Vector2(cellX, cellY); + prt.sizeDelta = new Vector2(cellW, cellH); + if (ActivePrefabMap != null) + ActivePrefabMap[(r, c)] = instance; + data.OnPrefabAcquire?.Invoke(instance); + } } } } @@ -336,65 +346,22 @@ namespace XericUI.XTable.Rendering.Component txt.Text.text = data.Text; } - if (data.Prefab != null) - PositionPrefabInstance(r, c, data.Prefab, cellX, cellY, cellW, cellH); + if (data.Prefab != null && PrefabPool != null) + { + GameObject instance = PrefabPool.Acquire(data.Prefab, r, c); + RectTransform prt = instance.GetComponent(); + prt.anchoredPosition = new Vector2(cellX, cellY); + prt.sizeDelta = new Vector2(cellW, cellH); + if (ActivePrefabMap != null) + ActivePrefabMap[(r, c)] = instance; + data.OnPrefabAcquire?.Invoke(instance); + } } } } #endregion - #region 预制体管理 - - private GameObject GetOrCreatePrefabInstance(int row, int col, GameObject prefab) - { - if (m_PrefabInstances == null) - m_PrefabInstances = new Dictionary<(int, int), GameObject>(); - - var key = (row, col); - if (m_PrefabInstances.TryGetValue(key, out GameObject existing) && existing != null) - return existing; - - GameObject instance = Object.Instantiate(prefab, m_Parent); - instance.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; - instance.name = $"_prefab_{row}_{col}"; - m_PrefabInstances[key] = instance; - return instance; - } - - private void PositionPrefabInstance(int row, int col, GameObject prefab, - float cellX, float cellY, float cellW, float cellH) - { - GameObject instance = GetOrCreatePrefabInstance(row, col, prefab); - if (instance == null) return; - - RectTransform prt = instance.GetComponent(); - if (prt == null) prt = instance.AddComponent(); - - prt.anchorMin = new Vector2(0, 1); - prt.anchorMax = new Vector2(0, 1); - prt.pivot = new Vector2(0, 1); - prt.anchoredPosition = new Vector2(cellX, cellY); - prt.sizeDelta = new Vector2(cellW, cellH); - } - - /// 销毁所有缓存预制体实例 - public void ClearPrefabInstances() - { - if (m_PrefabInstances == null) return; - foreach (var kv in m_PrefabInstances) - { - if (kv.Value != null) - { - if (Application.isPlaying) Object.Destroy(kv.Value); - else Object.DestroyImmediate(kv.Value); - } - } - m_PrefabInstances.Clear(); - } - - #endregion - #region 坐标计算 private float CalcRowTopY(int row, float[] rowHeights) diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs index ccd9f0d..8940bac 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -70,8 +70,11 @@ namespace XericUI.XTable.Rendering.Component [System.NonSerialized] private GameObject m_BackgroundGo; - /// 预制体实例缓存,key 为 (row, col),用于复用已实例化的预制体 - [System.NonSerialized] private Dictionary<(int, int), GameObject> m_PrefabInstances; + /// 预制体对象池——按原型分组管理 + [System.NonSerialized] private PrefabPrototypePool m_PrefabPool; + + /// 活跃预制体实例映射 (row, col) → instance,用于回调编排 + [System.NonSerialized] private Dictionary<(int, int), GameObject> m_ActivePrefabMap; /// 表格四叉树——按 StyleNamespace 分区,渲染阶段查询可见象限 [System.NonSerialized] private XTableQuadtree m_Quadtree; @@ -260,7 +263,14 @@ namespace XericUI.XTable.Rendering.Component } if (data.Prefab != null) - PositionPrefabInstance(row, col, data.Prefab, cellX, cellY, cellW, cellH); + { + GameObject instance = m_PrefabPool.Acquire(data.Prefab, row, col); + RectTransform prt = instance.GetComponent(); + prt.anchoredPosition = new Vector2(cellX, cellY); + prt.sizeDelta = new Vector2(cellW, cellH); + m_ActivePrefabMap[(row, col)] = instance; + data.OnPrefabAcquire?.Invoke(instance); + } } } } @@ -332,7 +342,14 @@ namespace XericUI.XTable.Rendering.Component } if (data.Prefab != null) - PositionPrefabInstance(row, col, data.Prefab, cellX, cellY, cellW, cellH); + { + GameObject instance = m_PrefabPool.Acquire(data.Prefab, row, col); + RectTransform prt = instance.GetComponent(); + prt.anchoredPosition = new Vector2(cellX, cellY); + prt.sizeDelta = new Vector2(cellW, cellH); + m_ActivePrefabMap[(row, col)] = instance; + data.OnPrefabAcquire?.Invoke(instance); + } } } } @@ -353,6 +370,8 @@ namespace XericUI.XTable.Rendering.Component m_TableData = new XTableData(); m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace); + m_PrefabPool = new PrefabPrototypePool(ContentTransform); + m_ActivePrefabMap = new Dictionary<(int, int), GameObject>(); RecalculateTotalSize(); EnsureDefaultStyleNamespace(); CreateDefaultSampleData(); @@ -405,7 +424,8 @@ namespace XericUI.XTable.Rendering.Component // 销毁所有动态生成的对象 if (m_Assembler != null) m_Assembler.DestroyAll(); - ClearPrefabInstances(); + m_PrefabPool?.DestroyAll(); + m_ActivePrefabMap?.Clear(); DestroyBorderRenderer(); if (m_BackgroundGo != null) { @@ -497,15 +517,36 @@ namespace XericUI.XTable.Rendering.Component MarkDirty(TableDirtyType.DataChanged); } - public void SetCellText(int row, int col, string text) + public XTableCellData SetCellText(int row, int col, string text) { var data = TableData.GetOrCreateCell(row, col); data.Text = text; MarkDirty(TableDirtyType.DataChanged); + return data; } public XTableCellData GetCellData(int row, int col) - => TableData.GetOrCreateCell(row, col); + => TableData.GetOrCreateCell(row, col); + + /// 为指定单元格设置预制体 + public XTableCellData SetCellPrefab(int row, int col, GameObject prefab) + { + var data = TableData.GetOrCreateCell(row, col); + data.Prefab = prefab; + MarkDirty(TableDirtyType.DataChanged); + return data; + } + + /// 清除指定单元格的预制体 + public void ClearCellPrefab(int row, int col) + { + var data = TableData.GetCell(row, col); + if (data == null) return; + data.Prefab = null; + data.OnPrefabAcquire = null; + data.OnPrefabRelease = null; + MarkDirty(TableDirtyType.DataChanged); + } /// 设置单元格的样式命名空间 public void SetCellStyleNamespace(int row, int col, string styleNamespace) @@ -808,19 +849,32 @@ namespace XericUI.XTable.Rendering.Component bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); } - // 清理上一次渲染的预制体实例 - ClearPrefabInstances(); + // 归还预制体前,触发所有活跃实例的释放回调 + if (m_ActivePrefabMap != null) + { + foreach (var kv in m_ActivePrefabMap) + { + var data = TableData.GetCell(kv.Key.Item1, kv.Key.Item2); + data?.OnPrefabRelease?.Invoke(kv.Value); + } + m_ActivePrefabMap.Clear(); + } - // 回收池 + // 回收预制体池 + m_PrefabPool?.ReturnAll(); + + // 回收 CellAssembler 池 m_Assembler.ReturnAll(); // 额外清理孤立的直接子对象(防止域重载或异常流程产生的残留) Transform poolRoot = m_Assembler.PoolRoot; + Transform prefabPoolRoot = m_PrefabPool?.PoolRoot; Transform contentT = ContentTransform; for (int i = contentT.childCount - 1; i >= 0; i--) { var child = contentT.GetChild(i); - if ((poolRoot != null && child == poolRoot)) continue; + if (poolRoot != null && child == poolRoot) continue; + if (prefabPoolRoot != null && child == prefabPoolRoot) continue; if (m_BackgroundGo != null && child.gameObject == m_BackgroundGo) continue; child.gameObject.hideFlags = HideFlags.None; @@ -931,6 +985,7 @@ namespace XericUI.XTable.Rendering.Component public void FullRebuild() { m_Assembler?.DestroyAll(); + m_PrefabPool?.DestroyAll(); ClearAllChildren(); m_BackgroundGo = null; m_BorderRenderer = null; @@ -938,6 +993,8 @@ namespace XericUI.XTable.Rendering.Component m_TableData = new XTableData(); m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace); + m_PrefabPool = new PrefabPrototypePool(ContentTransform); + m_ActivePrefabMap = new Dictionary<(int, int), GameObject>(); RecalculateTotalSize(); CreateDefaultSampleData(); CreateBackground(); @@ -992,7 +1049,8 @@ namespace XericUI.XTable.Rendering.Component m_TableData.ClearAllMerges(); m_TableData.BlockMap.Clear(); } - ClearPrefabInstances(); + m_ActivePrefabMap?.Clear(); + m_PrefabPool?.ReturnAll(); m_Quadtree?.Clear(); ClearSelection(); MarkDirty(TableDirtyType.DataChanged); @@ -1005,7 +1063,8 @@ namespace XericUI.XTable.Rendering.Component public void ClearTable() { m_Assembler?.DestroyAll(); - ClearPrefabInstances(); + m_PrefabPool?.DestroyAll(); + m_ActivePrefabMap?.Clear(); m_Quadtree?.Clear(); ClearAllChildren(); m_BackgroundGo = null; @@ -1017,6 +1076,8 @@ namespace XericUI.XTable.Rendering.Component ClearSelection(); m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace); + m_PrefabPool = new PrefabPrototypePool(ContentTransform); + m_ActivePrefabMap = new Dictionary<(int, int), GameObject>(); MarkDirty(TableDirtyType.All); RecalculateTotalSize(); CreateBackground(); @@ -1154,7 +1215,14 @@ namespace XericUI.XTable.Rendering.Component // 预制体渲染 if (data.Prefab != null) - PositionPrefabInstance(r, c, data.Prefab, cellX, cellY, cellW, cellH); + { + GameObject instance = m_PrefabPool.Acquire(data.Prefab, r, c); + RectTransform prt = instance.GetComponent(); + prt.anchoredPosition = new Vector2(cellX, cellY); + prt.sizeDelta = new Vector2(cellW, cellH); + m_ActivePrefabMap[(r, c)] = instance; + data.OnPrefabAcquire?.Invoke(instance); + } } } return; @@ -1230,68 +1298,20 @@ namespace XericUI.XTable.Rendering.Component // 预制体渲染 if (data.Prefab != null) - PositionPrefabInstance(r, c, data.Prefab, cellX, cellY, cellW, cellH); + { + GameObject instance = m_PrefabPool.Acquire(data.Prefab, r, c); + RectTransform prt = instance.GetComponent(); + prt.anchoredPosition = new Vector2(cellX, cellY); + prt.sizeDelta = new Vector2(cellW, cellH); + m_ActivePrefabMap[(r, c)] = instance; + data.OnPrefabAcquire?.Invoke(instance); + } } } } #endregion - #region 预制体管理 - - /// 获取或创建指定单元格的预制体实例 - private GameObject GetOrCreatePrefabInstance(int row, int col, GameObject prefab) - { - if (m_PrefabInstances == null) - m_PrefabInstances = new Dictionary<(int, int), GameObject>(); - - var key = (row, col); - if (m_PrefabInstances.TryGetValue(key, out GameObject existing) && existing != null) - return existing; - - GameObject instance = Instantiate(prefab, ContentTransform); - instance.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; - instance.name = $"_prefab_{row}_{col}"; - m_PrefabInstances[key] = instance; - return instance; - } - - /// 定位预制体实例到指定单元格区域(锚点左上角,枢轴左上角) - private void PositionPrefabInstance(int row, int col, GameObject prefab, float cellX, float cellY, float cellW, float cellH) - { - GameObject instance = GetOrCreatePrefabInstance(row, col, prefab); - if (instance == null) return; - - RectTransform prt = instance.GetComponent(); - if (prt == null) prt = instance.AddComponent(); - - prt.anchorMin = new Vector2(0, 1); - prt.anchorMax = new Vector2(0, 1); - prt.pivot = new Vector2(0, 1); - prt.anchoredPosition = new Vector2(cellX, cellY); - prt.sizeDelta = new Vector2(cellW, cellH); - } - - /// 销毁所有已缓存的预制体实例 - private void ClearPrefabInstances() - { - if (m_PrefabInstances == null) return; - - foreach (var kv in m_PrefabInstances) - { - if (kv.Value != null) - { - if (Application.isPlaying) - Destroy(kv.Value); - else - DestroyImmediate(kv.Value); - } - } - m_PrefabInstances.Clear(); - } - - #endregion - #region 坐标计算 private float GetRowTopY(int row) @@ -1667,6 +1687,10 @@ namespace XericUI.XTable.Rendering.Component m_ViewportRenderer.EndRow = m_FreezeRowCount; m_ViewportRenderer.StartCol = 0; m_ViewportRenderer.EndCol = m_FreezeColCount; + + // 共享预制体对象池 + m_ViewportRenderer.PrefabPool = m_PrefabPool; + m_ViewportRenderer.ActivePrefabMap = m_ActivePrefabMap; } /// 渲染 Viewport 层冻结单元格,随滚动偏移调整位置 diff --git a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs new file mode 100644 index 0000000..bcb61e1 --- /dev/null +++ b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs @@ -0,0 +1,186 @@ +using System.Collections.Generic; + +using UnityEngine; +using UnityEngine.Pool; + +namespace XericUI.XTable.Rendering.Elements +{ + /// + /// 预制体对象池——按原型分组管理,每个预制体对应一个独立的对象池。 + /// 使用 Stack + HashSet 模式与 保持一致。 + /// + public class PrefabPrototypePool + { + #region 内部类型 + + private class PrefabPool + { + public GameObject Prototype; + /// 休眠实例栈(池满时先归还的优先复用) + public Stack Available = new Stack(); + /// 活跃实例集合(O(1) 查找 + 防止重复归还) + public HashSet Active = new HashSet(); + } + + #endregion + + #region 字段 + + private Dictionary m_Pools = new Dictionary(); + private Transform m_Parent; + private Transform m_PoolRoot; + + /// 池根节点(所有回收对象均挂在此节点下,节点本身处于 inactive) + public Transform PoolRoot => m_PoolRoot; + + private const HideFlags POOL_FLAGS = HideFlags.DontSave | HideFlags.HideInHierarchy; + + private static readonly Vector2 AnchorTopLeft = new Vector2(0, 1); + private static readonly Vector2 PivotTopLeft = new Vector2(0, 1); + + #endregion + + #region 构造 / 销毁 + + public PrefabPrototypePool(Transform parent) + { + m_Parent = parent; + + GameObject poolRoot = new GameObject("_PrefabPoolRoot"); + poolRoot.hideFlags = POOL_FLAGS; + poolRoot.transform.SetParent(parent, false); + poolRoot.SetActive(false); + m_PoolRoot = poolRoot.transform; + } + + /// 彻底销毁所有池对象(活跃 + 休眠 + 池根) + public void DestroyAll() + { + foreach (var kv in m_Pools) + { + var pool = kv.Value; + // 销毁活跃实例 + foreach (var go in pool.Active) + KillObject(go); + pool.Active.Clear(); + // 销毁休眠实例 + while (pool.Available.Count > 0) + KillObject(pool.Available.Pop()); + } + m_Pools.Clear(); + + if (m_PoolRoot != null) + { + KillObject(m_PoolRoot.gameObject); + m_PoolRoot = null; + } + } + + private static void KillObject(GameObject go) + { + if (go == null) return; + if (Application.isPlaying) + Object.Destroy(go); + else + Object.DestroyImmediate(go); + } + + #endregion + + #region 池操作 + + /// 从池中获取实例(池空则 Instantiate 新的) + public GameObject Acquire(GameObject prefab, int row, int col) + { + if (prefab == null) return null; + + var pool = GetOrCreatePool(prefab); + + GameObject instance; + if (pool.Available.Count > 0) + { + instance = pool.Available.Pop(); + instance.SetActive(true); + instance.transform.SetParent(m_Parent, false); + } + else + { + instance = Object.Instantiate(prefab, m_Parent); + instance.hideFlags = POOL_FLAGS; + } + + instance.name = $"_prefab_{row}_{col}"; + + // 强制 RectTransform 左上角坐标系 + RectTransform rt = instance.GetComponent(); + if (rt == null) rt = instance.AddComponent(); + rt.anchorMin = AnchorTopLeft; + rt.anchorMax = AnchorTopLeft; + rt.pivot = PivotTopLeft; + + pool.Active.Add(instance); + return instance; + } + + /// 将指定实例归还对象池(Deactivate + 移入 PoolRoot) + public void Release(GameObject instance) + { + if (instance == null) return; + + // 查找所属池 + foreach (var kv in m_Pools) + { + if (kv.Value.Active.Remove(instance)) + { + instance.SetActive(false); + instance.transform.SetParent(m_PoolRoot, false); + kv.Value.Available.Push(instance); + return; + } + } + + // 不属于任何池(异常情况),直接销毁 + KillObject(instance); + } + + /// 将所有活跃实例回收至池 + public void ReturnAll() + { + foreach (var kv in m_Pools) + { + var pool = kv.Value; + foreach (var go in pool.Active) + { + go.SetActive(false); + go.transform.SetParent(m_PoolRoot, false); + pool.Available.Push(go); + } + pool.Active.Clear(); + } + } + + /// 获取当前所有活跃实例(用于外部遍历回调) + public IReadOnlyCollection GetActive(GameObject prefab) + { + if (prefab == null || !m_Pools.TryGetValue(prefab, out var pool)) + return System.Array.Empty(); + return pool.Active; + } + + #endregion + + #region 内部方法 + + private PrefabPool GetOrCreatePool(GameObject prefab) + { + if (!m_Pools.TryGetValue(prefab, out var pool)) + { + pool = new PrefabPool { Prototype = prefab }; + m_Pools[prefab] = pool; + } + return pool; + } + + #endregion + } +} diff --git a/Editor/SuperStyleSheet/StyleFieldDrawer.cs.meta b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs.meta similarity index 83% rename from Editor/SuperStyleSheet/StyleFieldDrawer.cs.meta rename to Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs.meta index ce7f7a4..08d3865 100644 --- a/Editor/SuperStyleSheet/StyleFieldDrawer.cs.meta +++ b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 8ee1f2c601403504faea388e1f91f07b +guid: 0bbffde7baff2d445b8b002b56b5c84d MonoImporter: externalObjects: {} serializedVersion: 2