补充样式组件样式路径的默认值。

针对表格生成预制体的逻辑进行优化。
This commit is contained in:
2026-07-09 17:10:40 +08:00
parent 35a84332b7
commit cbae052e0f
9 changed files with 412 additions and 474 deletions
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 44c3980d2307e514a869459f8153cda2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
-275
View File
@@ -1,275 +0,0 @@
using UnityEditor;
using UnityEngine;
using XericLibrary.Runtime.SuperStyleSheet;
namespace XericUIEditor
{
/// <summary>
/// StyleField 的自定义 Inspector 属性绘制器。
///
/// 默认折叠状态:仅显示样式路径下拉框。
/// 展开状态:显示命名空间、路径、资源值类型(有值时)、注解描述(有注解时)。
/// 无值/无注解的行不显示。
/// </summary>
[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";
/// <summary>折叠状态(按 property path 缓存)</summary>
private static readonly System.Collections.Generic.Dictionary<string, bool> s_FoldoutStates
= new System.Collections.Generic.Dictionary<string, bool>();
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<string>();
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;
}
}
/// <summary>
/// 若当前命名空间无可用路径(xsss 尚未加载),自动切换至首个有路径的命名空间。
/// </summary>
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
}
}
+38
View File
@@ -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--;
}
+25 -24
View File
@@ -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
};
/// <summary>默认实例(五态默认匹配 ColorTint 颜色块)</summary>
public static StyleSheetState Default => new StyleSheetState();
/// <summary>
/// 创建纹理五态样式块,StylePath 默认匹配 SpriteState 块。
/// </summary>
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
+41 -36
View File
@@ -1,48 +1,53 @@
using System;
using UnityEngine;
namespace XericUI.XTable.Core
{
/// <summary>
/// 单元格数据模型——仅保存数据内容,不保存自身尺寸。
/// 尺寸由渲染阶段的行列标题(行高/列宽)决定。
/// 样式通过 StyleNamespace 字符串引用 StyleManager 中的命名空间。
/// </summary>
[Serializable]
public class XTableCellData
{
/// <summary>文本内容</summary>
public string Text;
/// <summary>
/// 单元格数据模型——仅保存数据内容,不保存自身尺寸。
/// 尺寸由渲染阶段的行列标题(行高/列宽)决定。
/// 样式通过 StyleNamespace 字符串引用 StyleManager 中的命名空间。
/// </summary>
[Serializable]
public class XTableCellData
{
/// <summary>文本内容</summary>
public string Text;
/// <summary>图片纹理</summary>
public Texture2D Image;
/// <summary>图片纹理</summary>
public Texture2D Image;
/// <summary>预制体对象(用于嵌入复杂 UI 元素)</summary>
public GameObject Prefab;
/// <summary>预制体对象(用于嵌入复杂 UI 元素)</summary>
public GameObject Prefab;
/// <summary>
/// 样式命名空间——对应 StyleManager 中的命名空间。
/// 为空或 "xeric_table_default" 时使用默认表格样式。
/// 渲染时从此命名空间读取 /cell/bg/color、/cell/font/size 等路径。
/// </summary>
public string StyleNamespace;
/// <summary>
/// 样式命名空间——对应 StyleManager 中的命名空间。
/// 为空或 "xeric_table_default" 时使用默认表格样式。
/// 渲染时从此命名空间读取 /cell/bg/color、/cell/font/size 等路径。
/// </summary>
public string StyleNamespace;
public XTableCellData() { }
/// <summary>预制体实例从对象池取出时回调——外部可在此初始化实例内容</summary>
[NonSerialized] public System.Action<GameObject> OnPrefabAcquire;
public XTableCellData(string text)
{
Text = text;
}
/// <summary>预制体实例归还对象池前回调——外部可在此清理引用</summary>
[NonSerialized] public System.Action<GameObject> 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;
}
}
}
@@ -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;
/// <summary>当前渲染器中所有预制体实例的缓存</summary>
private Dictionary<(int, int), GameObject> m_PrefabInstances;
/// <summary>共享预制体对象池引用(由 XericUIActionTable 传入)</summary>
public PrefabPrototypePool PrefabPool;
/// <summary>共享活跃预制体映射表(由 XericUIActionTable 传入)</summary>
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<RectTransform>();
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<RectTransform>();
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<RectTransform>();
if (prt == null) prt = instance.AddComponent<RectTransform>();
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);
}
/// <summary>销毁所有缓存预制体实例</summary>
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)
@@ -70,8 +70,11 @@ namespace XericUI.XTable.Rendering.Component
[System.NonSerialized] private GameObject m_BackgroundGo;
/// <summary>预制体实例缓存,key 为 (row, col),用于复用已实例化的预制体</summary>
[System.NonSerialized] private Dictionary<(int, int), GameObject> m_PrefabInstances;
/// <summary>预制体对象池——按原型分组管理</summary>
[System.NonSerialized] private PrefabPrototypePool m_PrefabPool;
/// <summary>活跃预制体实例映射 (row, col) → instance,用于回调编排</summary>
[System.NonSerialized] private Dictionary<(int, int), GameObject> m_ActivePrefabMap;
/// <summary>表格四叉树——按 StyleNamespace 分区,渲染阶段查询可见象限</summary>
[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<RectTransform>();
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<RectTransform>();
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);
/// <summary>为指定单元格设置预制体</summary>
public XTableCellData SetCellPrefab(int row, int col, GameObject prefab)
{
var data = TableData.GetOrCreateCell(row, col);
data.Prefab = prefab;
MarkDirty(TableDirtyType.DataChanged);
return data;
}
/// <summary>清除指定单元格的预制体</summary>
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);
}
/// <summary>设置单元格的样式命名空间</summary>
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<RectTransform>();
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<RectTransform>();
prt.anchoredPosition = new Vector2(cellX, cellY);
prt.sizeDelta = new Vector2(cellW, cellH);
m_ActivePrefabMap[(r, c)] = instance;
data.OnPrefabAcquire?.Invoke(instance);
}
}
}
}
#endregion
#region 预制体管理
/// <summary>获取或创建指定单元格的预制体实例</summary>
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;
}
/// <summary>定位预制体实例到指定单元格区域(锚点左上角,枢轴左上角)</summary>
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<RectTransform>();
if (prt == null) prt = instance.AddComponent<RectTransform>();
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);
}
/// <summary>销毁所有已缓存的预制体实例</summary>
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;
}
/// <summary>渲染 Viewport 层冻结单元格,随滚动偏移调整位置</summary>
@@ -0,0 +1,186 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
namespace XericUI.XTable.Rendering.Elements
{
/// <summary>
/// 预制体对象池——按原型分组管理,每个预制体对应一个独立的对象池。
/// 使用 Stack + HashSet 模式与 <see cref="CellAssembler"/> 保持一致。
/// </summary>
public class PrefabPrototypePool
{
#region 内部类型
private class PrefabPool
{
public GameObject Prototype;
/// <summary>休眠实例栈(池满时先归还的优先复用)</summary>
public Stack<GameObject> Available = new Stack<GameObject>();
/// <summary>活跃实例集合(O(1) 查找 + 防止重复归还)</summary>
public HashSet<GameObject> Active = new HashSet<GameObject>();
}
#endregion
#region 字段
private Dictionary<GameObject, PrefabPool> m_Pools = new Dictionary<GameObject, PrefabPool>();
private Transform m_Parent;
private Transform m_PoolRoot;
/// <summary>池根节点(所有回收对象均挂在此节点下,节点本身处于 inactive)</summary>
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;
}
/// <summary>彻底销毁所有池对象(活跃 + 休眠 + 池根)</summary>
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 池操作
/// <summary>从池中获取实例(池空则 Instantiate 新的)</summary>
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<RectTransform>();
if (rt == null) rt = instance.AddComponent<RectTransform>();
rt.anchorMin = AnchorTopLeft;
rt.anchorMax = AnchorTopLeft;
rt.pivot = PivotTopLeft;
pool.Active.Add(instance);
return instance;
}
/// <summary>将指定实例归还对象池(Deactivate + 移入 PoolRoot)</summary>
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);
}
/// <summary>将所有活跃实例回收至池</summary>
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();
}
}
/// <summary>获取当前所有活跃实例(用于外部遍历回调)</summary>
public IReadOnlyCollection<GameObject> GetActive(GameObject prefab)
{
if (prefab == null || !m_Pools.TryGetValue(prefab, out var pool))
return System.Array.Empty<GameObject>();
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
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 8ee1f2c601403504faea388e1f91f07b
guid: 0bbffde7baff2d445b8b002b56b5c84d
MonoImporter:
externalObjects: {}
serializedVersion: 2