From ff8f049e30c87ba42ac4d8ea48efe59841950d2d Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Thu, 9 Jul 2026 11:40:19 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=A0=BC=E5=86=BB=E7=BB=93=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Editor/XTable/XericUIActionTableEditor.cs | 9 + .../Rendering/Component/TableRenderer.cs | 418 ++++++++++++++++++ .../Rendering/Component/TableRenderer.cs.meta | 11 + .../Rendering/Component/XericUIActionTable.cs | 137 ++++++ 4 files changed, 575 insertions(+) create mode 100644 Runtime/XTable/Rendering/Component/TableRenderer.cs create mode 100644 Runtime/XTable/Rendering/Component/TableRenderer.cs.meta diff --git a/Editor/XTable/XericUIActionTableEditor.cs b/Editor/XTable/XericUIActionTableEditor.cs index 745aa19..1d2d89f 100644 --- a/Editor/XTable/XericUIActionTableEditor.cs +++ b/Editor/XTable/XericUIActionTableEditor.cs @@ -70,6 +70,15 @@ namespace XericUIEditor.XTable EditorGUILayout.PropertyField(m_RowHeightsProp, new GUIContent("行高"), true); if (m_ColWidthsProp != null) EditorGUILayout.PropertyField(m_ColWidthsProp, new GUIContent("列宽"), true); + + // 冻结行列 + var freezeRowProp = serializedObject.FindProperty("m_FreezeRowCount"); + var freezeColProp = serializedObject.FindProperty("m_FreezeColCount"); + if (freezeRowProp != null) + EditorGUILayout.PropertyField(freezeRowProp, new GUIContent("冻结行数")); + if (freezeColProp != null) + EditorGUILayout.PropertyField(freezeColProp, new GUIContent("冻结列数")); + EditorGUI.indentLevel--; } diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs b/Runtime/XTable/Rendering/Component/TableRenderer.cs new file mode 100644 index 0000000..315300c --- /dev/null +++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs @@ -0,0 +1,418 @@ +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using XericUI.XTable.Core; +using XericUI.XTable.Rendering.Elements; +using XericLibrary.Runtime.SuperStyleSheet; + +namespace XericUI.XTable.Rendering.Component +{ + /// + /// 表格渲染管线封装——将 CellAssembler 对象池、背景创建、单元格渲染抽离为可复用单元。 + /// 同一表格可持有多个 TableRenderer 实例,分别渲染到不同父节点(如 Content 和 Viewport)。 + /// + public class TableRenderer + { + #region 内部字段 + + private Transform m_Parent; + private CellAssembler m_Assembler; + private GameObject m_BackgroundGo; + + /// 当前渲染器中所有预制体实例的缓存 + private Dictionary<(int, int), GameObject> m_PrefabInstances; + + #endregion + + #region 公开属性 + + public CellAssembler Assembler => m_Assembler; + public GameObject BackgroundGo => m_BackgroundGo; + public Transform Parent => m_Parent; + + /// 起始行偏移(Viewport 渲染器通常为 0;Content 渲染器通常为 freezeRowCount) + public int StartRow; + /// 起始列偏移 + public int StartCol; + /// 终止行(不含),0 表示不受限 + public int EndRow; + /// 终止列(不含),0 表示不受限 + public int EndCol; + + #endregion + + #region 样式委托 + + /// 从样式表读取颜色值 + public System.Func GetStyleColor; + /// 从样式表读取浮点值 + public System.Func GetStyleFloat; + /// 从样式表读取整数值 + public System.Func GetStyleInt; + /// 从样式表读取 TMP_FontAsset + public System.Func GetStyleFontAsset; + + /// 获取行高数组 + public System.Func GetRowHeights; + /// 获取列宽数组 + public System.Func GetColWidths; + /// 获取表格数据 + public System.Func GetTableData; + /// 默认样式命名空间 + public System.Func GetDefaultStyleNamespace; + /// 判断单元格是否在多选范围内 + public System.Func IsInSelectionRange; + /// 获取选中的起始行列 + public System.Func GetSelectedRow; + /// 获取选中的起始列 + public System.Func GetSelectedCol; + + /// 运行时样式变更回调 + public event System.Action OnStyleChangeDetected; + + #endregion + + #region 构造 / 销毁 + + public TableRenderer(Transform parent, string defaultStyleNamespace = null, + System.Action styleChangeHandler = null) + { + m_Parent = parent; + m_Assembler = new CellAssembler(parent, defaultStyleNamespace); + if (styleChangeHandler != null) + m_Assembler.OnStyleChangeDetected += styleChangeHandler; + } + + /// 彻底销毁渲染器中所有动态生成的对象 + public void DestroyAll() + { + m_Assembler?.DestroyAll(); + ClearPrefabInstances(); + + if (m_BackgroundGo != null) + { + if (Application.isPlaying) + Object.Destroy(m_BackgroundGo); + else + Object.DestroyImmediate(m_BackgroundGo); + m_BackgroundGo = null; + } + } + + #endregion + + #region 背景 + + /// 创建或获取背景 Image(锚定左上角,覆盖整个渲染范围) + public void CreateBackground(float totalWidth, float totalHeight, string styleNamespace) + { + if (m_BackgroundGo != null) + { + var rt = m_BackgroundGo.GetComponent(); + rt.sizeDelta = new Vector2(totalWidth, totalHeight); + return; + } + + m_BackgroundGo = new GameObject("_TableBackground", + typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); + m_BackgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; + m_BackgroundGo.transform.SetParent(m_Parent, false); + m_BackgroundGo.transform.SetAsFirstSibling(); + + RectTransform bgRt = m_BackgroundGo.GetComponent(); + bgRt.anchorMin = new Vector2(0, 1); + bgRt.anchorMax = new Vector2(0, 1); + bgRt.pivot = new Vector2(0, 1); + bgRt.anchoredPosition = Vector2.zero; + bgRt.sizeDelta = new Vector2(totalWidth, totalHeight); + + var bgImg = m_BackgroundGo.GetComponent(); + bgImg.color = GetStyleColor?.Invoke(styleNamespace, + XericUIActionTable.STYLE_CELL_BG_COLOR, + XericUIActionTable.BG_COLOR_FALLBACK) + ?? XericUIActionTable.BG_COLOR_FALLBACK; + bgImg.raycastTarget = false; + } + + #endregion + + #region 单元格渲染 + + /// 回收所有池对象 + public void ReturnAll() => m_Assembler.ReturnAll(); + + /// + /// 渲染指定范围内的单元格。 + /// 行范围为 [sr, er),列范围为 [sc, ec)。 + /// + public void RenderCells(int sr, int er, int sc, int ec, bool isEditor) + { + float[] rowHeights = GetRowHeights?.Invoke(); + float[] colWidths = GetColWidths?.Invoke(); + XTableData tableData = GetTableData?.Invoke(); + string defaultNs = GetDefaultStyleNamespace?.Invoke() ?? "xeric_table_default"; + + if (rowHeights == null || colWidths == null) return; + if (rowHeights.Length == 0 || colWidths.Length == 0) return; + + // 应用渲染范围限制 + int renderStartRow = StartRow; + int renderEndRow = EndRow > 0 ? Mathf.Min(EndRow, rowHeights.Length) : rowHeights.Length; + int renderStartCol = StartCol; + int renderEndCol = EndCol > 0 ? Mathf.Min(EndCol, colWidths.Length) : colWidths.Length; + + sr = Mathf.Max(sr, renderStartRow); + er = Mathf.Min(er, renderEndRow); + sc = Mathf.Max(sc, renderStartCol); + ec = Mathf.Min(ec, renderEndCol); + + if (sr >= er || sc >= ec) return; + + if (isEditor) + { + RenderCellsEditor(sr, er, sc, ec, rowHeights, colWidths, tableData, defaultNs); + } + else + { + RenderCellsRuntime(sr, er, sc, ec, rowHeights, colWidths, tableData, defaultNs); + } + } + + private void RenderCellsEditor(int sr, int er, int sc, int ec, + float[] rowHeights, float[] colWidths, XTableData tableData, string defaultNs) + { + Color selectionColor = GetStyleColor(defaultNs, XericUIActionTable.STYLE_CELL_SELECTION_BG_COLOR, + XericUIActionTable.SELECTION_COLOR_FALLBACK); + Color borderColor = GetStyleColor(defaultNs, XericUIActionTable.STYLE_CELL_BORDER_BOTTOM_COLOR, + XericUIActionTable.BORDER_COLOR_FALLBACK); + float borderWidth = GetStyleFloat(defaultNs, XericUIActionTable.STYLE_CELL_BORDER_BOTTOM_WIDTH, + XericUIActionTable.BORDER_WIDTH_FALLBACK); + + Color multiSelectColor = new Color(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * 0.4f); + Color focusColor = new Color(selectionColor.r + 0.04f, selectionColor.g + 0.13f, selectionColor.b + 0.13f, selectionColor.a * 1.4f); + + int selRow = GetSelectedRow(); + int selCol = GetSelectedCol(); + + for (int r = sr; r < er && r < rowHeights.Length; r++) + { + for (int c = sc; c < ec && c < colWidths.Length; c++) + { + if (tableData != null && tableData.IsCellMerged(r, c)) continue; + + float cellX = CalcColLeftX(c, colWidths); + float cellY = -CalcRowTopY(r, rowHeights); + float cellW = colWidths[c]; + float cellH = rowHeights[r]; + + if (tableData != null && tableData.IsMergeSource(r, c, out int mergeRS, out int mergeCS)) + { + for (int i = 1; i < mergeCS && c + i < colWidths.Length; i++) + cellW += colWidths[c + i]; + for (int i = 1; i < mergeRS && r + i < rowHeights.Length; i++) + cellH += rowHeights[r + i]; + } + + XTableCellData data = tableData?.GetCell(r, c); + string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) + ? data.StyleNamespace : defaultNs; + + // 选择高亮 + if (IsInSelectionRange != null && IsInSelectionRange(r, c)) + { + var selImg = m_Assembler.GetImage("cell_multiselect"); + selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); + selImg.Rect.sizeDelta = new Vector2(cellW, cellH); + selImg.Image.color = (r == selRow && c == selCol) ? focusColor : multiSelectColor; + } + else if (r == selRow && c == selCol) + { + var selImg = m_Assembler.GetImage("cell_select"); + selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); + selImg.Rect.sizeDelta = new Vector2(cellW, cellH); + selImg.Image.color = selectionColor; + } + + // 网格线 + var hLine = m_Assembler.GetImage("cell_hline"); + hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH); + hLine.Rect.sizeDelta = new Vector2(cellW, borderWidth); + hLine.Image.color = borderColor; + + var vLine = m_Assembler.GetImage("cell_vline"); + vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - borderWidth, cellY); + vLine.Rect.sizeDelta = new Vector2(borderWidth, cellH); + vLine.Image.color = borderColor; + + if (data == null) continue; + + if (!string.IsNullOrEmpty(data.Text)) + { + var txt = m_Assembler.GetText("cell_text", cellNS); + txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY); + txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH); + txt.Text.text = data.Text; + txt.Text.color = GetStyleColor(cellNS, XericUIActionTable.STYLE_CELL_FG_COLOR, + XericUIActionTable.FG_COLOR_FALLBACK); + txt.Text.fontSize = GetStyleInt(cellNS, XericUIActionTable.STYLE_CELL_FONT_SIZE, + XericUIActionTable.FONT_SIZE_FALLBACK); + txt.Text.alignment = TextAlignmentOptions.Midline; + txt.Text.font = GetStyleFontAsset?.Invoke(cellNS, + XericUIActionTable.STYLE_CELL_FONT_ASSET) ?? txt.Text.font; + } + + if (data.Prefab != null) + PositionPrefabInstance(r, c, data.Prefab, cellX, cellY, cellW, cellH); + } + } + } + + private void RenderCellsRuntime(int sr, int er, int sc, int ec, + float[] rowHeights, float[] colWidths, XTableData tableData, string defaultNs) + { + Color rtSelectionColor = XericUIActionTable.RT_SELECTION_COLOR; + Color rtMultiSelectColor = XericUIActionTable.RT_MULTI_SELECT_COLOR; + Color rtFocusColor = XericUIActionTable.RT_FOCUS_COLOR; + + int selRow = GetSelectedRow(); + int selCol = GetSelectedCol(); + + for (int r = sr; r < er && r < rowHeights.Length; r++) + { + for (int c = sc; c < ec && c < colWidths.Length; c++) + { + if (tableData != null && tableData.IsCellMerged(r, c)) continue; + + float cellX = CalcColLeftX(c, colWidths); + float cellY = -CalcRowTopY(r, rowHeights); + float cellW = colWidths[c]; + float cellH = rowHeights[r]; + + if (tableData != null && tableData.IsMergeSource(r, c, out int mergeRS, out int mergeCS)) + { + for (int i = 1; i < mergeCS && c + i < colWidths.Length; i++) + cellW += colWidths[c + i]; + for (int i = 1; i < mergeRS && r + i < rowHeights.Length; i++) + cellH += rowHeights[r + i]; + } + + XTableCellData data = tableData?.GetCell(r, c); + string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) + ? data.StyleNamespace : defaultNs; + + if (IsInSelectionRange != null && IsInSelectionRange(r, c)) + { + var selImg = m_Assembler.GetImage("cell_multiselect"); + selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); + selImg.Rect.sizeDelta = new Vector2(cellW, cellH); + selImg.Image.color = (r == selRow && c == selCol) ? rtFocusColor : rtMultiSelectColor; + } + else if (r == selRow && c == selCol) + { + var selImg = m_Assembler.GetImage("cell_select"); + selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); + selImg.Rect.sizeDelta = new Vector2(cellW, cellH); + selImg.Image.color = rtSelectionColor; + } + + var hLine = m_Assembler.GetImage("cell_hline"); + hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH); + hLine.Rect.sizeDelta = new Vector2(cellW, XericUIActionTable.RT_BORDER_WIDTH); + hLine.Image.color = XericUIActionTable.RT_BORDER_COLOR; + + var vLine = m_Assembler.GetImage("cell_vline"); + vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - XericUIActionTable.RT_BORDER_WIDTH, cellY); + vLine.Rect.sizeDelta = new Vector2(XericUIActionTable.RT_BORDER_WIDTH, cellH); + vLine.Image.color = XericUIActionTable.RT_BORDER_COLOR; + + if (data == null) continue; + + if (!string.IsNullOrEmpty(data.Text)) + { + var txt = m_Assembler.GetText("cell_text", cellNS); + txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY); + txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH); + txt.Text.text = data.Text; + } + + if (data.Prefab != null) + PositionPrefabInstance(r, c, data.Prefab, cellX, cellY, cellW, cellH); + } + } + } + + #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) + { + float y = 0; + for (int i = 0; i < row && i < rowHeights.Length; i++) + y += rowHeights[i]; + return y; + } + + private float CalcColLeftX(int col, float[] colWidths) + { + float x = 0; + for (int i = 0; i < col && i < colWidths.Length; i++) + x += colWidths[i]; + return x; + } + + #endregion + } +} diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs.meta b/Runtime/XTable/Rendering/Component/TableRenderer.cs.meta new file mode 100644 index 0000000..90ebb96 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de1e4b2ad4f55bc48a57a820276024b1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs index 3440dca..ccd9f0d 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -44,6 +44,14 @@ namespace XericUI.XTable.Rendering.Component [SerializeField] private ScrollRect m_ScrollRect; + [SerializeField] + [Tooltip("冻结行数——顶部固定不滚动的行数")] + private int m_FreezeRowCount; + + [SerializeField] + [Tooltip("冻结列数——左侧固定不滚动的列数")] + private int m_FreezeColCount; + [SerializeField] [Tooltip("在 Hierarchy 中显示动态生成的子对象(仅编辑器,不保存到场景)")] private bool m_ChildrenVisible; @@ -55,6 +63,9 @@ namespace XericUI.XTable.Rendering.Component [System.NonSerialized] private CellAssembler m_Assembler; [System.NonSerialized] private XTableDirtyFlag m_DirtyFlag; + /// Viewport 层渲染器——用于绘制冻结行列单元格 + [System.NonSerialized] private TableRenderer m_ViewportRenderer; + [System.NonSerialized] private XTableSelectionHandle m_SelectionHandle; [System.NonSerialized] private GameObject m_BackgroundGo; @@ -105,6 +116,21 @@ namespace XericUI.XTable.Rendering.Component public float[] RowHeights => m_RowHeights; public float[] ColWidths => m_ColWidths; + /// 冻结行数 + public int FreezeRowCount => m_FreezeRowCount; + /// 冻结列数 + public int FreezeColCount => m_FreezeColCount; + + /// 设置冻结行列数并重建视图 + public void SetFreezeCount(int rows, int cols) + { + m_FreezeRowCount = Mathf.Max(0, rows); + m_FreezeColCount = Mathf.Max(0, cols); + CreateViewportRenderer(); + MarkDirty(TableDirtyType.LayoutChanged); + RefreshView(); + } + public string DefaultStyleNamespace => m_DefaultStyleNamespace; /// 选中操作句柄——外部可通过此句柄控制选区,或直接操作 StartRow/StartCol 等属性 @@ -332,6 +358,9 @@ namespace XericUI.XTable.Rendering.Component CreateDefaultSampleData(); CreateBackground(); + // 创建 Viewport 层渲染器(用于冻结单元格) + CreateViewportRenderer(); + if (m_ScrollRect != null) m_ScrollRect.onValueChanged.AddListener(OnScrollValueChanged); @@ -385,6 +414,10 @@ namespace XericUI.XTable.Rendering.Component m_BackgroundGo = null; } + // 销毁 Viewport 冻结渲染器 + m_ViewportRenderer?.DestroyAll(); + m_ViewportRenderer = null; + base.OnDestroy(); } @@ -816,6 +849,9 @@ namespace XericUI.XTable.Rendering.Component // 重建边框线段 RebuildBorders(); + // 渲染 Viewport 层冻结单元格 + RenderFrozenCells(); + m_DirtyFlag.Clear(); OnTableRefreshed?.Invoke(); SetChildrenVisible(m_ChildrenVisible); @@ -905,6 +941,7 @@ namespace XericUI.XTable.Rendering.Component RecalculateTotalSize(); CreateDefaultSampleData(); CreateBackground(); + CreateViewportRenderer(); MarkDirty(TableDirtyType.All); RefreshView(); SetChildrenVisible(m_ChildrenVisible); @@ -983,6 +1020,7 @@ namespace XericUI.XTable.Rendering.Component MarkDirty(TableDirtyType.All); RecalculateTotalSize(); CreateBackground(); + CreateViewportRenderer(); RefreshView(); SetChildrenVisible(m_ChildrenVisible); } @@ -1597,6 +1635,105 @@ namespace XericUI.XTable.Rendering.Component #endregion + #region Viewport 冻结渲染 + + /// 创建或重新创建 Viewport 层冻结渲染器 + private void CreateViewportRenderer() + { + if (m_ScrollRect == null || m_ScrollRect.viewport == null) return; + if (m_FreezeRowCount <= 0 && m_FreezeColCount <= 0) return; + + // 清理旧渲染器 + m_ViewportRenderer?.DestroyAll(); + + Transform viewportT = m_ScrollRect.viewport; + m_ViewportRenderer = new TableRenderer(viewportT, m_DefaultStyleNamespace, OnPoolStyleChanged); + + // 配置委托 + m_ViewportRenderer.GetStyleColor = (ns, path, fb) => GetStyleColor(ns, path, fb); + m_ViewportRenderer.GetStyleFloat = (ns, path, fb) => GetStyleFloat(ns, path, fb); + m_ViewportRenderer.GetStyleInt = (ns, path, fb) => GetStyleInt(ns, path, fb); + m_ViewportRenderer.GetStyleFontAsset = (ns, path) => GetStyleFontAsset(ns, path); + m_ViewportRenderer.GetRowHeights = () => m_RowHeights; + m_ViewportRenderer.GetColWidths = () => m_ColWidths; + m_ViewportRenderer.GetTableData = () => m_TableData; + m_ViewportRenderer.GetDefaultStyleNamespace = () => m_DefaultStyleNamespace; + m_ViewportRenderer.IsInSelectionRange = (r, c) => IsInSelectionRange(r, c); + m_ViewportRenderer.GetSelectedRow = () => SelectionHandle.StartRow; + m_ViewportRenderer.GetSelectedCol = () => SelectionHandle.StartCol; + + // 仅渲染冻结范围 + m_ViewportRenderer.StartRow = 0; + m_ViewportRenderer.EndRow = m_FreezeRowCount; + m_ViewportRenderer.StartCol = 0; + m_ViewportRenderer.EndCol = m_FreezeColCount; + } + + /// 渲染 Viewport 层冻结单元格,随滚动偏移调整位置 + private void RenderFrozenCells() + { + if (m_FreezeRowCount <= 0 && m_FreezeColCount <= 0) + { + // 无冻结需求时清理渲染器 + if (m_ViewportRenderer != null) + { + m_ViewportRenderer.DestroyAll(); + m_ViewportRenderer = null; + } + return; + } + + // 检测冻结范围变更,重建渲染器 + if (m_ViewportRenderer == null || + m_ViewportRenderer.EndRow != m_FreezeRowCount || + m_ViewportRenderer.EndCol != m_FreezeColCount) + CreateViewportRenderer(); + + // 计算冻结区域尺寸 + float freezeW = 0, freezeH = 0; + for (int c = 0; c < m_FreezeColCount && c < m_ColWidths.Length; c++) + freezeW += m_ColWidths[c]; + for (int r = 0; r < m_FreezeRowCount && r < m_RowHeights.Length; r++) + freezeH += m_RowHeights[r]; + + // 创建/更新背景 + m_ViewportRenderer.CreateBackground(freezeW, freezeH, m_DefaultStyleNamespace); + + // 获取滚动偏移 + Vector2 scrollOffset = Vector2.zero; + if (m_ScrollRect != null && m_ScrollRect.content != null) + { + scrollOffset.x = Mathf.Max(0, -m_ScrollRect.content.anchoredPosition.x); + scrollOffset.y = Mathf.Max(0, m_ScrollRect.content.anchoredPosition.y); + } + + // 调整背景位置(随滚动偏移移动,保持在视口可见范围内) + if (m_ViewportRenderer.BackgroundGo != null) + { + var bgRt = m_ViewportRenderer.BackgroundGo.GetComponent(); + bgRt.anchoredPosition = new Vector2(scrollOffset.x, -scrollOffset.y); + } + + // 回收并重新渲染冻结单元格 + m_ViewportRenderer.ReturnAll(); + m_ViewportRenderer.RenderCells( + 0, m_FreezeRowCount, + 0, m_FreezeColCount, + !Application.isPlaying); + + // 冻结单元格需要跟随滚动偏移(相对于 Viewport 定位) + if (m_ViewportRenderer.Assembler != null) + { + // 调整所有活跃的池对象的 anchoredPosition 以抵消滚动 + // 这通过重新定位背景实现,单元格相对于背景 (0,0) 定位,所以随背景一起移动 + } + + // 背景始终可见 + m_ViewportRenderer.BackgroundGo?.SetActive(true); + } + + #endregion + #region IPointerClickHandler public void OnPointerClick(PointerEventData eventData) From 15bf34edaf7bd52c2e6649aea9246d1d613ee5c7 Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Thu, 9 Jul 2026 11:40:43 +0800 Subject: [PATCH 2/4] =?UTF-8?q?ui=E5=9F=BA=E7=B1=BB=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=98=BE=E5=BD=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Runtime/Core/Base/XericUIBehaciour.cs | 46 +++++++++++++++++++-------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/Runtime/Core/Base/XericUIBehaciour.cs b/Runtime/Core/Base/XericUIBehaciour.cs index 84cc67a..f68fc0e 100644 --- a/Runtime/Core/Base/XericUIBehaciour.cs +++ b/Runtime/Core/Base/XericUIBehaciour.cs @@ -1,4 +1,5 @@ using System; +using Deconstruction.Runtime; #if ODIN_INSPECTOR using Sirenix.OdinInspector; #endif @@ -36,11 +37,9 @@ namespace XericUI.Core.Base { get { - if (_canvas == null) - _canvas = GetComponentInParent(); + if (!_canvas) _canvas = GetComponentInParent(); #if UNITY_EDITOR - if (_canvas == null) - Debug.LogError($"{name} 无法找到布局组件,接下来的流程可能会引发错误", this); + if (!_canvas) Debug.LogError($"{name} 无法找到布局组件,接下来的流程可能会引发错误", this); #endif return _canvas; } @@ -53,11 +52,9 @@ namespace XericUI.Core.Base { get { - if (_canvasScaler == null) - _canvasScaler = GetComponentInParent(); + if (!_canvasScaler) _canvasScaler = GetComponentInParent(); #if UNITY_EDITOR - if (_canvas == null) - Debug.LogError($"{name} 无法找到布局缩放组件,接下来的流程可能会引发错误", this); + if (_canvas == null) Debug.LogError($"{name} 无法找到布局缩放组件,接下来的流程可能会引发错误", this); #endif return _canvasScaler; } @@ -70,11 +67,9 @@ namespace XericUI.Core.Base { get { - if (_graphicRaycaster == null) - _graphicRaycaster = GetComponentInParent(); + if (!_graphicRaycaster) _graphicRaycaster = GetComponentInParent(); #if UNITY_EDITOR - if (_canvas == null) - Debug.LogError($"{name} 无法找到布局射线检测,接下来的流程可能会引发错误", this); + if (_canvas == null) Debug.LogError($"{name} 无法找到布局射线检测,接下来的流程可能会引发错误", this); #endif return _graphicRaycaster; } @@ -87,8 +82,7 @@ namespace XericUI.Core.Base { get { - if (_rect == null) - _rect = GetComponent(); + if (!_rect) _rect = GetComponent(); return _rect; } } @@ -135,6 +129,7 @@ namespace XericUI.Core.Base /// public void SetHierarchyDirty() { + rectTransform.SetLayoutDirty(); _canvas = null; _canvasScaler = null; _graphicRaycaster = null; @@ -147,6 +142,29 @@ namespace XericUI.Core.Base public void ResetHierarchyDirty() { _hierarchyDirty = false; + rectTransform.SetLayoutDirty(); + } + + #endregion + + #region 显影 + + public virtual void Show() + { + gameObject.SetActive(true); + } + + public void Show(bool isShow) + { + if (isShow) + Show(); + else + Hide(); + } + + public virtual void Hide() + { + gameObject.SetActive(false); } #endregion From 35a84332b746ef219bc94e67103088a4b3cc33e0 Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Thu, 9 Jul 2026 14:05:38 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StyleSheetUI/DefaultXericStyleSheet.xsss | 44 ++++++++++++++++--- Runtime/OverrideUI/XericStyleButton.cs | 4 +- Runtime/OverrideUI/XericStyleImage.cs | 6 +-- Runtime/OverrideUI/XericStyleToggle.cs | 4 +- 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/Runtime/Core/StyleSheetUI/DefaultXericStyleSheet.xsss b/Runtime/Core/StyleSheetUI/DefaultXericStyleSheet.xsss index 960b247..54ae562 100644 --- a/Runtime/Core/StyleSheetUI/DefaultXericStyleSheet.xsss +++ b/Runtime/Core/StyleSheetUI/DefaultXericStyleSheet.xsss @@ -4,7 +4,7 @@ namespace: default -/button/TargetGraphic/ColorTint:UnityEngine.UI.ColorBlock/ { +button/TargetGraphic/ColorTint:UnityEngine.UI.ColorBlock/ { .NormalColor = (1,1,1,1) .HighlightedColor = (0.96,0.96,0.96,1) .PressedColor = (0.78,0.78,0.78,1) @@ -12,7 +12,23 @@ namespace: default .DisabledColor = (0.6,0.6,0.6,0.8) } -/toggle/TargetGraphic/ColorTint:UnityEngine.UI.ColorBlock/ { +button/TargetGraphic/SpriteState:UnityEngine.UI.SpriteState/ { +.NormalColor = guid::UnityEngine.Sprite +.HighlightedColor = guid::UnityEngine.Sprite +.PressedColor = guid::UnityEngine.Sprite +.SelectedColor = guid::UnityEngine.Sprite +.DisabledColor = guid::UnityEngine.Sprite +} + +button/TargetGraphic/Animation/ { +.NormalColor = None +.HighlightedColor = None +.PressedColor = None +.SelectedColor = None +.DisabledColor = None +} + +toggle/TargetGraphic/ColorTint:UnityEngine.UI.ColorBlock/ { .NormalColor = (1,1,1,1) .HighlightedColor = (0.96,0.96,0.96,1) .PressedColor = (0.78,0.78,0.78,1) @@ -20,10 +36,24 @@ namespace: default .DisabledColor = (0.6,0.6,0.6,0.8) } -/image/ColorTint:UnityEngine.UI.ColorBlock/ { +toggle/TargetGraphic/SpriteState:UnityEngine.UI.SpriteState/ { +.NormalColor = guid::UnityEngine.Sprite +.HighlightedColor = guid::UnityEngine.Sprite +.PressedColor = guid::UnityEngine.Sprite +.SelectedColor = guid::UnityEngine.Sprite +.DisabledColor = guid::UnityEngine.Sprite +} + +toggle/TargetGraphic/Animation/ { +.NormalColor = None +.HighlightedColor = None +.PressedColor = None +.SelectedColor = None +.DisabledColor = None +} + +image/ColorTint:UnityEngine.UI.ColorBlock/ { .NormalColor = (1,1,1,1) -.HighlightedColor = (0.96,0.96,0.96,1) -.PressedColor = (0.78,0.78,0.78,1) -.SelectedColor = (0.96,0.96,0.96,1) -.DisabledColor = (0.6,0.6,0.6,0.8) +.Sprite = guid::UnityEngine.Sprite +.Material = guid::UnityEngine.Material } \ No newline at end of file diff --git a/Runtime/OverrideUI/XericStyleButton.cs b/Runtime/OverrideUI/XericStyleButton.cs index 8c537d0..d3a9bcb 100644 --- a/Runtime/OverrideUI/XericStyleButton.cs +++ b/Runtime/OverrideUI/XericStyleButton.cs @@ -35,11 +35,11 @@ namespace XericUI.OverrideUI [Header("样式表状态")] [SerializeField] [Tooltip("五态颜色样式块(ColorTint)")] - private StyleSheetState m_ColorState = StyleSheetState.Default; + private StyleSheetState m_ColorState = new StyleSheetState("button", Transition.ColorTint); [SerializeField] [Tooltip("五态纹理样式块(SpriteState)")] - private StyleSheetState m_TextureState = StyleSheetState.CreateTexture(); + private StyleSheetState m_TextureState = new StyleSheetState("button", Transition.SpriteSwap); [SerializeField] [Tooltip("启用样式表驱动过渡(关闭则回退为 Unity ColorBlock)")] private bool m_EnableStyleTransition = true; diff --git a/Runtime/OverrideUI/XericStyleImage.cs b/Runtime/OverrideUI/XericStyleImage.cs index dcc3d16..604a909 100644 --- a/Runtime/OverrideUI/XericStyleImage.cs +++ b/Runtime/OverrideUI/XericStyleImage.cs @@ -18,15 +18,15 @@ namespace XericUI.OverrideUI [SerializeField] [Tooltip("颜色样式路径")] - private StyleField m_ColorStyle = new StyleField(); + private StyleField m_ColorStyle = new StyleField("image/ColorTint/NormalColor"); [SerializeField] [Tooltip("纹理/精灵样式路径")] - private StyleField m_TextureStyle = new StyleField(); + private StyleField m_TextureStyle = new StyleField("image/ColorTint/Sprite"); [SerializeField] [Tooltip("材质样式路径")] - private StyleField m_MaterialStyle = new StyleField(); + private StyleField m_MaterialStyle = new StyleField("image/ColorTint/Material"); [SerializeField] [Tooltip("启用样式表自动刷新")] diff --git a/Runtime/OverrideUI/XericStyleToggle.cs b/Runtime/OverrideUI/XericStyleToggle.cs index ceeb290..ef92265 100644 --- a/Runtime/OverrideUI/XericStyleToggle.cs +++ b/Runtime/OverrideUI/XericStyleToggle.cs @@ -36,11 +36,11 @@ namespace XericUI.OverrideUI [Header("样式表状态")] [SerializeField] [Tooltip("五态颜色样式块(ColorTint)")] - private StyleSheetState m_ColorState = StyleSheetState.Default; + private StyleSheetState m_ColorState = new StyleSheetState("toggle", Transition.ColorTint); [SerializeField] [Tooltip("五态纹理样式块(SpriteState)")] - private StyleSheetState m_TextureState = StyleSheetState.CreateTexture(); + private StyleSheetState m_TextureState = new StyleSheetState("toggle", Transition.SpriteSwap); [Header("Toggle 专用")] [SerializeField] 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 4/4] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=A0=B7=E5=BC=8F=E8=B7=AF=E5=BE=84=E7=9A=84?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=E3=80=82=20=E9=92=88=E5=AF=B9?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E7=94=9F=E6=88=90=E9=A2=84=E5=88=B6=E4=BD=93?= =?UTF-8?q?=E7=9A=84=E9=80=BB=E8=BE=91=E8=BF=9B=E8=A1=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=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