From 5c51d35d8e33879e925d958c37d8a9376d10cd7c Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Wed, 29 Jul 2026 10:10:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A1=A8=E6=A0=BC=E5=86=BB?= =?UTF-8?q?=E7=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Rendering/Component/TableRenderer.cs | 550 ++++++------------ .../Component/XericUIActionTable.Border.cs | 84 ++- .../Component/XericUIActionTable.Data.cs | 15 +- .../Component/XericUIActionTable.Freeze.cs | 237 +++++--- .../XericUIActionTable.RendererFilter.cs | 13 +- .../Rendering/Component/XericUIActionTable.cs | 18 +- .../Rendering/Elements/CellAssembler.cs | 14 +- .../Rendering/Elements/PrefabPrototypePool.cs | 21 +- .../Rendering/Elements/SlotCounterPool.cs | 52 +- 9 files changed, 453 insertions(+), 551 deletions(-) diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs b/Runtime/XTable/Rendering/Component/TableRenderer.cs index 63c5314..7a91345 100644 --- a/Runtime/XTable/Rendering/Component/TableRenderer.cs +++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs @@ -4,449 +4,253 @@ 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 + /// 指定父级和索引范围的表格区域渲染器。池由表格全局持有,区域仅持有可见对象追踪。 + public sealed class TableRenderer { - #region 内部字段 + private struct Tracked + { + public CellAssembler.PooledText Text; + public CellAssembler.PooledImage Image; + public CellAssembler.PooledImage Selection; + } - private Transform m_Parent; - private CellAssembler m_Assembler; - private GameObject m_BackgroundGo; + private readonly Transform m_Parent; + private readonly Dictionary<(int, int), Tracked> m_Tracked = new(); + private readonly HashSet<(int, int)> m_Rendered = new(); + private readonly Dictionary m_SpriteCache = new(); + private GameObject m_BackgroundGo; - /// 共享预制体对象池引用(由 XericUIActionTable 传入) - public PrefabPrototypePool PrefabPool; + public CellAssembler Assembler; + public PrefabPrototypePool PrefabPool; + public Dictionary<(int, int), GameObject> ActivePrefabMap; + public float CellPadding; + public XTableConfig Config; - /// 共享活跃预制体映射表(由 XericUIActionTable 传入) - public Dictionary<(int, int), GameObject> ActivePrefabMap; - - /// Texture2D → Sprite 缓存,避免每帧分配 - private Dictionary m_SpriteCache; - - /// 单元格内容水平内边距(单侧),由 XericUIActionTable 设置 - public float CellPadding; - private float CellPadding2 => CellPadding * 2f; - - /// 表格全局配置资源,由 XericUIActionTable 设置 - public XTableConfig Config; - - #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; + public Transform Parent => m_Parent; + public GameObject BackgroundGo => m_BackgroundGo; - #endregion - - #region 构造 / 销毁 - - public TableRenderer(Transform parent, string defaultStyleNamespace = null, - System.Action styleChangeHandler = null) + public TableRenderer(Transform parent) { m_Parent = parent; - m_Assembler = new CellAssembler(parent, defaultStyleNamespace); - if (styleChangeHandler != null) - m_Assembler.OnStyleChangeDetected += styleChangeHandler; } - /// 彻底销毁渲染器中所有动态生成的对象 public void DestroyAll() { - m_Assembler?.DestroyAll(); - - if (m_BackgroundGo != null) - { - if (Application.isPlaying) - Object.Destroy(m_BackgroundGo); - else - Object.DestroyImmediate(m_BackgroundGo); - m_BackgroundGo = null; - } + ReleaseAll(); + if (m_BackgroundGo == null) return; + 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) + public void CreateBackground(float width, float height, string styleNamespace, Vector2 position = default) { - if (m_BackgroundGo != null) + if (m_BackgroundGo == null) { - var rt = m_BackgroundGo.GetComponent(); - rt.sizeDelta = new Vector2(totalWidth, totalHeight); + 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(); + var image = m_BackgroundGo.GetComponent(); + image.raycastTarget = false; + image.color = GetStyleColor?.Invoke(styleNamespace, XericUIActionTable.STYLE_CELL_BG_COLOR, + XericUIActionTable.BG_COLOR_FALLBACK) ?? XericUIActionTable.BG_COLOR_FALLBACK; + } + var rt = m_BackgroundGo.GetComponent(); + rt.anchorMin = new Vector2(0, 1); + rt.anchorMax = new Vector2(0, 1); + rt.pivot = new Vector2(0, 1); + rt.anchoredPosition = position; + rt.sizeDelta = new Vector2(width, height); + } + + public void Render(int sr, int er, int sc, int ec, bool editorMode, bool fullRefresh) + { + var rows = GetRowHeights?.Invoke(); + var cols = GetColWidths?.Invoke(); + if (Assembler == null || rows == null || cols == null) return; + sr = Mathf.Max(sr, StartRow); + sc = Mathf.Max(sc, StartCol); + er = Mathf.Min(er, EndRow > 0 ? EndRow : rows.Length); + ec = Mathf.Min(ec, EndCol > 0 ? EndCol : cols.Length); + if (sr >= er || sc >= ec) + { + if (fullRefresh) ReleaseAll(); 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(); + var target = new HashSet<(int, int)>(); + for (int r = sr; r < er; r++) + for (int c = sc; c < ec; c++) + target.Add((r, c)); - 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); - } + if (fullRefresh) + ReleaseAll(); else + ReleaseExcept(target); + + foreach (var cell in target) + if (fullRefresh || !m_Rendered.Contains(cell)) + RenderCell(cell.Item1, cell.Item2, rows, cols, editorMode); + + m_Rendered.Clear(); + foreach (var cell in target) m_Rendered.Add(cell); + } + + public void ReleaseAll() + { + foreach (var pair in m_Tracked) + ReleaseTracked(pair.Value); + m_Tracked.Clear(); + m_Rendered.Clear(); + if (ActivePrefabMap == null) return; + var keys = new List<(int, int)>(); + foreach (var pair in ActivePrefabMap) keys.Add(pair.Key); + foreach (var key in keys) ActivePrefabMap.Remove(key); + } + + private void ReleaseExcept(HashSet<(int, int)> target) + { + var remove = new List<(int, int)>(); + foreach (var pair in m_Tracked) + if (!target.Contains(pair.Key)) remove.Add(pair.Key); + foreach (var key in remove) { - RenderCellsRuntime(sr, er, sc, ec, rowHeights, colWidths, tableData, defaultNs); + ReleaseTracked(m_Tracked[key]); + m_Tracked.Remove(key); } } - private void RenderCellsEditor(int sr, int er, int sc, int ec, - float[] rowHeights, float[] colWidths, XTableData tableData, string defaultNs) + private void ReleaseTracked(Tracked tracked) { - Color selectionColor = GetStyleColor(defaultNs, XericUIActionTable.STYLE_CELL_SELECTION_BG_COLOR, - XericUIActionTable.SELECTION_COLOR_FALLBACK); + Assembler.ReleaseText(tracked.Text); + Assembler.ReleaseImage(tracked.Image); + Assembler.ReleaseImage(tracked.Selection); + } - Color multiSelectColor = new Color(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * Config.MultiSelectAlphaFactor); - Color focusColor = new Color(selectionColor.r + Config.FocusRAdd, selectionColor.g + Config.FocusGAdd, selectionColor.b + Config.FocusBAdd, selectionColor.a * Config.FocusAlphaMul); - - int selRow = GetSelectedRow(); - int selCol = GetSelectedCol(); - - for (int r = sr; r < er && r < rowHeights.Length; r++) + private void RenderCell(int row, int col, float[] rows, float[] cols, bool editorMode) + { + var data = GetTableData?.Invoke(); + if (data != null && data.IsCellMerged(row, col)) return; + string defaultNs = GetDefaultStyleNamespace?.Invoke() ?? "xeric_table_default"; + string ns = data?.GetCell(row, col)?.GetEffectiveStyleNamespace(defaultNs) ?? defaultNs; + float x = CalcColLeftX(col, cols); + float y = -CalcRowTopY(row, rows); + float width = cols[col]; + float height = rows[row]; + if (data != null && data.IsMergeSource(row, col, out int mergeRows, out int mergeCols)) { - for (int c = sc; c < ec && c < colWidths.Length; c++) + for (int i = 1; i < mergeCols && col + i < cols.Length; i++) width += cols[col + i]; + for (int i = 1; i < mergeRows && row + i < rows.Length; i++) height += rows[row + i]; + } + + int cellId = row * cols.Length + col; + var tracked = new Tracked(); + bool selected = IsInSelectionRange?.Invoke(row, col) == true || + (row == GetSelectedRow?.Invoke() && col == GetSelectedCol?.Invoke()); + if (selected) + { + var selection = Assembler.GetImage(cellId, "cell_selection", ns, m_Parent); + if (selection != null) { - if (tableData != null && tableData.IsCellMerged(r, c)) continue; + selection.Rect.anchoredPosition = new Vector2(x, y); + selection.Rect.sizeDelta = new Vector2(width, height); + selection.Image.color = editorMode + ? GetStyleColor(ns, XericUIActionTable.STYLE_CELL_SELECTION_BG_COLOR, XericUIActionTable.SELECTION_COLOR_FALLBACK) + : Config.RuntimeSelectionColor; + tracked.Selection = selection; + } + } - 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)) + var cell = data?.GetCell(row, col); + if (cell != null) + { + if (cell.ContentType == CellContentType.Text && !string.IsNullOrEmpty(cell.Text)) + { + var text = Assembler.GetText(cellId, "cell_text", ns, m_Parent); + if (text != null) { - 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 ? data.GetEffectiveStyleNamespace(defaultNs) : defaultNs; - int cellId = r * colWidths.Length + c; - - // 选择高亮 - if (IsInSelectionRange != null && IsInSelectionRange(r, c)) - { - var selImg = m_Assembler.GetImage(cellId, "cell_multiselect"); - if (selImg != null) + text.Rect.anchoredPosition = new Vector2(x + CellPadding, y); + text.Rect.sizeDelta = new Vector2(width - CellPadding * 2f, height); + text.Text.SetText(cell.Text); + if (editorMode) { - selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); - selImg.Rect.sizeDelta = new Vector2(cellW, cellH); - selImg.Image.color = (r == selRow && c == selCol) ? focusColor : multiSelectColor; + text.Text.color = GetStyleColor(ns, XericUIActionTable.STYLE_CELL_FG_COLOR, XericUIActionTable.FG_COLOR_FALLBACK); + text.Text.fontSize = GetStyleInt(ns, XericUIActionTable.STYLE_CELL_FONT_SIZE, XericUIActionTable.FONT_SIZE_FALLBACK); + text.Text.font = GetStyleFontAsset?.Invoke(ns, XericUIActionTable.STYLE_CELL_FONT_ASSET) ?? text.Text.font; } + tracked.Text = text; } - else if (r == selRow && c == selCol) + } + else if (cell.ContentType == CellContentType.Image && cell.Image != null) + { + var image = Assembler.GetImage(cellId, "cell_image", ns, m_Parent); + if (image != null) { - var selImg = m_Assembler.GetImage(cellId, "cell_select"); - if (selImg != null) - { - selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); - selImg.Rect.sizeDelta = new Vector2(cellW, cellH); - selImg.Image.color = selectionColor; - } + image.Rect.anchoredPosition = new Vector2(x + CellPadding, y); + image.Rect.sizeDelta = new Vector2(width - CellPadding * 2f, height); + image.Image.sprite = GetSprite(cell.Image); + tracked.Image = image; } - - if (data == null) continue; - - switch (data.ContentType) + } + else if (cell.ContentType == CellContentType.Prefab && cell.Prefab != null && PrefabPool != null) + { + var prefab = PrefabPool.Acquire(cell.Prefab, cellId, m_Parent); + if (prefab != null) { - case CellContentType.Text: - if (!string.IsNullOrEmpty(data.Text)) - { - var txt = m_Assembler.GetText(cellId, "cell_text", cellNS); - if (txt != null) - { - txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); - txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); - txt.Text.SetText(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; - } - } - break; - case CellContentType.Image: - if (data.Image != null) - { - Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage(cellId, "cell_image", cellNS); - if (img != null) - { - img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); - img.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); - img.Image.sprite = sprite; - } - } - break; - case CellContentType.Prefab: - if (data.Prefab != null && PrefabPool != null) - { - GameObject instance = PrefabPool.Acquire(data.Prefab, cellId); - if (instance != null) - { - 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); - } - } - break; + var rt = prefab.GetComponent(); + rt.anchoredPosition = new Vector2(x, y); + rt.sizeDelta = new Vector2(width, height); + if (ActivePrefabMap != null) + ActivePrefabMap[(row, col)] = prefab; + cell.OnPrefabAcquire?.Invoke(prefab); } } } + m_Tracked[(row, col)] = tracked; } - private void RenderCellsRuntime(int sr, int er, int sc, int ec, - float[] rowHeights, float[] colWidths, XTableData tableData, string defaultNs) + private Sprite GetSprite(Texture2D texture) { - Color rtSelectionColor = Config.RuntimeSelectionColor; - Color rtMultiSelectColor = Config.RuntimeMultiSelectColor; - Color rtFocusColor = Config.RuntimeFocusColor; - - int selRow = GetSelectedRow(); - int selCol = GetSelectedCol(); - - for (int r = sr; r < er && r < rowHeights.Length; r++) + if (!m_SpriteCache.TryGetValue(texture, out var sprite)) { - 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 ? data.GetEffectiveStyleNamespace(defaultNs) : defaultNs; - int cellId = r * colWidths.Length + c; - - if (IsInSelectionRange != null && IsInSelectionRange(r, c)) - { - var selImg = m_Assembler.GetImage(cellId, "cell_multiselect"); - if (selImg != null) - { - 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(cellId, "cell_select"); - if (selImg != null) - { - selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); - selImg.Rect.sizeDelta = new Vector2(cellW, cellH); - selImg.Image.color = rtSelectionColor; - } - } - - if (data == null) continue; - - switch (data.ContentType) - { - case CellContentType.Text: - if (!string.IsNullOrEmpty(data.Text)) - { - var txt = m_Assembler.GetText(cellId, "cell_text", cellNS); - if (txt != null) - { - txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); - txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); - txt.Text.SetText(data.Text); - } - } - break; - case CellContentType.Image: - if (data.Image != null) - { - Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage(cellId, "cell_image", cellNS); - if (img != null) - { - img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); - img.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); - img.Image.sprite = sprite; - } - } - break; - case CellContentType.Prefab: - if (data.Prefab != null && PrefabPool != null) - { - GameObject instance = PrefabPool.Acquire(data.Prefab, cellId); - if (instance != null) - { - 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); - } - } - break; - } - } - } - } - - #endregion - - #region 辅助方法 - - /// 将 Texture2D 转为 Sprite(带缓存),用于 Image 类型单元格渲染 - private Sprite GetOrCreateSprite(Texture2D texture) - { - if (texture == null) return null; - - if (m_SpriteCache == null) - m_SpriteCache = new Dictionary(); - - if (!m_SpriteCache.TryGetValue(texture, out Sprite sprite)) - { - sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f)); - m_SpriteCache[texture] = sprite; + sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(.5f, .5f)); + m_SpriteCache.Add(texture, sprite); } return sprite; } - #endregion - - #region 坐标计算 - - private float CalcRowTopY(int row, float[] rowHeights) + private static float CalcRowTopY(int row, float[] values) { - float y = 0; - for (int i = 0; i < row && i < rowHeights.Length; i++) - y += rowHeights[i]; - return y; + float result = 0; + for (int i = 0; i < row; i++) result += values[i]; + return result; } - private float CalcColLeftX(int col, float[] colWidths) + private static float CalcColLeftX(int col, float[] values) { - float x = 0; - for (int i = 0; i < col && i < colWidths.Length; i++) - x += colWidths[i]; - return x; + float result = 0; + for (int i = 0; i < col; i++) result += values[i]; + return result; } - - #endregion } } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs index cc5a0d4..fdbca16 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs @@ -77,16 +77,27 @@ namespace XericUI.XTable.Rendering.Component int rows = m_RowHeights.Length; int cols = m_ColWidths.Length; - for (int boundary = 0; boundary <= rows; boundary++) - SubmitHorizontalBoundary(renderer, boundary, cols); - for (int boundary = 0; boundary <= cols; boundary++) - SubmitVerticalBoundary(renderer, boundary, rows); + int freezeRows = Mathf.Clamp(m_FreezeRowCount, 0, rows); + int freezeCols = Mathf.Clamp(m_FreezeColCount, 0, cols); + SubmitBorders(renderer, freezeRows, rows, freezeCols, cols); + RebuildFrozenBorders(freezeRows, freezeCols, rows, cols); } - private void SubmitHorizontalBoundary(UILineRendererV2 renderer, int boundary, int cols) + private void SubmitBorders(UILineRendererV2 renderer, int rowStart, int rowEnd, int colStart, int colEnd) { - int start = 0; - while (start < cols) + if (renderer == null || rowStart >= rowEnd || colStart >= colEnd) return; + renderer.ClearAll(); + renderer.rectTransform.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + for (int boundary = rowStart; boundary <= rowEnd; boundary++) + SubmitHorizontalBoundary(renderer, boundary, colStart, colEnd); + for (int boundary = colStart; boundary <= colEnd; boundary++) + SubmitVerticalBoundary(renderer, boundary, rowStart, rowEnd); + } + + private void SubmitHorizontalBoundary(UILineRendererV2 renderer, int boundary, int colStart, int colEnd) + { + int start = colStart; + while (start < colEnd) { if (IsMergedInteriorBoundary(XTableBorderAxis.Horizontal, boundary, start)) { @@ -95,7 +106,7 @@ namespace XericUI.XTable.Rendering.Component } string ns = ResolveBorderNamespace(XTableBorderAxis.Horizontal, boundary, start); int end = start + 1; - while (end < cols && !IsMergedInteriorBoundary(XTableBorderAxis.Horizontal, boundary, end) + while (end < colEnd && !IsMergedInteriorBoundary(XTableBorderAxis.Horizontal, boundary, end) && ResolveBorderNamespace(XTableBorderAxis.Horizontal, boundary, end) == ns) end++; @@ -106,10 +117,10 @@ namespace XericUI.XTable.Rendering.Component } } - private void SubmitVerticalBoundary(UILineRendererV2 renderer, int boundary, int rows) + private void SubmitVerticalBoundary(UILineRendererV2 renderer, int boundary, int rowStart, int rowEnd) { - int start = 0; - while (start < rows) + int start = rowStart; + while (start < rowEnd) { if (IsMergedInteriorBoundary(XTableBorderAxis.Vertical, boundary, start)) { @@ -118,7 +129,7 @@ namespace XericUI.XTable.Rendering.Component } string ns = ResolveBorderNamespace(XTableBorderAxis.Vertical, boundary, start); int end = start + 1; - while (end < rows && !IsMergedInteriorBoundary(XTableBorderAxis.Vertical, boundary, end) + while (end < rowEnd && !IsMergedInteriorBoundary(XTableBorderAxis.Vertical, boundary, end) && ResolveBorderNamespace(XTableBorderAxis.Vertical, boundary, end) == ns) end++; @@ -129,6 +140,55 @@ namespace XericUI.XTable.Rendering.Component } } + private void RebuildFrozenBorders(int freezeRows, int freezeCols, int rows, int cols) + { + if (freezeRows == 0 && freezeCols == 0) + { + DestroyFrozenBorderRenderers(); + return; + } + if (freezeCols > 0 && freezeRows < rows) + SubmitBorders(GetOrCreateFrozenBorderRenderer(m_FrozenColumnsContainer, ref m_FrozenColumnBorderRenderer, "_FrozenColumnBorders"), + freezeRows, rows, 0, freezeCols); + if (freezeRows > 0 && freezeCols < cols) + SubmitBorders(GetOrCreateFrozenBorderRenderer(m_FrozenRowsContainer, ref m_FrozenRowBorderRenderer, "_FrozenRowBorders"), + 0, freezeRows, freezeCols, cols); + if (freezeRows > 0 && freezeCols > 0) + SubmitBorders(GetOrCreateFrozenBorderRenderer(m_FrozenCornerContainer, ref m_FrozenCornerBorderRenderer, "_FrozenCornerBorders"), + 0, freezeRows, 0, freezeCols); + } + + private static UILineRendererV2 GetOrCreateFrozenBorderRenderer(Transform parent, + ref UILineRendererV2 renderer, string name) + { + if (renderer != null) return renderer; + if (parent == null) return null; + var go = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(UILineRendererV2)); + go.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; + go.transform.SetParent(parent, false); + var rt = go.GetComponent(); + rt.anchorMin = new Vector2(0, 1); + rt.anchorMax = new Vector2(0, 1); + rt.pivot = new Vector2(0, 1); + renderer = go.GetComponent(); + return renderer; + } + + private void DestroyFrozenBorderRenderers() + { + DestroyFrozenBorderRenderer(ref m_FrozenColumnBorderRenderer); + DestroyFrozenBorderRenderer(ref m_FrozenRowBorderRenderer); + DestroyFrozenBorderRenderer(ref m_FrozenCornerBorderRenderer); + } + + private static void DestroyFrozenBorderRenderer(ref UILineRendererV2 renderer) + { + if (renderer == null) return; + if (Application.isPlaying) Destroy(renderer.gameObject); + else DestroyImmediate(renderer.gameObject); + renderer = null; + } + private string ResolveBorderNamespace(XTableBorderAxis axis, int fixedIndex, int index) { string ns = m_TableData?.GetBorderNamespace(axis, fixedIndex, index); diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs index 2928a45..28b13af 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs @@ -28,6 +28,16 @@ namespace XericUI.XTable.Rendering.Component #endregion + #region 渲染分区 + + private bool IsNormalRegionCell(int row, int col) + { + return row >= Mathf.Clamp(m_FreezeRowCount, 0, m_RowHeights?.Length ?? 0) + && col >= Mathf.Clamp(m_FreezeColCount, 0, m_ColWidths?.Length ?? 0); + } + + #endregion + #region 坐标公式 private float GetRowTopY(int row) @@ -104,10 +114,7 @@ namespace XericUI.XTable.Rendering.Component // Content 的 anchor/pivot 在 Awake 中强制设为 (0,1),anchoredPosition // 直接表示 Content 左上角相对于 Viewport 左上角的偏移。 Vector2 ap = ct.anchoredPosition; - float visibleLeft = ap.x; - float visibleTop = ap.y; - - m_VisibleRect = new Rect(visibleLeft, visibleTop, vt.rect.width, vt.rect.height); + m_VisibleRect = new Rect(ap.x, ap.y, vt.rect.width, vt.rect.height); } private void CalcVisibleRange(out int sr, out int er, out int sc, out int ec) diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs index dbfe628..df362ec 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs @@ -1,117 +1,164 @@ -using System; using UnityEngine; -using UnityEngine.UI; -using XericUI.XTable.Core; using XericUI.XTable.Rendering.Elements; -using XericLibrary.Runtime.SuperStyleSheet; namespace XericUI.XTable.Rendering.Component { public partial class XericUIActionTable { - #region Viewport 冻结渲染 + private static readonly Vector2 FrozenAnchor = new(0f, 1f); - /// 创建或重新创建 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; - - // 共享预制体对象池 - m_ViewportRenderer.PrefabPool = m_PrefabPool; - m_ViewportRenderer.ActivePrefabMap = m_ActivePrefabMap; - m_ViewportRenderer.CellPadding = Config.CellHorizontalPadding; - m_ViewportRenderer.Config = Config; + CreateFrozenContainers(); + CreateFrozenRenderers(); } - /// 渲染 Viewport 层冻结单元格,随滚动偏移调整位置 - private void RenderFrozenCells() + private void CreateFrozenContainers() { - if (m_FreezeRowCount <= 0 && m_FreezeColCount <= 0) + Transform viewport = m_ScrollRect.viewport; + m_FrozenColumnsContainer = CreateFrozenContainer(viewport, "_FrozenColumns"); + m_FrozenRowsContainer = CreateFrozenContainer(viewport, "_FrozenRows"); + m_FrozenCornerContainer = CreateFrozenContainer(viewport, "_FrozenCorner"); + } + + private static RectTransform CreateFrozenContainer(Transform parent, string name) + { + var existing = parent.Find(name) as RectTransform; + if (existing != null) return existing; + var go = new GameObject(name, typeof(RectTransform)); + go.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; + go.transform.SetParent(parent, false); + var rt = go.GetComponent(); + rt.anchorMin = FrozenAnchor; + rt.anchorMax = FrozenAnchor; + rt.pivot = FrozenAnchor; + return rt; + } + + private void CreateFrozenRenderers() + { + if (m_FrozenColumnsContainer == null) return; + m_FrozenColumnRenderer?.DestroyAll(); + m_FrozenRowRenderer?.DestroyAll(); + m_FrozenCornerRenderer?.DestroyAll(); + DestroyFrozenBorderRenderers(); + m_FrozenColumnRenderer = CreateFrozenRenderer(m_FrozenColumnsContainer); + m_FrozenRowRenderer = CreateFrozenRenderer(m_FrozenRowsContainer); + m_FrozenCornerRenderer = CreateFrozenRenderer(m_FrozenCornerContainer); + } + + private TableRenderer CreateFrozenRenderer(Transform parent) + { + var renderer = new TableRenderer(parent) { - // 无冻结需求时清理渲染器 - if (m_ViewportRenderer != null) - { - m_ViewportRenderer.DestroyAll(); - m_ViewportRenderer = null; - } + Assembler = m_Assembler, + PrefabPool = m_PrefabPool, + CellPadding = CellPadding, + Config = Config, + GetStyleColor = (ns, path, fallback) => GetStyleColor(ns, path, fallback), + GetStyleInt = (ns, path, fallback) => GetStyleInt(ns, path, fallback), + GetStyleFontAsset = (ns, path) => GetStyleFontAsset(ns, path), + GetRowHeights = () => m_RowHeights, + GetColWidths = () => m_ColWidths, + GetTableData = () => m_TableData, + GetDefaultStyleNamespace = () => m_DefaultStyleNamespace, + IsInSelectionRange = (r, c) => IsInSelectionRange(r, c), + GetSelectedRow = () => SelectionHandle.StartRow, + GetSelectedCol = () => SelectionHandle.StartCol, + }; + return renderer; + } + + private void DestroyFrozenRenderers() + { + m_FrozenColumnRenderer?.DestroyAll(); + m_FrozenRowRenderer?.DestroyAll(); + m_FrozenCornerRenderer?.DestroyAll(); + DestroyFrozenBorderRenderers(); + m_FrozenColumnRenderer = null; + m_FrozenRowRenderer = null; + m_FrozenCornerRenderer = null; + DestroyFrozenContainer(ref m_FrozenColumnsContainer); + DestroyFrozenContainer(ref m_FrozenRowsContainer); + DestroyFrozenContainer(ref m_FrozenCornerContainer); + } + + private static void DestroyFrozenContainer(ref RectTransform container) + { + if (container == null) return; + if (Application.isPlaying) Object.Destroy(container.gameObject); + else Object.DestroyImmediate(container.gameObject); + container = null; + } + + private void ReleaseFrozenCells() + { + m_FrozenColumnRenderer?.ReleaseAll(); + m_FrozenRowRenderer?.ReleaseAll(); + m_FrozenCornerRenderer?.ReleaseAll(); + } + + private void RenderFrozenCells(bool fullRefresh = true) + { + int rows = m_RowHeights?.Length ?? 0; + int cols = m_ColWidths?.Length ?? 0; + int freezeRows = Mathf.Clamp(m_FreezeRowCount, 0, rows); + int freezeCols = Mathf.Clamp(m_FreezeColCount, 0, cols); + if (freezeRows == 0 && freezeCols == 0) + { + DestroyFrozenRenderers(); 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); + if (m_FrozenColumnRenderer == null) CreateViewportRenderer(); + if (m_FrozenColumnRenderer == null) return; + CreateFrozenBackgrounds(freezeRows, freezeCols); + SyncFrozenContainers(); + CalcVisibleRange(out int visibleStartRow, out int visibleEndRow, out int visibleStartCol, out int visibleEndCol); +#if UNITY_EDITOR + bool editorMode = !Application.isPlaying; +#else + bool editorMode = false; +#endif + // 固定顺序:Content 已先完成,然后冻结列、冻结行、左上交叉区。 + if (freezeCols > 0) + m_FrozenColumnRenderer.Render(Mathf.Max(freezeRows, visibleStartRow), visibleEndRow, + 0, freezeCols, editorMode, fullRefresh); + if (freezeRows > 0) + m_FrozenRowRenderer.Render(0, freezeRows, Mathf.Max(freezeCols, visibleStartCol), visibleEndCol, + editorMode, fullRefresh); + if (freezeRows > 0 && freezeCols > 0) + m_FrozenCornerRenderer.Render(0, freezeRows, 0, freezeCols, editorMode, fullRefresh); } - #endregion + private void CreateFrozenBackgrounds(int freezeRows, int freezeCols) + { + float freezeWidth = GetColLeftX(freezeCols); + float freezeHeight = GetRowTopY(freezeRows); + if (freezeCols > 0) + m_FrozenColumnRenderer.CreateBackground(freezeWidth, m_TotalHeight, m_DefaultStyleNamespace); + if (freezeRows > 0) + m_FrozenRowRenderer.CreateBackground(m_TotalWidth - freezeWidth, freezeHeight, + m_DefaultStyleNamespace, new Vector2(freezeWidth, 0f)); + if (freezeRows > 0 && freezeCols > 0) + m_FrozenCornerRenderer.CreateBackground(freezeWidth, freezeHeight, m_DefaultStyleNamespace); + } + + private void SyncFrozenContainers() + { + if (m_ScrollRect == null || m_ScrollRect.content == null) return; + Vector2 contentPosition = m_ScrollRect.content.anchoredPosition; + SyncFrozenContainer(m_FrozenColumnsContainer, new Vector2(0f, contentPosition.y)); + SyncFrozenContainer(m_FrozenRowsContainer, new Vector2(contentPosition.x, 0f)); + SyncFrozenContainer(m_FrozenCornerContainer, Vector2.zero); + } + + private void SyncFrozenContainer(RectTransform container, Vector2 position) + { + if (container == null) return; + container.anchoredPosition = position; + container.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + } } } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs index 5a80526..b4d643d 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs @@ -78,6 +78,7 @@ namespace XericUI.XTable.Rendering.Component foreach (var cell in q.Cells) { if (!TableData.IsCellMerged(cell.Item1, cell.Item2) + && IsNormalRegionCell(cell.Item1, cell.Item2) && IsCellVisible(cell.Item1, cell.Item2)) result.Add(cell); } @@ -89,7 +90,7 @@ namespace XericUI.XTable.Rendering.Component for (int r = sr; r < er; r++) for (int c = sc; c < ec; c++) { - if (!TableData.IsCellMerged(r, c) && IsCellVisible(r, c)) + if (!TableData.IsCellMerged(r, c) && IsNormalRegionCell(r, c) && IsCellVisible(r, c)) result.Add((r, c)); } } @@ -103,7 +104,7 @@ namespace XericUI.XTable.Rendering.Component for (int r = sr; r < er; r++) for (int c = sc; c < ec; c++) { - if (!TableData.IsCellMerged(r, c) && IsCellVisible(r, c)) + if (!TableData.IsCellMerged(r, c) && IsNormalRegionCell(r, c) && IsCellVisible(r, c)) result.Add((r, c)); } } @@ -312,6 +313,7 @@ namespace XericUI.XTable.Rendering.Component } m_PrefabPool?.ReturnAll(); ReleaseAllTrackedCells(); + ReleaseFrozenCells(); // 清理孤立子对象 CleanOrphanChildren(); @@ -327,9 +329,9 @@ namespace XericUI.XTable.Rendering.Component foreach (var (r, c) in m_LastRenderedCells) RenderCellContent(r, c, ns, editorMode); - // 边框 + 冻结 + // 主 Content 已先渲染,冻结区随后复用其释放的对象;边框按区域最后提交。 + RenderFrozenCells(true); RebuildBorders(); - RenderFrozenCells(); m_DirtyFlag.Clear(); OnTableRefreshed?.Invoke(); @@ -410,6 +412,9 @@ namespace XericUI.XTable.Rendering.Component RenderCellContent(cell.Item1, cell.Item2, ns, editorMode); } + // 冻结列、冻结行与左上角在 Content 后更新,并复用同一全局对象池。 + RenderFrozenCells(false); + // 5. 交换:将 newVisible 作为下一帧的比较基准 (m_LastRenderedCells, newVisible) = (newVisible, m_LastRenderedCells); diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs index d88fd5d..5ba26d2 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -60,8 +60,13 @@ namespace XericUI.XTable.Rendering.Component [System.NonSerialized] private CellAssembler m_Assembler; [System.NonSerialized] private XTableDirtyFlag m_DirtyFlag; - /// Viewport 层渲染器——用于绘制冻结行列单元格 - [System.NonSerialized] private TableRenderer m_ViewportRenderer; + /// Viewport 下的冻结列、冻结行与左上交叉区渲染器。 + [System.NonSerialized] private TableRenderer m_FrozenColumnRenderer; + [System.NonSerialized] private TableRenderer m_FrozenRowRenderer; + [System.NonSerialized] private TableRenderer m_FrozenCornerRenderer; + [System.NonSerialized] private RectTransform m_FrozenColumnsContainer; + [System.NonSerialized] private RectTransform m_FrozenRowsContainer; + [System.NonSerialized] private RectTransform m_FrozenCornerContainer; [System.NonSerialized] private XTableSelectionHandle m_SelectionHandle; @@ -82,8 +87,11 @@ namespace XericUI.XTable.Rendering.Component [System.NonSerialized] private readonly HashSet m_ActiveStyleNamespaces = new(); [System.NonSerialized] private bool m_StyleNamespaceDirty = true; - /// 边框线段渲染器——使用 UILineRendererV2 的坐标线段绘制 + /// 各渲染区域的边框线段渲染器。 [System.NonSerialized] private UILineRendererV2 m_BorderRenderer; + [System.NonSerialized] private UILineRendererV2 m_FrozenColumnBorderRenderer; + [System.NonSerialized] private UILineRendererV2 m_FrozenRowBorderRenderer; + [System.NonSerialized] private UILineRendererV2 m_FrozenCornerBorderRenderer; private float m_TotalWidth; private float m_TotalHeight; @@ -309,9 +317,7 @@ namespace XericUI.XTable.Rendering.Component DestroyBorderRenderer(); DestroyBackgroundRenderer(); - // 销毁 Viewport 冻结渲染器 - m_ViewportRenderer?.DestroyAll(); - m_ViewportRenderer = null; + DestroyFrozenRenderers(); base.OnDestroy(); } diff --git a/Runtime/XTable/Rendering/Elements/CellAssembler.cs b/Runtime/XTable/Rendering/Elements/CellAssembler.cs index bfa90db..179fdc6 100644 --- a/Runtime/XTable/Rendering/Elements/CellAssembler.cs +++ b/Runtime/XTable/Rendering/Elements/CellAssembler.cs @@ -96,11 +96,14 @@ namespace XericUI.XTable.Rendering.Elements #region 池操作 /// 为指定 cellId 获取一个池化 Image - public PooledImage GetImage(int cellId, string name = "cell_img", string styleNamespace = null) + public PooledImage GetImage(int cellId, string name = "cell_img", string styleNamespace = null, + Transform parent = null) { var item = m_ImagePool.Acquire(cellId); if (item == null) return null; + if (parent != null && item.Root.transform.parent != parent) + item.Root.transform.SetParent(parent, false); item.CellId = cellId; item.Root.name = name; ResetImageDefaults(item); @@ -109,11 +112,14 @@ namespace XericUI.XTable.Rendering.Elements } /// 为指定 cellId 获取一个池化 TMP_Text - public PooledText GetText(int cellId, string name = "cell_text", string styleNamespace = null) + public PooledText GetText(int cellId, string name = "cell_text", string styleNamespace = null, + Transform parent = null) { var item = m_TextPool.Acquire(cellId); if (item == null) return null; + if (parent != null && item.Root.transform.parent != parent) + item.Root.transform.SetParent(parent, false); item.CellId = cellId; item.Root.name = name; ResetTextDefaults(item); @@ -155,7 +161,7 @@ namespace XericUI.XTable.Rendering.Elements { if (item == null) return; item.Style?.Unbind(); - m_ImagePool.Release(item.CellId); + m_ImagePool.ReleaseByObject(item); } /// 归还单个 Text 池对象(通过 CellId 定位槽位并移入 FrozenSlots) @@ -163,7 +169,7 @@ namespace XericUI.XTable.Rendering.Elements { if (item == null) return; item.Style?.Unbind(); - m_TextPool.Release(item.CellId); + m_TextPool.ReleaseByObject(item); } #endregion diff --git a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs index 436aba7..95d1f7a 100644 --- a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs +++ b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs @@ -75,15 +75,20 @@ namespace XericUI.XTable.Rendering.Elements /// 为指定 cellId 获取预制体实例。 /// 若该 cellId 已有绑定的实例,直接返回;否则创建新实例(需未达上限)。 /// - public GameObject Acquire(GameObject prefab, int cellId) + public GameObject Acquire(GameObject prefab, int cellId, Transform parent = null) { if (prefab == null) return null; var pool = GetOrCreatePool(prefab); + Transform targetParent = parent ?? m_Parent; - // 已绑定 → 直接复用 + // 已绑定 → 直接复用并迁移到当前渲染区域。 if (pool.Slots.TryGetValue(cellId, out var existing)) + { + if (targetParent != null && existing.transform.parent != targetParent) + existing.transform.SetParent(targetParent, false); return existing; + } // 创建新实例 if (m_TotalCreated >= MAX_PREFAB_ITEMS) @@ -92,7 +97,7 @@ namespace XericUI.XTable.Rendering.Elements return null; } - var instance = Object.Instantiate(prefab, m_Parent); + var instance = Object.Instantiate(prefab, targetParent); instance.hideFlags = POOL_FLAGS; instance.name = $"_prefab_{cellId}"; m_TotalCreated++; @@ -142,17 +147,9 @@ namespace XericUI.XTable.Rendering.Elements KillObject(instance); } - /// 将所有活跃槽位清空并销毁对象 + /// 结束当前渲染周期。实例保留在全局槽位中,以便跨区域复用。 public void ReturnAll() { - foreach (var kv in m_Pools) - { - var pool = kv.Value; - foreach (var instance in pool.Slots.Values) - KillObject(instance); - pool.Slots.Clear(); - } - m_TotalCreated = 0; } /// 获取当前所有活跃实例(用于外部遍历回调) diff --git a/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs index 0d86346..efe7857 100644 --- a/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs +++ b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs @@ -7,8 +7,8 @@ namespace XericUI.XTable.Rendering.Elements { /// /// 基于计数器的槽位对象池(泛型)。 - /// 每个 cellId 映射到固定索引的 SlotCounter,槽位在计数器间通过 FrozenSlots 流转复用。 - /// 避免 Queue 模式下对象与位置解耦导致的堆叠问题,同时不触发 SetActive/SetParent 等生命周期事件。 + /// 每个 cellId 映射到固定索引的 SlotCounter,释放槽位保留在原计数器内复用。 + /// 避免跨计数器拆分碰撞组导致的对象位置冲突。 /// /// 池对象类型 public class SlotCounterPool where T : class @@ -37,9 +37,6 @@ namespace XericUI.XTable.Rendering.Elements private SlotCounter[] m_Counters = new SlotCounter[POOL_SIZE]; - /// 冻结槽位列表——全局可偷取的休眠槽位(末尾偷取,避免 Canvas 重建) - private List m_FrozenSlots = new List(); - /// 已创建对象总数(不含预制体 Instantiate) private int m_CreatedCount; @@ -64,7 +61,7 @@ namespace XericUI.XTable.Rendering.Elements /// /// 为指定 cellId 获取一个池对象。 - /// 优先复用本计数器内的空闲槽位,其次从 FrozenSlots 偷取,最后创建新对象。 + /// 优先复用本计数器内的空闲槽位,最后创建新对象。 /// public T Acquire(int cellId) { @@ -88,18 +85,7 @@ namespace XericUI.XTable.Rendering.Elements } } - // 2. 从 FrozenSlots 末尾偷取 - if (m_FrozenSlots.Count > 0) - { - int lastIdx = m_FrozenSlots.Count - 1; - var stolen = m_FrozenSlots[lastIdx]; - m_FrozenSlots.RemoveAt(lastIdx); - stolen.CellId = cellId; - counter.Slots.Add(stolen); - return stolen.PooledObject; - } - - // 3. 创建新对象 + // 2. 创建新对象。槽位不能跨计数器流转,避免破坏碰撞单元格的稳定关系。 if (m_CreatedCount < POOL_SIZE) { var obj = m_Factory(); @@ -115,7 +101,7 @@ namespace XericUI.XTable.Rendering.Elements } /// - /// 释放指定 cellId 占用的槽位,将其移入 FrozenSlots 供其他计数器复用。 + /// 释放指定 cellId 占用的槽位,并保留在原计数器中供相同碰撞组复用。 /// public void Release(int cellId) { @@ -129,10 +115,7 @@ namespace XericUI.XTable.Rendering.Elements { if (counter.Slots[i].CellId == cellId) { - var slot = counter.Slots[i]; - counter.Slots.RemoveAt(i); - slot.CellId = -1; - m_FrozenSlots.Add(slot); + counter.Slots[i].CellId = -1; return; } } @@ -156,10 +139,7 @@ namespace XericUI.XTable.Rendering.Elements { if (ReferenceEquals(counter.Slots[i].PooledObject, pooledObject)) { - var slot = counter.Slots[i]; - counter.Slots.RemoveAt(i); - slot.CellId = -1; - m_FrozenSlots.Add(slot); + counter.Slots[i].CellId = -1; return true; } } @@ -167,26 +147,20 @@ namespace XericUI.XTable.Rendering.Elements return false; } - /// 将所有活跃槽位移入 FrozenSlots + /// 释放所有活跃槽位,但保留其所属计数器。 public void ReturnAll() { for (int ci = 0; ci < POOL_SIZE; ci++) { var counter = m_Counters[ci]; if (counter == null) continue; - - for (int i = counter.Slots.Count - 1; i >= 0; i--) - { - var slot = counter.Slots[i]; - counter.Slots.RemoveAt(i); - slot.CellId = -1; - m_FrozenSlots.Add(slot); - } + for (int i = 0; i < counter.Slots.Count; i++) + counter.Slots[i].CellId = -1; } } /// - /// 彻底销毁所有池对象(包括计数器内活跃槽位和 FrozenSlots 中的冻结槽位)。 + /// 彻底销毁所有池对象。 /// public void DestroyAll(Action destroyFunc) { @@ -202,10 +176,6 @@ namespace XericUI.XTable.Rendering.Elements counter.Slots.Clear(); } - foreach (var slot in m_FrozenSlots) - destroyFunc(slot.PooledObject); - m_FrozenSlots.Clear(); - m_CreatedCount = 0; }