From 353cedcf1cc7ca76b3c56d1f438810f23fa1cf87 Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Thu, 30 Jul 2026 08:25:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=B9=E8=B1=A1=E6=B1=A0?= =?UTF-8?q?=E9=87=8A=E6=94=BE=E9=80=BB=E8=BE=91=EF=BC=8C=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E5=99=A8=E4=BB=85=E4=BD=9C=E4=B8=BA=E5=AF=B9=E8=B1=A1=E6=B1=A0?= =?UTF-8?q?=E5=A4=8D=E7=94=A8=E5=B0=81=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Runtime/Core/Base/XericUIBehaviour.cs | 42 +++ .../Rendering/Component/TableRenderer.cs | 4 - .../Component/XericUIActionTable.Api.cs | 148 ++++++++- .../Component/XericUIActionTable.ExcelIO.cs | 4 +- .../XericUIActionTable.RendererFilter.cs | 56 +--- .../XericUIActionTable.UpdateScope.cs | 175 +++++++---- .../Rendering/Elements/CellAssembler.cs | 29 +- .../Rendering/Elements/PrefabPrototypePool.cs | 294 +++++++----------- .../Rendering/Elements/SlotCounterPool.cs | 11 +- 9 files changed, 459 insertions(+), 304 deletions(-) diff --git a/Runtime/Core/Base/XericUIBehaviour.cs b/Runtime/Core/Base/XericUIBehaviour.cs index 0b49b72..ae6eb28 100644 --- a/Runtime/Core/Base/XericUIBehaviour.cs +++ b/Runtime/Core/Base/XericUIBehaviour.cs @@ -268,7 +268,49 @@ namespace XericUI.Core.Base rt.anchoredPosition = anchorLocalPos + pivotOffset; } } + + /// + /// 根据目标单元格与父级画布的可用空间定位弹窗。 + /// 默认以左上角对齐单元格左下角;下方空间不足时改用底部锚点显示在单元格上方, + /// 右侧空间不足时改为从右侧向左展开。 + /// + public void AutoSnapAlignedToRect(RectTransform cellRect) + { + var rt = transform as RectTransform; + if (rt == null || cellRect == null) return; + var parentRT = rt.parent as RectTransform; + if (parentRT == null) return; + + Canvas.ForceUpdateCanvases(); + + var cellCorners = new Vector3[4]; + cellRect.GetWorldCorners(cellCorners); + for (int i = 0; i < cellCorners.Length; i++) + cellCorners[i] = parentRT.InverseTransformPoint(cellCorners[i]); + + var popupWidth = parentRT.InverseTransformVector( + rt.TransformVector(Vector3.right * rt.rect.width)).magnitude; + var popupHeight = parentRT.InverseTransformVector( + rt.TransformVector(Vector3.up * rt.rect.height)).magnitude; + var parentRect = parentRT.rect; + + var leftSpace = cellCorners[2].x - parentRect.xMin; + var rightSpace = parentRect.xMax - cellCorners[0].x; + var placeRight = rightSpace >= popupWidth || rightSpace >= leftSpace; + + var bottomSpace = cellCorners[0].y - parentRect.yMin; + var topSpace = parentRect.yMax - cellCorners[1].y; + var placeBelow = bottomSpace >= popupHeight || bottomSpace >= topSpace; + + rt.pivot = new Vector2(placeRight ? 0f : 1f, placeBelow ? 1f : 0f); + var target = new Vector3( + placeRight ? cellCorners[0].x : cellCorners[2].x, + placeBelow ? cellCorners[0].y : cellCorners[1].y, + 0f); + rt.position = parentRT.TransformPoint(target); + } + #endregion #region 显影 diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs b/Runtime/XTable/Rendering/Component/TableRenderer.cs index 42cbb7a..4cbaab2 100644 --- a/Runtime/XTable/Rendering/Component/TableRenderer.cs +++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs @@ -120,10 +120,6 @@ namespace XericUI.XTable.Rendering.Component 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) diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs index 7283c7d..a22242e 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs @@ -19,15 +19,28 @@ namespace XericUI.XTable.Rendering.Component { /// 设置冻结行列数并重建视图 public void SetFreezeCount(int rows, int cols) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.SetFreezeCount, rows, cols))) return; + SetFreezeCountImmediate(rows, cols); + } + + private void SetFreezeCountImmediate(int rows, int cols, bool refresh = true) { m_FreezeRowCount = Mathf.Max(0, rows); m_FreezeColCount = Mathf.Max(0, cols); CreateViewportRenderer(); MarkDirty(TableDirtyType.LayoutChanged); - RefreshView(); + if (refresh && !IsCheckingUpdate) + RefreshView(); } public void SetCellData(int row, int col, XTableCellData data) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.SetCellData, row, col, cellData: data))) return; + SetCellDataImmediate(row, col, data); + } + + private void SetCellDataImmediate(int row, int col, XTableCellData data) { TableData.SetCell(row, col, data); MarkDirty(TableDirtyType.DataChanged); @@ -35,6 +48,12 @@ namespace XericUI.XTable.Rendering.Component /// 设置文本内容(ContentType 自动设为 Text) public XTableCellData SetCellText(int row, int col, string text) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.SetCellText, row, col, stringValue1: text))) return null; + return SetCellTextImmediate(row, col, text); + } + + private XTableCellData SetCellTextImmediate(int row, int col, string text) { var data = TableData.GetOrCreateCell(row, col); data.Text = text; @@ -45,6 +64,12 @@ namespace XericUI.XTable.Rendering.Component /// 设置图片纹理(ContentType 自动设为 Image) public XTableCellData SetCellImage(int row, int col, Texture2D image) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.SetCellImage, row, col, texture: image))) return null; + return SetCellImageImmediate(row, col, image); + } + + private XTableCellData SetCellImageImmediate(int row, int col, Texture2D image) { var data = TableData.GetOrCreateCell(row, col); data.Image = image; @@ -56,6 +81,12 @@ namespace XericUI.XTable.Rendering.Component /// 设置预制体(ContentType 自动设为 Prefab) /// 注意是设置预制体而不是实例,设置后订阅单元格预制体生成事件 OnPrefabAcquire 处理获取事件 public XTableCellData SetCellPrefab(int row, int col, GameObject prefab) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.SetCellPrefab, row, col, prefab: prefab))) return null; + return SetCellPrefabImmediate(row, col, prefab); + } + + private XTableCellData SetCellPrefabImmediate(int row, int col, GameObject prefab) { var data = TableData.GetOrCreateCell(row, col); data.Prefab = prefab; @@ -66,6 +97,12 @@ namespace XericUI.XTable.Rendering.Component /// 设置单元格或合并矩形的样式,并覆写其四条外边范围。 public XTableCellData SetCellStyle(int row, int col, string styleNamespace, string stylePath) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.SetCellStyle, row, col, stringValue1: styleNamespace, stringValue2: stylePath))) return null; + return SetCellStyleImmediate(row, col, styleNamespace, stylePath); + } + + private XTableCellData SetCellStyleImmediate(int row, int col, string styleNamespace, string stylePath) { ResolveStyleRect(row, col, out int rowStart, out int rowEnd, out int colStart, out int colEnd); XTableCellData result = null; @@ -86,6 +123,12 @@ namespace XericUI.XTable.Rendering.Component /// 设置单元格的样式命名空间(兼容旧版,等同于 StyleNamespace 字段) public void SetCellStyleNamespace(int row, int col, string styleNamespace) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.SetCellStyleNamespace, row, col, stringValue1: styleNamespace))) return; + SetCellStyleNamespaceImmediate(row, col, styleNamespace); + } + + private void SetCellStyleNamespaceImmediate(int row, int col, string styleNamespace) { ResolveStyleRect(row, col, out int rowStart, out int rowEnd, out int colStart, out int colEnd); for (int r = rowStart; r < rowEnd; r++) @@ -108,6 +151,12 @@ namespace XericUI.XTable.Rendering.Component /// 清除文本内容 public void ClearCellText(int row, int col) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.ClearCellText, row, col))) return; + ClearCellTextImmediate(row, col); + } + + private void ClearCellTextImmediate(int row, int col) { var data = TableData.GetCell(row, col); if (data == null) return; @@ -119,6 +168,12 @@ namespace XericUI.XTable.Rendering.Component /// 清除图片纹理 public void ClearCellImage(int row, int col) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.ClearCellImage, row, col))) return; + ClearCellImageImmediate(row, col); + } + + private void ClearCellImageImmediate(int row, int col) { var data = TableData.GetCell(row, col); if (data == null) return; @@ -128,6 +183,12 @@ namespace XericUI.XTable.Rendering.Component /// 清除预制体及委托 public void ClearCellPrefab(int row, int col) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.ClearCellPrefab, row, col))) return; + ClearCellPrefabImmediate(row, col); + } + + private void ClearCellPrefabImmediate(int row, int col) { var data = TableData.GetCell(row, col); if (data == null) return; @@ -139,6 +200,12 @@ namespace XericUI.XTable.Rendering.Component /// 清除单元格样式覆盖 public void ClearCellStyle(int row, int col) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.ClearCellStyle, row, col))) return; + ClearCellStyleImmediate(row, col); + } + + private void ClearCellStyleImmediate(int row, int col) { ResolveStyleRect(row, col, out int rowStart, out int rowEnd, out int colStart, out int colEnd); for (int r = rowStart; r < rowEnd; r++) @@ -154,7 +221,10 @@ namespace XericUI.XTable.Rendering.Component } public XTableCellData GetCellData(int row, int col) - => TableData.GetOrCreateCell(row, col); + { + ThrowIfCheckingUpdate(nameof(GetCellData)); + return TableData.GetOrCreateCell(row, col); + } /// 获取单元格的样式命名空间(含回退逻辑) public string GetCellStyleNamespace(int row, int col) @@ -164,12 +234,24 @@ namespace XericUI.XTable.Rendering.Component } public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.MergeCells, startRow, startCol, rowSpan, colSpan))) return; + MergeCellsImmediate(startRow, startCol, rowSpan, colSpan); + } + + private void MergeCellsImmediate(int startRow, int startCol, int rowSpan, int colSpan) { TableData.MergeCells(startRow, startCol, rowSpan, colSpan); MarkDirty(TableDirtyType.DataChanged); } public void UnmergeCells(int startRow, int startCol) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.UnmergeCells, startRow, startCol))) return; + UnmergeCellsImmediate(startRow, startCol); + } + + private void UnmergeCellsImmediate(int startRow, int startCol) { TableData.UnmergeCells(startRow, startCol); MarkDirty(TableDirtyType.DataChanged); @@ -184,6 +266,12 @@ namespace XericUI.XTable.Rendering.Component /// 在末尾追加一行,默认行高来自 Config public void AddRow(float height = -1f) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.AddRow, floatValue: height))) return; + AddRowImmediate(height); + } + + private void AddRowImmediate(float height) { if (height <= 0) height = Config.DefaultRowHeight; #if UNITY_EDITOR @@ -194,7 +282,6 @@ namespace XericUI.XTable.Rendering.Component System.Array.Copy(m_RowHeights, newArr, m_RowHeights.Length); newArr[newLen - 1] = height; m_RowHeights = newArr; - RecordStructuralChange(false); MarkDirty(TableDirtyType.LayoutChanged); #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(this); @@ -203,6 +290,12 @@ namespace XericUI.XTable.Rendering.Component /// 在末尾追加一列,默认列宽来自 Config public void AddColumn(float width = -1f) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.AddColumn, floatValue: width))) return; + AddColumnImmediate(width); + } + + private void AddColumnImmediate(float width) { if (width <= 0) width = Config.DefaultColumnWidth; #if UNITY_EDITOR @@ -213,7 +306,6 @@ namespace XericUI.XTable.Rendering.Component System.Array.Copy(m_ColWidths, newArr, m_ColWidths.Length); newArr[newLen - 1] = width; m_ColWidths = newArr; - RecordStructuralChange(false); MarkDirty(TableDirtyType.LayoutChanged); #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(this); @@ -222,6 +314,12 @@ namespace XericUI.XTable.Rendering.Component /// 设置指定行的高度 public void SetRowHeight(int row, float height) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.SetRowHeight, row, floatValue: height))) return; + SetRowHeightImmediate(row, height); + } + + private void SetRowHeightImmediate(int row, float height) { if (row < 0 || row >= m_RowHeights.Length) return; if (height <= 0) return; @@ -237,6 +335,12 @@ namespace XericUI.XTable.Rendering.Component /// 设置指定列的宽度 public void SetColWidth(int col, float width) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.SetColWidth, col: col, floatValue: width))) return; + SetColWidthImmediate(col, width); + } + + private void SetColWidthImmediate(int col, float width) { if (col < 0 || col >= m_ColWidths.Length) return; if (width <= 0) return; @@ -252,15 +356,18 @@ namespace XericUI.XTable.Rendering.Component /// 删除指定行,单元格数据上移 public void RemoveRow(int row) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.RemoveRow, row))) return; + RemoveRowImmediate(row); + } + + private void RemoveRowImmediate(int row) { if (row < 0 || row >= m_RowHeights.Length) return; #if UNITY_EDITOR UnityEditor.Undo.RecordObject(this, "Remove Table Row"); #endif int colCount = m_ColWidths.Length; - CaptureActivePrefabReleaseCallbacks(); - RecordStructuralChange(true); - // 先解除涉及该行的合并 for (int c = 0; c < colCount; c++) TableData.UnmergeCells(row, c); @@ -289,6 +396,7 @@ namespace XericUI.XTable.Rendering.Component for (int i = row; i < newLen; i++) newArr[i] = m_RowHeights[i + 1]; m_RowHeights = newArr; + TableData.ClearAllBorderCache(); // 裁剪选区到新尺寸 ClampSelectionToBounds(); @@ -301,15 +409,18 @@ namespace XericUI.XTable.Rendering.Component /// 删除指定列,单元格数据左移 public void RemoveColumn(int col) + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.RemoveColumn, col: col))) return; + RemoveColumnImmediate(col); + } + + private void RemoveColumnImmediate(int col) { if (col < 0 || col >= m_ColWidths.Length) return; #if UNITY_EDITOR UnityEditor.Undo.RecordObject(this, "Remove Table Column"); #endif int rowCount = m_RowHeights.Length; - CaptureActivePrefabReleaseCallbacks(); - RecordStructuralChange(true); - // 先解除涉及该列的合并 for (int r = 0; r < rowCount; r++) TableData.UnmergeCells(r, col); @@ -338,6 +449,7 @@ namespace XericUI.XTable.Rendering.Component for (int i = col; i < newLen; i++) newArr[i] = m_ColWidths[i + 1]; m_ColWidths = newArr; + TableData.ClearAllBorderCache(); // 裁剪选区到新尺寸 ClampSelectionToBounds(); @@ -432,6 +544,7 @@ namespace XericUI.XTable.Rendering.Component [ContextMenu("Xeric UI/完全重建表格")] public void FullRebuild() { + ThrowIfCheckingUpdate(nameof(FullRebuild)); m_IsRebuilding = true; try { @@ -470,6 +583,7 @@ namespace XericUI.XTable.Rendering.Component /// 重新生成表格但保留已有文本数据——先保存,重建后再回填 public void FullRebuildPreservingData() { + ThrowIfCheckingUpdate(nameof(FullRebuildPreservingData)); m_IsRebuilding = true; try { @@ -540,6 +654,7 @@ namespace XericUI.XTable.Rendering.Component /// public void ClearData() { + ThrowIfCheckingUpdate(nameof(ClearData)); if (m_TableData != null) { // 清理预制体:先调用 OnPrefabRelease 再清理映射 @@ -616,10 +731,17 @@ namespace XericUI.XTable.Rendering.Component /// 仅强制刷新样式,不重建数据结构 public void ForceStyleRefresh() + { + if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.ForceStyleRefresh))) return; + ForceStyleRefreshImmediate(); + } + + private void ForceStyleRefreshImmediate() { StyleManager.ForceRefresh(); MarkDirty(TableDirtyType.StyleChanged); - RefreshView(); + if (!IsCheckingUpdate) + RefreshView(); } /// 构建剪贴板数据对象(尺寸 + 单元格文本 + 合并信息) @@ -631,10 +753,8 @@ namespace XericUI.XTable.Rendering.Component /// 将剪贴板数据应用到表格——覆盖尺寸、命名空间、单元格文本、合并信息并刷新 public void ApplyClipboardData(XTableClipboardData data) { + ThrowIfCheckingUpdate(nameof(ApplyClipboardData)); if (data == null) return; - CaptureActivePrefabReleaseCallbacks(); - RecordStructuralChange(true); - // 确保数据层存在 if (m_TableData == null) m_TableData = new XTableData(); diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs index 6620fd6..b963c60 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs @@ -24,6 +24,7 @@ namespace XericUI.XTable.Rendering.Component /// 工作表索引(0-based),默认 0 即第一个工作表 public void LoadFromExcelFile(string filePath, int sheetIndex = 0) { + ThrowIfCheckingUpdate(nameof(LoadFromExcelFile)); if (!TryOpenPackage(filePath, out ExcelPackage package)) return; using (package) { @@ -46,6 +47,7 @@ namespace XericUI.XTable.Rendering.Component /// 工作表名称 public void LoadFromExcelFile(string filePath, string sheetName) { + ThrowIfCheckingUpdate(nameof(LoadFromExcelFile)); if (string.IsNullOrEmpty(sheetName)) { Debug.LogWarning("[XTable] LoadFromExcelFile: 工作表名称为空"); @@ -262,8 +264,6 @@ namespace XericUI.XTable.Rendering.Component } // 标记脏并刷新 - CaptureActivePrefabReleaseCallbacks(); - RecordStructuralChange(true); RecalculateTotalSize(); RequestRefresh(TableDirtyType.DataChanged | TableDirtyType.LayoutChanged, true); } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs index 77572ab..5c4733a 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs @@ -225,39 +225,22 @@ namespace XericUI.XTable.Rendering.Component #region 完整刷新 - private void CommitStructuralRenderCleanup(bool invalidatePrefabBindings, - Dictionary> prefabReleaseCallbacks) + private void ReleaseRenderedBindings(Dictionary> prefabReleaseCallbacks = null) { ReleaseAllTrackedCells(); ReleaseFrozenCells(); m_LastRenderedCells.Clear(); - - if (m_ActivePrefabMap != null) + m_PrefabPool?.ReturnAll(instance => { - foreach (var pair in m_ActivePrefabMap) - { - if (prefabReleaseCallbacks != null && prefabReleaseCallbacks.ContainsKey(pair.Value)) continue; - var data = TableData.GetCell(pair.Key.Item1, pair.Key.Item2); - if (data?.OnPrefabRelease != null && prefabReleaseCallbacks != null - && !prefabReleaseCallbacks.ContainsKey(pair.Value)) - prefabReleaseCallbacks.Add(pair.Value, data.OnPrefabRelease); - } - m_ActivePrefabMap.Clear(); - } - - if (invalidatePrefabBindings) - m_PrefabPool?.ReleaseAll(instance => - { - if (prefabReleaseCallbacks != null && prefabReleaseCallbacks.TryGetValue(instance, out var callback)) - callback(instance); - }); - - m_QuadtreeDirty = true; - m_StyleNamespaceDirty = true; + if (prefabReleaseCallbacks != null && prefabReleaseCallbacks.TryGetValue(instance, out var callback)) + callback(instance); + }); + m_ActivePrefabMap?.Clear(); } - public void RefreshView() + public void RefreshView(bool releaseBindings = true) { + TableDirtyType refreshedFlags = m_DirtyFlag.Flags; if (m_Assembler == null) { if (rectTransform != null) @@ -288,19 +271,8 @@ namespace XericUI.XTable.Rendering.Component UpdateBackground(); - // 归还所有追踪对象 + 预制体 - if (m_ActivePrefabMap != null) - { - foreach (var kv in m_ActivePrefabMap) - { - var cellData = TableData.GetCell(kv.Key.Item1, kv.Key.Item2); - cellData?.OnPrefabRelease?.Invoke(kv.Value); - } - m_ActivePrefabMap.Clear(); - } - m_PrefabPool?.ReturnAll(); - ReleaseAllTrackedCells(); - ReleaseFrozenCells(); + if (releaseBindings) + ReleaseRenderedBindings(CaptureActivePrefabReleaseCallbacks()); // 清理孤立子对象 CleanOrphanChildren(); @@ -320,7 +292,7 @@ namespace XericUI.XTable.Rendering.Component RenderFrozenCells(true); RebuildBorders(); - m_DirtyFlag.Clear(); + m_DirtyFlag.ClearFlags(refreshedFlags); OnTableRefreshed?.Invoke(); SetChildrenVisible(m_ChildrenVisible); } @@ -380,7 +352,9 @@ namespace XericUI.XTable.Rendering.Component foreach (var key in m_ReusableCellList) { - // 不调用 OnPrefabRelease 和 Release——预制体保留在槽位中 + GameObject instance = m_ActivePrefabMap[key]; + var data = TableData.GetCell(key.Item1, key.Item2); + m_PrefabPool?.Release(instance, data?.OnPrefabRelease); m_ActivePrefabMap.Remove(key); } } @@ -398,7 +372,7 @@ namespace XericUI.XTable.Rendering.Component // 5. 交换:将 newVisible 作为下一帧的比较基准 (m_LastRenderedCells, newVisible) = (newVisible, m_LastRenderedCells); - m_DirtyFlag.ClearFlags(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged); + m_DirtyFlag.ClearFlags(refreshedFlags & (TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged)); OnTableRefreshed?.Invoke(); SetChildrenVisible(m_ChildrenVisible); } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs index 4fb2523..b7850e0 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs @@ -1,37 +1,84 @@ using System; using System.Collections.Generic; using UnityEngine; +using XericUI.XTable.Core; namespace XericUI.XTable.Rendering.Component { public partial class XericUIActionTable { + private enum UpdateCommandType + { + SetFreezeCount, + SetCellData, + SetCellText, + SetCellImage, + SetCellPrefab, + ClearCellText, + ClearCellImage, + ClearCellPrefab, + SetCellStyle, + SetCellStyleNamespace, + ClearCellStyle, + MergeCells, + UnmergeCells, + AddRow, + AddColumn, + RemoveRow, + RemoveColumn, + SetRowHeight, + SetColWidth, + ForceStyleRefresh + } + + private readonly struct UpdateCommand + { + public readonly UpdateCommandType Type; + public readonly int Row; + public readonly int Col; + public readonly int IntValue1; + public readonly int IntValue2; + public readonly float FloatValue; + public readonly string StringValue1; + public readonly string StringValue2; + public readonly XTableCellData CellData; + public readonly Texture2D Texture; + public readonly GameObject Prefab; + + public UpdateCommand(UpdateCommandType type, int row = 0, int col = 0, + int intValue1 = 0, int intValue2 = 0, float floatValue = 0f, + string stringValue1 = null, string stringValue2 = null, + XTableCellData cellData = null, Texture2D texture = null, GameObject prefab = null) + { + Type = type; + Row = row; + Col = col; + IntValue1 = intValue1; + IntValue2 = intValue2; + FloatValue = floatValue; + StringValue1 = stringValue1; + StringValue2 = stringValue2; + CellData = cellData; + Texture = texture; + Prefab = prefab; + } + } + private sealed class UpdateContext { public TableDirtyType DirtyFlags; - public bool HasStructuralChange; - public bool InvalidatePrefabBindings; - public bool RequiresImmediateRefresh; - public readonly Dictionary> PrefabReleaseCallbacks = new(); + public readonly List Commands = new(); public void Reset() { DirtyFlags = TableDirtyType.None; - HasStructuralChange = false; - InvalidatePrefabBindings = false; - RequiresImmediateRefresh = false; - PrefabReleaseCallbacks.Clear(); + Commands.Clear(); } public void MergeFrom(UpdateContext child) { DirtyFlags |= child.DirtyFlags; - HasStructuralChange |= child.HasStructuralChange; - InvalidatePrefabBindings |= child.InvalidatePrefabBindings; - RequiresImmediateRefresh |= child.RequiresImmediateRefresh; - foreach (var pair in child.PrefabReleaseCallbacks) - if (!PrefabReleaseCallbacks.ContainsKey(pair.Key)) - PrefabReleaseCallbacks.Add(pair.Key, pair.Value); + Commands.AddRange(child.Commands); } } @@ -51,25 +98,22 @@ namespace XericUI.XTable.Rendering.Component { if (m_Disposed) return; m_Disposed = true; - if (m_Table != null) - m_Table.EndCheckUpdate(m_Token); + m_Table?.EndCheckUpdate(m_Token); m_Table = null; } } [NonSerialized] private readonly Stack m_UpdateContextStack = new(); [NonSerialized] private readonly Stack m_UpdateContextPool = new(); - [NonSerialized] private long m_NextUpdateToken; [NonSerialized] private readonly Stack m_UpdateTokenStack = new(); + [NonSerialized] private long m_NextUpdateToken; private bool IsCheckingUpdate => m_UpdateContextStack.Count > 0; - /// - /// 开始一个可嵌套的批量更新作用域。作用域内的数据修改立即生效,渲染与结构资源整理延迟到最外层 Dispose。 - /// + /// 开始一个可嵌套的命令缓冲作用域。可变 API 在最外层 Dispose 时按调用顺序执行。 public UpdateScope CheckUpdate() { - var context = m_UpdateContextPool.Count > 0 ? m_UpdateContextPool.Pop() : new UpdateContext(); + UpdateContext context = m_UpdateContextPool.Count > 0 ? m_UpdateContextPool.Pop() : new UpdateContext(); context.Reset(); m_UpdateContextStack.Push(context); long token = ++m_NextUpdateToken; @@ -77,6 +121,13 @@ namespace XericUI.XTable.Rendering.Component return new UpdateScope(this, token); } + private bool TryEnqueueUpdateCommand(UpdateCommand command) + { + if (!IsCheckingUpdate) return false; + m_UpdateContextStack.Peek().Commands.Add(command); + return true; + } + private void EndCheckUpdate(long token) { if (m_UpdateTokenStack.Count == 0 || m_UpdateTokenStack.Peek() != token) @@ -94,65 +145,81 @@ namespace XericUI.XTable.Rendering.Component return; } + m_UpdateContextStack.Push(context); try { - CommitUpdateContext(context); + Dictionary> prefabCallbacks = CaptureActivePrefabReleaseCallbacks(); + ExecuteUpdateCommands(context); + if (context.DirtyFlags == TableDirtyType.None) return; + + m_UpdateContextStack.Pop(); + ReleaseRenderedBindings(prefabCallbacks); + ApplyDirty(context.DirtyFlags); + RefreshView(false); + RefreshSelectionOverlays(); } finally { + if (m_UpdateContextStack.Count > 0 && ReferenceEquals(m_UpdateContextStack.Peek(), context)) + m_UpdateContextStack.Pop(); ReturnUpdateContext(context); } } + private void ExecuteUpdateCommands(UpdateContext context) + { + foreach (UpdateCommand command in context.Commands) + { + switch (command.Type) + { + case UpdateCommandType.SetFreezeCount: SetFreezeCountImmediate(command.Row, command.Col, false); break; + case UpdateCommandType.SetCellData: SetCellDataImmediate(command.Row, command.Col, command.CellData); break; + case UpdateCommandType.SetCellText: SetCellTextImmediate(command.Row, command.Col, command.StringValue1); break; + case UpdateCommandType.SetCellImage: SetCellImageImmediate(command.Row, command.Col, command.Texture); break; + case UpdateCommandType.SetCellPrefab: SetCellPrefabImmediate(command.Row, command.Col, command.Prefab); break; + case UpdateCommandType.ClearCellText: ClearCellTextImmediate(command.Row, command.Col); break; + case UpdateCommandType.ClearCellImage: ClearCellImageImmediate(command.Row, command.Col); break; + case UpdateCommandType.ClearCellPrefab: ClearCellPrefabImmediate(command.Row, command.Col); break; + case UpdateCommandType.SetCellStyle: SetCellStyleImmediate(command.Row, command.Col, command.StringValue1, command.StringValue2); break; + case UpdateCommandType.SetCellStyleNamespace: SetCellStyleNamespaceImmediate(command.Row, command.Col, command.StringValue1); break; + case UpdateCommandType.ClearCellStyle: ClearCellStyleImmediate(command.Row, command.Col); break; + case UpdateCommandType.MergeCells: MergeCellsImmediate(command.Row, command.Col, command.IntValue1, command.IntValue2); break; + case UpdateCommandType.UnmergeCells: UnmergeCellsImmediate(command.Row, command.Col); break; + case UpdateCommandType.AddRow: AddRowImmediate(command.FloatValue); break; + case UpdateCommandType.AddColumn: AddColumnImmediate(command.FloatValue); break; + case UpdateCommandType.RemoveRow: RemoveRowImmediate(command.Row); break; + case UpdateCommandType.RemoveColumn: RemoveColumnImmediate(command.Col); break; + case UpdateCommandType.SetRowHeight: SetRowHeightImmediate(command.Row, command.FloatValue); break; + case UpdateCommandType.SetColWidth: SetColWidthImmediate(command.Col, command.FloatValue); break; + case UpdateCommandType.ForceStyleRefresh: ForceStyleRefreshImmediate(); break; + } + } + } + private void ReturnUpdateContext(UpdateContext context) { context.Reset(); m_UpdateContextPool.Push(context); } - private void CommitUpdateContext(UpdateContext context) + private Dictionary> CaptureActivePrefabReleaseCallbacks() { - if (context.DirtyFlags == TableDirtyType.None) return; - - if (context.HasStructuralChange) - CommitStructuralRenderCleanup(context.InvalidatePrefabBindings, context.PrefabReleaseCallbacks); - - ApplyDirty(context.DirtyFlags); - if (context.RequiresImmediateRefresh) - RefreshView(); - RefreshSelectionOverlays(); - } - - private void RecordStructuralChange(bool invalidatePrefabBindings) - { - if (!IsCheckingUpdate) return; - UpdateContext context = m_UpdateContextStack.Peek(); - context.HasStructuralChange = true; - context.InvalidatePrefabBindings |= invalidatePrefabBindings; - } - - private void CaptureActivePrefabReleaseCallbacks() - { - if (!IsCheckingUpdate || m_ActivePrefabMap == null) return; - var callbacks = m_UpdateContextStack.Peek().PrefabReleaseCallbacks; + var callbacks = new Dictionary>(); + if (m_ActivePrefabMap == null) return callbacks; foreach (var pair in m_ActivePrefabMap) { - var callback = TableData.GetCell(pair.Key.Item1, pair.Key.Item2)?.OnPrefabRelease; + Action callback = TableData.GetCell(pair.Key.Item1, pair.Key.Item2)?.OnPrefabRelease; if (callback != null && !callbacks.ContainsKey(pair.Value)) callbacks.Add(pair.Value, callback); } + return callbacks; } private void RequestRefresh(TableDirtyType type, bool immediate) { MarkDirty(type); - if (IsCheckingUpdate) - { - m_UpdateContextStack.Peek().RequiresImmediateRefresh |= immediate; - return; - } - if (immediate) - RefreshView(); + if (IsCheckingUpdate) return; + if (immediate) RefreshView(); } private void ThrowIfCheckingUpdate(string operation) diff --git a/Runtime/XTable/Rendering/Elements/CellAssembler.cs b/Runtime/XTable/Rendering/Elements/CellAssembler.cs index 179fdc6..61079ca 100644 --- a/Runtime/XTable/Rendering/Elements/CellAssembler.cs +++ b/Runtime/XTable/Rendering/Elements/CellAssembler.cs @@ -105,6 +105,7 @@ namespace XericUI.XTable.Rendering.Elements if (parent != null && item.Root.transform.parent != parent) item.Root.transform.SetParent(parent, false); item.CellId = cellId; + item.Root.SetActive(true); item.Root.name = name; ResetImageDefaults(item); BindStyle(item.Style, styleNamespace); @@ -121,6 +122,7 @@ namespace XericUI.XTable.Rendering.Elements if (parent != null && item.Root.transform.parent != parent) item.Root.transform.SetParent(parent, false); item.CellId = cellId; + item.Root.SetActive(true); item.Root.name = name; ResetTextDefaults(item); BindStyle(item.Style, styleNamespace); @@ -148,30 +150,41 @@ namespace XericUI.XTable.Rendering.Elements OnStyleChangeDetected?.Invoke(); } - /// 归还所有槽位至 FrozenSlots(全量刷新时调用) + /// 归还所有槽位(全量刷新时调用)。被归还对象会隐藏,直到下次 Acquire。 public void ReturnAll() { - // Unbind 所有活跃槽位的样式回调 - m_ImagePool.ReturnAll(); - m_TextPool.ReturnAll(); + m_ImagePool.ReturnAll(HideImage); + m_TextPool.ReturnAll(HideText); } - /// 归还单个 Image 池对象(通过 CellId 定位槽位并移入 FrozenSlots) + /// 归还单个 Image 池对象并隐藏。 public void ReleaseImage(PooledImage item) { if (item == null) return; - item.Style?.Unbind(); + HideImage(item); m_ImagePool.ReleaseByObject(item); } - /// 归还单个 Text 池对象(通过 CellId 定位槽位并移入 FrozenSlots) + /// 归还单个 Text 池对象并隐藏。 public void ReleaseText(PooledText item) { if (item == null) return; - item.Style?.Unbind(); + HideText(item); m_TextPool.ReleaseByObject(item); } + private static void HideImage(PooledImage item) + { + item.Style?.Unbind(); + item.Root.SetActive(false); + } + + private static void HideText(PooledText item) + { + item.Style?.Unbind(); + item.Root.SetActive(false); + } + #endregion #region 默认值重置 diff --git a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs index 9e99e8e..62b8519 100644 --- a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs +++ b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs @@ -1,195 +1,133 @@ +using System; using System.Collections.Generic; - using UnityEngine; namespace XericUI.XTable.Rendering.Elements { - /// - /// 预制体对象池——按原型分组管理,每个原型使用计数器槽位绑定 cellId 到预制体实例。 - /// 预制体不参与 FrozenSlots 流转:始终绑定在 cellId 槽位上,仅在表格销毁时释放。 - /// - public class PrefabPrototypePool - { - #region 内部类型 + /// 按原型管理的预制体池。占用槽位绑定 cellId,归还后隐藏并留在同原型待命池中复用。 + public class PrefabPrototypePool + { + private class PrefabPool + { + public readonly Dictionary ActiveSlots = new(); + public readonly Stack Standby = new(); + } - private class PrefabPool - { - public GameObject Prototype; - /// cellId → 预制体实例 的计数器槽位绑定 - public Dictionary Slots = new Dictionary(); - } + private readonly Dictionary m_Pools = new(); + private readonly Transform m_Parent; + private const int MAX_PREFAB_ITEMS = 256; + private int m_TotalCreated; + private const HideFlags POOL_FLAGS = HideFlags.DontSave | HideFlags.HideInHierarchy; + private static readonly Vector2 AnchorTopLeft = new(0, 1); + private static readonly Vector2 PivotTopLeft = new(0, 1); - #endregion + public PrefabPrototypePool(Transform parent) => m_Parent = parent; - #region 字段 + public GameObject Acquire(GameObject prefab, int cellId, Transform parent = null) + { + if (prefab == null) return null; + PrefabPool pool = GetOrCreatePool(prefab); + Transform targetParent = parent ?? m_Parent; + if (pool.ActiveSlots.TryGetValue(cellId, out GameObject existing)) + { + Prepare(existing, targetParent, cellId); + return existing; + } - private Dictionary m_Pools = new Dictionary(); - private Transform m_Parent; + GameObject instance = pool.Standby.Count > 0 ? pool.Standby.Pop() : Create(prefab, targetParent); + if (instance == null) return null; + pool.ActiveSlots[cellId] = instance; + Prepare(instance, targetParent, cellId); + return instance; + } - /// 渲染项目总数上限(全局,跨所有原型) - private const int MAX_PREFAB_ITEMS = 256; - private int m_TotalCreated; + public void ReturnAll(Action beforeRelease = null) + { + foreach (PrefabPool pool in m_Pools.Values) + { + foreach (GameObject instance in pool.ActiveSlots.Values) + { + beforeRelease?.Invoke(instance); + instance.SetActive(false); + pool.Standby.Push(instance); + } + pool.ActiveSlots.Clear(); + } + } - private const HideFlags POOL_FLAGS = HideFlags.DontSave | HideFlags.HideInHierarchy; + public bool Release(GameObject instance, Action beforeRelease = null) + { + if (instance == null) return false; + foreach (PrefabPool pool in m_Pools.Values) + { + int foundCellId = -1; + foreach (var pair in pool.ActiveSlots) + { + if (pair.Value != instance) continue; + foundCellId = pair.Key; + break; + } + if (foundCellId < 0) continue; + beforeRelease?.Invoke(instance); + pool.ActiveSlots.Remove(foundCellId); + instance.SetActive(false); + pool.Standby.Push(instance); + return true; + } + return false; + } - private static readonly Vector2 AnchorTopLeft = new Vector2(0, 1); - private static readonly Vector2 PivotTopLeft = new Vector2(0, 1); + public void DestroyAll() + { + foreach (PrefabPool pool in m_Pools.Values) + { + foreach (GameObject instance in pool.ActiveSlots.Values) KillObject(instance); + foreach (GameObject instance in pool.Standby) KillObject(instance); + } + m_Pools.Clear(); + m_TotalCreated = 0; + } - #endregion + private PrefabPool GetOrCreatePool(GameObject prefab) + { + if (!m_Pools.TryGetValue(prefab, out PrefabPool pool)) + { + pool = new PrefabPool(); + m_Pools.Add(prefab, pool); + } + return pool; + } - #region 构造 / 销毁 + private GameObject Create(GameObject prefab, Transform parent) + { + if (m_TotalCreated >= MAX_PREFAB_ITEMS) + { + Debug.LogWarning($"[XericUIActionTable] Prefab 池已达上限 {MAX_PREFAB_ITEMS},无法分配新实例"); + return null; + } + GameObject instance = UnityEngine.Object.Instantiate(prefab, parent); + instance.hideFlags = POOL_FLAGS; + m_TotalCreated++; + return instance; + } - public PrefabPrototypePool(Transform parent) - { - m_Parent = parent; - } + private static void Prepare(GameObject instance, Transform parent, int cellId) + { + if (parent != null && instance.transform.parent != parent) + instance.transform.SetParent(parent, false); + instance.name = $"_prefab_{cellId}"; + RectTransform rt = instance.GetComponent() ?? instance.AddComponent(); + rt.anchorMin = AnchorTopLeft; + rt.anchorMax = AnchorTopLeft; + rt.pivot = PivotTopLeft; + instance.SetActive(true); + } - /// 彻底销毁所有池对象(所有原型的活跃槽位) - public void DestroyAll() - { - foreach (var kv in m_Pools) - { - var pool = kv.Value; - foreach (var instance in pool.Slots.Values) - KillObject(instance); - pool.Slots.Clear(); - } - m_Pools.Clear(); - m_TotalCreated = 0; - } - - private static void KillObject(GameObject go) - { - if (go == null) return; - if (Application.isPlaying) - Object.Destroy(go); - else - Object.DestroyImmediate(go); - } - - #endregion - - #region 池操作 - - /// - /// 为指定 cellId 获取预制体实例。 - /// 若该 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) - { - Debug.LogWarning($"[XericUIActionTable] Prefab 池已达上限 {MAX_PREFAB_ITEMS},无法分配新实例"); - return null; - } - - var instance = Object.Instantiate(prefab, targetParent); - instance.hideFlags = POOL_FLAGS; - instance.name = $"_prefab_{cellId}"; - m_TotalCreated++; - - // 强制 RectTransform 左上角坐标系 - RectTransform rt = instance.GetComponent(); - if (rt == null) rt = instance.AddComponent(); - rt.anchorMin = AnchorTopLeft; - rt.anchorMax = AnchorTopLeft; - rt.pivot = PivotTopLeft; - - pool.Slots[cellId] = instance; - return instance; - } - - /// - /// 归还指定实例至槽位(仅从槽位移除 + 销毁对象,不在滚动时调用)。 - /// 预制体的回收仅发生在表格销毁时。 - /// - public void Release(GameObject instance) - { - if (instance == null) return; - - foreach (var kv in m_Pools) - { - var pool = kv.Value; - // 查找并移除 - int? foundKey = null; - foreach (var slot in pool.Slots) - { - if (slot.Value == instance) - { - foundKey = slot.Key; - break; - } - } - if (foundKey.HasValue) - { - pool.Slots.Remove(foundKey.Value); - m_TotalCreated--; - KillObject(instance); - return; - } - } - - // 不属于任何池(异常情况),直接销毁 - KillObject(instance); - } - - /// 销毁所有按 cellId 绑定的实例,并在销毁前通知调用方。 - public void ReleaseAll(System.Action beforeRelease = null) - { - foreach (var pool in m_Pools.Values) - { - foreach (var instance in pool.Slots.Values) - { - beforeRelease?.Invoke(instance); - KillObject(instance); - } - pool.Slots.Clear(); - } - m_Pools.Clear(); - m_TotalCreated = 0; - } - - /// 结束当前渲染周期。实例保留在全局槽位中,以便跨区域复用。 - public void ReturnAll() - { - } - - /// 获取当前所有活跃实例(用于外部遍历回调) - public IReadOnlyCollection GetActive(GameObject prefab) - { - if (prefab == null || !m_Pools.TryGetValue(prefab, out var pool)) - return System.Array.Empty(); - return pool.Slots.Values; - } - - #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 - } + private static void KillObject(GameObject go) + { + if (go == null) return; + if (Application.isPlaying) UnityEngine.Object.Destroy(go); + else UnityEngine.Object.DestroyImmediate(go); + } + } } diff --git a/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs index efe7857..a8c2dce 100644 --- a/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs +++ b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs @@ -147,15 +147,20 @@ namespace XericUI.XTable.Rendering.Elements return false; } - /// 释放所有活跃槽位,但保留其所属计数器。 - public void ReturnAll() + /// 释放所有活跃槽位;回调在槽位变为空闲前执行。 + public void ReturnAll(Action beforeRelease = null) { for (int ci = 0; ci < POOL_SIZE; ci++) { var counter = m_Counters[ci]; if (counter == null) continue; for (int i = 0; i < counter.Slots.Count; i++) - counter.Slots[i].CellId = -1; + { + var slot = counter.Slots[i]; + if (slot.CellId == -1) continue; + beforeRelease?.Invoke(slot.PooledObject); + slot.CellId = -1; + } } }