From 28a850e342cb37b8d34f1c7e1782172a6b4c304f Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Tue, 28 Jul 2026 08:26:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=A1=A8=E6=A0=BC=E6=80=A7?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=8B=86=E5=88=86=E4=BB=A3=E7=A0=81=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=81=8C=E8=B4=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Component/XericUIActionTable.Api.cs | 566 +++++ .../Component/XericUIActionTable.Api.cs.meta | 11 + .../Component/XericUIActionTable.Border.cs | 203 ++ .../XericUIActionTable.Border.cs.meta | 11 + .../Component/XericUIActionTable.Data.cs | 194 ++ .../Component/XericUIActionTable.Data.cs.meta | 11 + .../Component/XericUIActionTable.Editor.cs | 97 + .../XericUIActionTable.Editor.cs.meta | 11 + .../Component/XericUIActionTable.Freeze.cs | 117 + .../XericUIActionTable.Freeze.cs.meta | 11 + .../XericUIActionTable.RendererFilter.cs | 523 +++++ .../XericUIActionTable.RendererFilter.cs.meta | 11 + .../XericUIActionTable.StyleParams.cs | 67 +- .../Rendering/Component/XericUIActionTable.cs | 2011 +---------------- .../Rendering/Elements/CellAssembler.cs | 16 + 15 files changed, 1868 insertions(+), 1992 deletions(-) create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs.meta create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs.meta create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs.meta create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs.meta create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs.meta create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs.meta diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs new file mode 100644 index 0000000..e1366a3 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs @@ -0,0 +1,566 @@ +using System; +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using XericUI.Core.Base; +using XericUI.XTable.Core; +using XericUI.XTable.Mapping; +using XericUI.XTable.Rendering.Elements; +using XericLibrary.Runtime.SuperStyleSheet; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace XericUI.XTable.Rendering.Component +{ + public partial class XericUIActionTable + { + /// 设置冻结行列数并重建视图 + 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 void SetCellData(int row, int col, XTableCellData data) + { + TableData.SetCell(row, col, data); + MarkDirty(TableDirtyType.DataChanged); + } + + /// 设置文本内容(ContentType 自动设为 Text) + public XTableCellData SetCellText(int row, int col, string text) + { + var data = TableData.GetOrCreateCell(row, col); + data.Text = text; + data.ContentType = CellContentType.Text; + MarkDirty(TableDirtyType.DataChanged); + return data; + } + + /// 设置图片纹理(ContentType 自动设为 Image) + public XTableCellData SetCellImage(int row, int col, Texture2D image) + { + var data = TableData.GetOrCreateCell(row, col); + data.Image = image; + data.ContentType = CellContentType.Image; + MarkDirty(TableDirtyType.DataChanged); + return data; + } + + /// 设置预制体(ContentType 自动设为 Prefab) + /// 注意是设置预制体而不是实例,设置后订阅单元格预制体生成事件 OnPrefabAcquire 处理获取事件 + public XTableCellData SetCellPrefab(int row, int col, GameObject prefab) + { + var data = TableData.GetOrCreateCell(row, col); + data.Prefab = prefab; + data.ContentType = CellContentType.Prefab; + MarkDirty(TableDirtyType.DataChanged); + return data; + } + + /// 设置单元格的样式命名空间,同时写入边框缓存(谁最后设置归谁) + public XTableCellData SetCellStyle(int row, int col, string styleNamespace, string stylePath) + { + var data = TableData.GetOrCreateCell(row, col); + if (data.CellStyle == null) + data.CellStyle = new XericLibrary.Runtime.SuperStyleSheet.StyleMember(); + data.CellStyle.CurrentNamespace = styleNamespace ?? ""; + data.CellStyle.StylePath = stylePath ?? ""; + MarkDirty(TableDirtyType.StyleChanged); + return data; + } + + /// 设置单元格的样式命名空间(兼容旧版,等同于 StyleNamespace 字段) + public void SetCellStyleNamespace(int row, int col, string styleNamespace) + { + // 合并源:遍历所有被合并单元格,全部写入该样式 + if (TableData.IsMergeSource(row, col, out int rowSpan, out int colSpan)) + { + for (int r = row; r < row + rowSpan; r++) + { + for (int c = col; c < col + colSpan; c++) + { + if (r < m_RowHeights.Length && c < m_ColWidths.Length) + ApplyCellStyleToCell(r, c, styleNamespace); + } + } + } + else + { + ApplyCellStyleToCell(row, col, styleNamespace); + } + + MarkDirty(TableDirtyType.StyleChanged); + } + + /// 在单个单元格上设置样式,同时写入其右侧和底部的边框缓存 + private void ApplyCellStyleToCell(int row, int col, string ns) + { + var data = TableData.GetOrCreateCell(row, col); + data.StyleNamespace = ns; + TableData.SetBorderRightNs(row, col, ns); + TableData.SetBorderBottomNs(row, col, ns); + } + + /// 清除文本内容 + public void ClearCellText(int row, int col) + { + var data = TableData.GetCell(row, col); + if (data == null) return; + data.Text = null; + if (data.ContentType == CellContentType.Text) + data.ContentType = CellContentType.Text; // 保持类型不变,仅清空内容 + MarkDirty(TableDirtyType.DataChanged); + } + + /// 清除图片纹理 + public void ClearCellImage(int row, int col) + { + var data = TableData.GetCell(row, col); + if (data == null) return; + data.Image = null; + MarkDirty(TableDirtyType.DataChanged); + } + + /// 清除预制体及委托 + 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 ClearCellStyle(int row, int col) + { + var data = TableData.GetCell(row, col); + if (data == null) return; + data.CellStyle = null; + MarkDirty(TableDirtyType.StyleChanged); + } + + public XTableCellData GetCellData(int row, int col) + => TableData.GetOrCreateCell(row, col); + + /// 获取单元格的样式命名空间(含回退逻辑) + public string GetCellStyleNamespace(int row, int col) + { + var data = TableData.GetCell(row, col); + return data != null ? data.GetEffectiveStyleNamespace(m_DefaultStyleNamespace) : m_DefaultStyleNamespace; + } + + public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan) + { + TableData.MergeCells(startRow, startCol, rowSpan, colSpan); + MarkDirty(TableDirtyType.DataChanged); + } + + public void UnmergeCells(int startRow, int startCol) + { + TableData.UnmergeCells(startRow, startCol); + MarkDirty(TableDirtyType.DataChanged); + } + + /// 判断指定单元格是否被合并覆盖(非源,会被重定向) + public bool IsCellMerged(int row, int col) => TableData.IsCellMerged(row, col); + + /// 判断指定单元格是否为合并源(锚点),并返回合并跨度 + public bool IsMergeSource(int row, int col, out int rowSpan, out int colSpan) + => TableData.IsMergeSource(row, col, out rowSpan, out colSpan); + + /// 在末尾追加一行,默认行高来自 Config + public void AddRow(float height = -1f) + { + if (height <= 0) height = Config.DefaultRowHeight; +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(this, "Add Table Row"); +#endif + int newLen = m_RowHeights.Length + 1; + var newArr = new float[newLen]; + System.Array.Copy(m_RowHeights, newArr, m_RowHeights.Length); + newArr[newLen - 1] = height; + m_RowHeights = newArr; + MarkDirty(TableDirtyType.LayoutChanged); +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 在末尾追加一列,默认列宽来自 Config + public void AddColumn(float width = -1f) + { + if (width <= 0) width = Config.DefaultColumnWidth; +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(this, "Add Table Column"); +#endif + int newLen = m_ColWidths.Length + 1; + var newArr = new float[newLen]; + System.Array.Copy(m_ColWidths, newArr, m_ColWidths.Length); + newArr[newLen - 1] = width; + m_ColWidths = newArr; + MarkDirty(TableDirtyType.LayoutChanged); +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 设置指定行的高度 + public void SetRowHeight(int row, float height) + { + if (row < 0 || row >= m_RowHeights.Length) return; + if (height <= 0) return; +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(this, "Set Row Height"); +#endif + m_RowHeights[row] = height; + MarkDirty(TableDirtyType.LayoutChanged); +#if UNITY_EDITOR + if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 设置指定列的宽度 + public void SetColWidth(int col, float width) + { + if (col < 0 || col >= m_ColWidths.Length) return; + if (width <= 0) return; +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(this, "Set Column Width"); +#endif + m_ColWidths[col] = width; + MarkDirty(TableDirtyType.LayoutChanged); +#if UNITY_EDITOR + if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 删除指定行,单元格数据上移 + public void RemoveRow(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; + + // 先解除涉及该行的合并 + for (int c = 0; c < colCount; c++) + TableData.UnmergeCells(row, c); + + // 将下方行数据上移 + for (int r = row + 1; r < m_RowHeights.Length; r++) + for (int c = 0; c < colCount; c++) + { + var data = TableData.GetCell(r, c); + if (data != null) + TableData.SetCell(r - 1, c, data); + else + TableData.SetCell(r - 1, c, null); // 显式清空原位置 + } + + // 清空最后一行数据 + int lastRow = m_RowHeights.Length - 1; + for (int c = 0; c < colCount; c++) + TableData.SetCell(lastRow, c, null); + + // 缩小行高数组 + int newLen = m_RowHeights.Length - 1; + var newArr = new float[newLen]; + for (int i = 0; i < row; i++) + newArr[i] = m_RowHeights[i]; + for (int i = row; i < newLen; i++) + newArr[i] = m_RowHeights[i + 1]; + m_RowHeights = newArr; + + // 裁剪选区到新尺寸 + ClampSelectionToBounds(); + + MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged); +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 删除指定列,单元格数据左移 + public void RemoveColumn(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; + + // 先解除涉及该列的合并 + for (int r = 0; r < rowCount; r++) + TableData.UnmergeCells(r, col); + + // 将右侧列数据左移 + for (int r = 0; r < rowCount; r++) + for (int c = col + 1; c < m_ColWidths.Length; c++) + { + var data = TableData.GetCell(r, c); + if (data != null) + TableData.SetCell(r, c - 1, data); + else + TableData.SetCell(r, c - 1, null); + } + + // 清空最后一列数据 + int lastCol = m_ColWidths.Length - 1; + for (int r = 0; r < rowCount; r++) + TableData.SetCell(r, lastCol, null); + + // 缩小列宽数组 + int newLen = m_ColWidths.Length - 1; + var newArr = new float[newLen]; + for (int i = 0; i < col; i++) + newArr[i] = m_ColWidths[i]; + for (int i = col; i < newLen; i++) + newArr[i] = m_ColWidths[i + 1]; + m_ColWidths = newArr; + + // 裁剪选区到新尺寸 + ClampSelectionToBounds(); + + MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged); +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 设置外部选择句柄(替换内部句柄) + public void SetSelectionHandle(XTableSelectionHandle handle) + { + if (m_SelectionHandle != null) + m_SelectionHandle.OnSelectionChanged -= OnHandleSelectionChanged; + m_SelectionHandle = handle; + if (m_SelectionHandle != null) + m_SelectionHandle.OnSelectionChanged += OnHandleSelectionChanged; + MarkDirty(TableDirtyType.SelectionChanged); + } + + /// 单选一个单元格 + public void SelectCell(int row, int col) + { + SelectionHandle.Select(row, col); + MarkDirty(TableDirtyType.SelectionChanged); + } + + /// 多选一个范围 + public void SelectCell(int startRow, int startCol, int endRow, int endCol) + { + SelectionHandle.Select(startRow, startCol, endRow, endCol); + MarkDirty(TableDirtyType.SelectionChanged); + } + + /// 清除选择 + public void ClearSelection() + { + SelectionHandle.Clear(); + MarkDirty(TableDirtyType.SelectionChanged); + } + + /// 通过文本选择一个单元格(如 "A0") + public bool SelectCellByText(string text) + { + if (!XTableCoordinateFormat.TryParseCell(text, out int row, out int col)) + return false; + SelectCell(row, col); + return true; + } + + /// 通过文本选择一个范围(如 "A0:B1", "A:A", "0:0") + public bool SelectRangeByText(string text) + { + if (!XTableCoordinateFormat.TryParseRange(text, + out int sr, out int sc, out int er, out int ec)) + return false; + + SelectCell(sr, sc, er, ec); + return true; + } + + /// 获取当前选区的文本描述(单格 "A0",多格 "A0:B1") + public string GetSelectionText() + { + if (!SelectionHandle.HasSelection) return string.Empty; + if (SelectionHandle.HasMultiSelection) + { + return XTableCoordinateFormat.RangeToText( + SelectionHandle.MinRow, SelectionHandle.MinCol, + SelectionHandle.MaxRow, SelectionHandle.MaxCol); + } + + return XTableCoordinateFormat.CellToText( + SelectionHandle.StartRow, SelectionHandle.StartCol); + } + + private void OnHandleSelectionChanged(XTableSelectionHandle handle) + { + MarkDirty(TableDirtyType.SelectionChanged); + } + + /// 裁剪选区到当前行列尺寸范围内 + private void ClampSelectionToBounds() + { + int maxR = (m_RowHeights?.Length ?? 0) - 1; + int maxC = (m_ColWidths?.Length ?? 0) - 1; + SelectionHandle.ClampTo(maxR, maxC); + } + + /// 完全重建表格——销毁所有动态生成的对象并重新初始化 + [ContextMenu("Xeric UI/完全重建表格")] + public void FullRebuild() + { + m_Assembler?.DestroyAll(); + m_PrefabPool?.DestroyAll(); + ClearAllChildren(); + m_BackgroundGo = null; + m_BorderRenderer = null; + + 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(); + CreateViewportRenderer(); + MarkDirty(TableDirtyType.All); + RefreshView(); + SetChildrenVisible(m_ChildrenVisible); + } + + /// 重新生成表格但保留已有文本数据——先保存,重建后再回填 + public void FullRebuildPreservingData() + { + // 保存当前单元格文本 + var savedTexts = new Dictionary<(int, int), string>(); + if (m_TableData != null) + { + int rows = m_RowHeights?.Length ?? 0; + int cols = m_ColWidths?.Length ?? 0; + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + var cell = m_TableData.GetCell(r, c); + if (cell != null && !string.IsNullOrEmpty(cell.Text)) + savedTexts[(r, c)] = cell.Text; + } + } + } + + FullRebuild(); + + // 恢复文本数据 + foreach (var kv in savedTexts) + { + int r = kv.Key.Item1, c = kv.Key.Item2; + if (r < (m_RowHeights?.Length ?? 0) && c < (m_ColWidths?.Length ?? 0)) + m_TableData.GetOrCreateCell(r, c).Text = kv.Value; + } + + RefreshView(); + SetChildrenVisible(m_ChildrenVisible); + } + + /// + /// 清空所有数据(单元格文本、合并信息),但保留行列排布和样式。 + /// 等价于"清空内容但不改变表格结构"。 + /// + public void ClearData() + { + if (m_TableData != null) + { + m_TableData.ClearAllMerges(); + m_TableData.BlockMap.Clear(); + } + m_ActivePrefabMap?.Clear(); + m_PrefabPool?.ReturnAll(); + m_Quadtree?.Clear(); + ClearSelection(); + MarkDirty(TableDirtyType.DataChanged); + RefreshView(); + } + + /// + /// 清空表格所有内容——数据、行列、样式,等同 但不创建默认示例数据。 + /// + public void ClearTable() + { + m_Assembler?.DestroyAll(); + m_PrefabPool?.DestroyAll(); + m_ActivePrefabMap?.Clear(); + m_Quadtree?.Clear(); + ClearAllChildren(); + m_BackgroundGo = null; + m_BorderRenderer = null; + + m_TableData = new XTableData(); + m_RowHeights = null; + m_ColWidths = null; + 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(); + CreateViewportRenderer(); + RefreshView(); + SetChildrenVisible(m_ChildrenVisible); + } + + /// 仅强制刷新样式,不重建数据结构 + public void ForceStyleRefresh() + { + StyleManager.ForceRefresh(); + MarkDirty(TableDirtyType.StyleChanged); + RefreshView(); + } + + /// 构建剪贴板数据对象(尺寸 + 单元格文本 + 合并信息) + public XTableClipboardData BuildClipboardData() + { + return XTableClipboardData.FromTable(this); + } + + /// 将剪贴板数据应用到表格——覆盖尺寸、命名空间、单元格文本、合并信息并刷新 + public void ApplyClipboardData(XTableClipboardData data) + { + if (data == null) return; + + // 确保数据层存在 + if (m_TableData == null) + m_TableData = new XTableData(); + + // 应用尺寸与默认命名空间 + if (data.RowHeights != null && data.RowHeights.Length > 0) + m_RowHeights = data.RowHeights; + if (data.ColWidths != null && data.ColWidths.Length > 0) + m_ColWidths = data.ColWidths; + if (!string.IsNullOrEmpty(data.DefaultStyleNamespace)) + m_DefaultStyleNamespace = data.DefaultStyleNamespace; + + // 写回单元格文本与合并信息 + data.PopulateToTable(this); + + MarkDirty(TableDirtyType.DataChanged); + RecalculateTotalSize(); + RefreshView(); + SetChildrenVisible(m_ChildrenVisible); + } + } +} diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs.meta b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs.meta new file mode 100644 index 0000000..702bfa2 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c49b16a58833a040ab95beac397acb2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs new file mode 100644 index 0000000..b312ed2 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using XericUI.Core.Base; +using XericUI.XTable.Core; +using XericUI.XTable.Mapping; +using XericUI.XTable.Rendering.Elements; +using XericLibrary.Runtime.SuperStyleSheet; +using XericLibrary.Runtime.UIGraph; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace XericUI.XTable.Rendering.Component +{ + public partial class XericUIActionTable + { + /// + /// 获取或创建边框渲染器 GameObject 和 组件。 + /// + private UILineRendererV2 GetOrCreateBorderRenderer() + { + if (m_BorderRenderer != null) + return m_BorderRenderer; + + Transform contentT = ContentTransform; + // 查找已有对象 + var existing = contentT.Find("_TableBorders"); + if (existing != null) + { + m_BorderRenderer = existing.GetComponent(); + if (m_BorderRenderer != null) return m_BorderRenderer; + } + + var go = new GameObject("_TableBorders", + typeof(RectTransform), typeof(CanvasRenderer), typeof(UILineRendererV2)); + go.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; + go.transform.SetParent(contentT, false); + + RectTransform rt = go.GetComponent(); + rt.anchorMin = new Vector2(0, 1); + rt.anchorMax = new Vector2(0, 1); + rt.pivot = new Vector2(0, 1); + float pad = m_ContentOffset * 2f; + rt.anchoredPosition = new Vector2(m_ContentOffset, -m_ContentOffset); + rt.sizeDelta = new Vector2(m_TotalWidth + pad, m_TotalHeight + pad); + + m_BorderRenderer = go.GetComponent(); + return m_BorderRenderer; + } + + /// 销毁边框渲染器 + private void DestroyBorderRenderer() + { + if (m_BorderRenderer != null) + { + if (Application.isPlaying) + Destroy(m_BorderRenderer.gameObject); + else + DestroyImmediate(m_BorderRenderer.gameObject); + m_BorderRenderer = null; + } + } + + /// + /// 重建表格的全部边框线段。 + /// 第一阶段:从 OuterBorderNs 绘制 -1 行(顶部外框)和 -1 列(左侧外框)。 + /// 第二阶段:遍历非合并覆盖单元格,从边框缓存读取样式命名空间,按 per-cell 样式绘制右侧和底部边框。 + /// 合并源在最外沿绘制,内部边框自动跳过。 + /// + private void RebuildBorders() + { + if (m_RowHeights == null || m_ColWidths == null) return; + if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return; + + var renderer = GetOrCreateBorderRenderer(); + renderer.ClearAll(); + + var borderRt = renderer.GetComponent(); + borderRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + +#if UNITY_EDITOR + Color borderColor; + float borderWidth; + if (!Application.isPlaying) + { + borderColor = GetCellBorderColor(m_DefaultStyleNamespace); + borderWidth = GetCellBorderWidth(m_DefaultStyleNamespace); + } + else + { + borderColor = Config.RuntimeBorderColor; + borderWidth = Config.RuntimeBorderWidth; + } +#else + Color borderColor = Config.RuntimeBorderColor; + float borderWidth = Config.RuntimeBorderWidth; +#endif + + int rows = m_RowHeights.Length; + int cols = m_ColWidths.Length; + + // ════════════════════════════════════════════ + // 阶段 1:-1 行(顶部外框)和 -1 列(左侧外框) + // ════════════════════════════════════════════ + + // 顶部外框:从 col=0 到 col=cols-1,每列一段 + float topY = 0; + for (int c = 0; c < cols; c++) + { + string ns = m_TableData?.GetOuterBorderNs(-1, c); + if (string.IsNullOrEmpty(ns)) ns = m_DefaultStyleNamespace; + Color color = GetCellBorderColor(ns); + float width = GetCellBorderWidth(ns); + float leftX = GetColLeftX(c); + renderer.DrawLine( + new Vector2(leftX, topY), + new Vector2(leftX + m_ColWidths[c], topY), + color, width, UVMode.ByDistance); + } + + // 左侧外框:从 row=0 到 row=rows-1,每行一段 + float xEdge = 0; + for (int r = 0; r < rows; r++) + { + string ns = m_TableData?.GetOuterBorderNs(r, -1); + if (string.IsNullOrEmpty(ns)) ns = m_DefaultStyleNamespace; + Color color = GetCellBorderColor(ns); + float width = GetCellBorderWidth(ns); + float rowTopY = -GetRowTopY(r); + renderer.DrawLine( + new Vector2(xEdge, rowTopY), + new Vector2(xEdge, rowTopY - m_RowHeights[r]), + color, width, UVMode.ByDistance); + } + + // ════════════════════════════════════════════ + // 阶段 2:每个单元格的右侧和底部边框 + // ════════════════════════════════════════════ + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + // 跳过被合并覆盖的单元格 + if (m_TableData != null && m_TableData.IsCellMerged(r, c)) + continue; + + float cellX = GetColLeftX(c); + float cellY = -GetRowTopY(r); + float cellW = m_ColWidths[c]; + float cellH = m_RowHeights[r]; + + int mergeRS = 1, mergeCS = 1; + if (m_TableData != null) + m_TableData.IsMergeSource(r, c, out mergeRS, out mergeCS); + + // 扩展合并尺寸 + for (int i = 1; i < mergeCS && c + i < cols; i++) + cellW += m_ColWidths[c + i]; + for (int i = 1; i < mergeRS && r + i < rows; i++) + cellH += m_RowHeights[r + i]; + + float rightX = cellX + cellW; + float bottomY = cellY - cellH; + + // —— 右侧边框 —— + string rightNs = m_TableData?.GetBorderRightNs(r, c); + if (string.IsNullOrEmpty(rightNs)) rightNs = m_DefaultStyleNamespace; + Color rightColor = GetCellBorderColor(rightNs); + float rightWidth = GetCellBorderWidth(rightNs); + + int rightCol = c + mergeCS; + if (rightCol <= cols) + { + renderer.DrawLine( + new Vector2(rightX, cellY), + new Vector2(rightX, bottomY), + rightColor, rightWidth, UVMode.ByDistance); + } + + // —— 底部边框 —— + string bottomNs = m_TableData?.GetBorderBottomNs(r, c); + if (string.IsNullOrEmpty(bottomNs)) bottomNs = m_DefaultStyleNamespace; + Color bottomColor = GetCellBorderColor(bottomNs); + float bottomWidth = GetCellBorderWidth(bottomNs); + + int bottomRow = r + mergeRS; + if (bottomRow <= rows) + { + renderer.DrawLine( + new Vector2(cellX, bottomY), + new Vector2(rightX, bottomY), + bottomColor, bottomWidth, UVMode.ByDistance); + } + } + } + } + } +} diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs.meta b/Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs.meta new file mode 100644 index 0000000..baf8292 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Border.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8357070a070775e42954b1783029541e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs new file mode 100644 index 0000000..26a0cb8 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using XericUI.Core.Base; +using XericUI.XTable.Core; + +namespace XericUI.XTable.Rendering.Component +{ + /// + /// 表格数据与坐标计算——可见性判定、坐标公式、尺寸重算、四叉树维护。 + /// + public partial class XericUIActionTable + { + #region 可见性判定 + + /// 判断指定单元格是否与当前可见矩形有重叠(用于裁剪剔除) + private bool IsCellVisible(int row, int col) + { + // 计入 m_ContentOffset 以匹配单元格在 Content 中的实际渲染位置 + float cellLeft = GetColLeftX(col) + m_ContentOffset; + float cellRight = cellLeft + m_ColWidths[col]; + float cellTop = GetRowTopY(row) + m_ContentOffset; + float cellBottom = cellTop + m_RowHeights[row]; + + return cellRight > m_VisibleRect.x && cellLeft < m_VisibleRect.xMax && + cellBottom > m_VisibleRect.y && cellTop < m_VisibleRect.yMax; + } + + #endregion + + #region 坐标公式 + + private float GetRowTopY(int row) + { + float y = 0; + for (int i = 0; i < row && i < m_RowHeights.Length; i++) + y += m_RowHeights[i]; + return y; + } + + private float GetColLeftX(int col) + { + float x = 0; + for (int i = 0; i < col && i < m_ColWidths.Length; i++) + x += m_ColWidths[i]; + return x; + } + + #endregion + + #region Sprite 缓存 + + /// 将 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; + } + return sprite; + } + + #endregion + + #region 尺寸计算与可见矩形 + + private void RecalculateTotalSize() + { + m_TotalWidth = 0; + if (m_ColWidths != null) + foreach (float w in m_ColWidths) + m_TotalWidth += w; + + m_TotalHeight = 0; + if (m_RowHeights != null) + foreach (float h in m_RowHeights) + m_TotalHeight += h; + + m_ContentOffset = GetCellBorderWidth(m_DefaultStyleNamespace) * 0.5f; + float pad = m_ContentOffset * 2f; + + if (m_ScrollRect != null && m_ScrollRect.content != null) + m_ScrollRect.content.sizeDelta = new Vector2( + m_TotalWidth + pad, m_TotalHeight + pad); + } + + private void UpdateVisibleRect() + { + m_VisibleRect = new Rect(0, 0, m_TotalWidth, m_TotalHeight); + + if (m_ScrollRect == null || m_ScrollRect.viewport == null || m_ScrollRect.content == null) + return; + + RectTransform vt = m_ScrollRect.viewport; + RectTransform ct = m_ScrollRect.content as RectTransform; + if (vt == null || ct == null) return; + + // 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); + } + + private void CalcVisibleRange(out int sr, out int er, out int sc, out int ec) + { + sr = 0; + er = 0; + sc = 0; + ec = 0; + + float accY = 0; + for (int r = 0; r < m_RowHeights.Length; r++) + { + float rowBottom = accY + m_RowHeights[r]; + if (rowBottom > m_VisibleRect.y && accY < m_VisibleRect.yMax) + { + if (er == 0 && sr == 0) sr = r; + er = r + 1; + } + + accY = rowBottom; + if (accY > m_VisibleRect.yMax) break; + } + + float accX = 0; + for (int c = 0; c < m_ColWidths.Length; c++) + { + float colRight = accX + m_ColWidths[c]; + if (colRight > m_VisibleRect.x && accX < m_VisibleRect.xMax) + { + if (ec == 0 && sc == 0) sc = c; + ec = c + 1; + } + + accX = colRight; + if (accX > m_VisibleRect.xMax) break; + } + + if (sr < 0) sr = 0; + if (sc < 0) sc = 0; + } + + #endregion + + #region 四叉树 + + /// 强制重建四叉树(外部调用入口,适合编辑器调试或手动刷新) + public void ForceRebuildQuadtree() + { + if (m_Quadtree == null) + m_Quadtree = new XTableQuadtree(); + MaintainQuadtree(); + } + + /// 从当前 XTableData 收集单元格信息并重建四叉树 + private void MaintainQuadtree() + { + if (m_TableData == null || m_RowHeights == null || m_ColWidths == null) + return; + if (m_Quadtree == null) + m_Quadtree = new XTableQuadtree(); + + // 收集所有有数据的单元格(跳过被合并覆盖的子单元格) + var cells = new List(); + for (int r = 0; r < m_RowHeights.Length; r++) + { + for (int c = 0; c < m_ColWidths.Length; c++) + { + if (m_TableData.IsCellMerged(r, c)) continue; + + var data = m_TableData.GetCell(r, c); + if (data == null) continue; + + string ns = !string.IsNullOrEmpty(data.StyleNamespace) + ? data.StyleNamespace : m_DefaultStyleNamespace; + cells.Add(new XTableQuadtreeCell { Row = r, Col = c, Namespace = ns }); + } + } + + m_Quadtree.Build(m_TotalWidth, m_TotalHeight, m_RowHeights, m_ColWidths, cells, Config.QuadtreeMaxCellsPerNode); + } + + #endregion + } +} diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs.meta b/Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs.meta new file mode 100644 index 0000000..5b1d71a --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Data.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af4d46cbfa5975f4da50bee5277e679e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs new file mode 100644 index 0000000..98b94a7 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs @@ -0,0 +1,97 @@ +using UnityEngine; +using XericUI.Core.Base; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace XericUI.XTable.Rendering.Component +{ + public partial class XericUIActionTable + { + #region 编辑器模式支持 + + [System.NonSerialized] private bool m_EditorUpdateSubscribed; + + private void SubscribeEditorUpdate() + { + if (m_EditorUpdateSubscribed) return; +#if UNITY_EDITOR + UnityEditor.EditorApplication.update += EditorUpdate; + m_EditorUpdateSubscribed = true; +#endif + } + + private void UnsubscribeEditorUpdate() + { + if (!m_EditorUpdateSubscribed) return; +#if UNITY_EDITOR + UnityEditor.EditorApplication.update -= EditorUpdate; + m_EditorUpdateSubscribed = false; +#endif + } + +#if UNITY_EDITOR + private void EditorUpdate() + { + if (this == null || !isActiveAndEnabled) + { + UnsubscribeEditorUpdate(); + return; + } + + if (!m_DirtyFlag.IsDirty) return; + + var flags = m_DirtyFlag.Flags; + if ((flags & ~(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged)) == 0) + RefreshViewLight(); + else + RefreshView(); + } +#endif + + #endregion + + /// 销毁 transform 下所有子对象(用于域重载后清理残留) + private void ClearAllChildren() + { + Transform t = ContentTransform; + if (t == null) return; + +#if UNITY_EDITOR + // 编辑器模式下先重置 HideFlags 再销毁,防止 DontSave 对象拒绝销毁 + while (t.childCount > 0) + { + var child = t.GetChild(0); + child.gameObject.hideFlags = HideFlags.None; + if (Application.isPlaying) + Destroy(child.gameObject); + else + DestroyImmediate(child.gameObject); + } +#else + for (int i = t.childCount - 1; i >= 0; i--) + { + var child = t.GetChild(i); + Destroy(child.gameObject); + } +#endif + } + + /// 设置所有动态子对象的可见性(仅编辑器) + public void SetChildrenVisible(bool visible) + { + m_ChildrenVisible = visible; +#if UNITY_EDITOR + Transform t = ContentTransform; + if (t == null) return; + for (int i = t.childCount - 1; i >= 0; i--) + { + var child = t.GetChild(i); + child.gameObject.hideFlags = visible + ? HideFlags.DontSave + : HideFlags.DontSave | HideFlags.HideInHierarchy; + } +#endif + } + } +} diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs.meta b/Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs.meta new file mode 100644 index 0000000..c30cbc7 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b8508d58614894145a342cc75c9bc1d1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs new file mode 100644 index 0000000..dbfe628 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs @@ -0,0 +1,117 @@ +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 冻结渲染 + + /// 创建或重新创建 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; + } + + /// 渲染 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 + } +} diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs.meta b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs.meta new file mode 100644 index 0000000..e0a859c --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8dc33902374469e4aac60318ee862346 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs new file mode 100644 index 0000000..de20af0 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs @@ -0,0 +1,523 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; +using XericUI.XTable.Core; +using XericUI.XTable.Rendering.Elements; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace XericUI.XTable.Rendering.Component +{ + public partial class XericUIActionTable + { + #region 增量渲染追踪 + + [NonSerialized] private HashSet<(int, int)> m_LastRenderedCells = new(); + [NonSerialized] private Dictionary<(int, int), CellTrackedObjs> m_CellTracked = new(); + + /// 可复用列表,避免每帧分配 GC + [NonSerialized] private List<(int, int)> m_ReusableCellList = new(capacity: 128); + [NonSerialized] private List m_ReusableQuadrants = new(capacity: 32); + + /// 控制滚动时是否使用四叉树(大表格推荐) + private bool UseQuadtreeForScroll => m_Quadtree != null && m_Quadtree.IsBuilt + && m_RowHeights.Length * m_ColWidths.Length > 2000; + + private struct CellTrackedObjs + { + public CellAssembler.PooledText CellText; + public CellAssembler.PooledImage CellImage; + public CellAssembler.PooledImage SelectBg; + public CellAssembler.PooledImage HLine; + public CellAssembler.PooledImage VLine; + } + + #endregion + + #region 追踪对象生命周期 + + private void ReleaseTrackedCell(int row, int col) + { + var key = (row, col); + if (!m_CellTracked.TryGetValue(key, out var objs)) return; + m_Assembler?.ReleaseText(objs.CellText); + m_Assembler?.ReleaseImage(objs.CellImage); + m_Assembler?.ReleaseImage(objs.SelectBg); + m_Assembler?.ReleaseImage(objs.HLine); + m_Assembler?.ReleaseImage(objs.VLine); + m_CellTracked.Remove(key); + } + + private void ReleaseAllTrackedCells() + { + foreach (var kv in m_CellTracked) + { + var objs = kv.Value; + m_Assembler?.ReleaseText(objs.CellText); + m_Assembler?.ReleaseImage(objs.CellImage); + m_Assembler?.ReleaseImage(objs.SelectBg); + m_Assembler?.ReleaseImage(objs.HLine); + m_Assembler?.ReleaseImage(objs.VLine); + } + m_CellTracked.Clear(); + m_LastRenderedCells.Clear(); + } + + #endregion + + #region 可见单元格计算 + + /// 计算当前可见矩形内所有未被合并覆盖的单元格 + private void ComputeVisibleCellsInto(HashSet<(int, int)> result) + { + result.Clear(); + + if (m_Quadtree != null && m_Quadtree.IsBuilt) + { + m_ReusableQuadrants.Clear(); + m_Quadtree.QueryVisible(m_VisibleRect, m_ReusableQuadrants); + foreach (var q in m_ReusableQuadrants) + { + if (string.IsNullOrEmpty(q.Namespace) || q.Cells == null) continue; + foreach (var cell in q.Cells) + { + if (!TableData.IsCellMerged(cell.Item1, cell.Item2) + && IsCellVisible(cell.Item1, cell.Item2)) + result.Add(cell); + } + } + } + else + { + CalcVisibleRange(out int sr, out int er, out int sc, out int ec); + for (int r = sr; r < er; r++) + for (int c = sc; c < ec; c++) + { + if (!TableData.IsCellMerged(r, c) && IsCellVisible(r, c)) + result.Add((r, c)); + } + } + } + + /// [Scroll] 仅用简单范围遍历计算可见单元格(避免四叉树遍历及 QuadrantResult 分配) + private void ComputeVisibleCellsScrollInto(HashSet<(int, int)> result) + { + result.Clear(); + CalcVisibleRange(out int sr, out int er, out int sc, out int ec); + for (int r = sr; r < er; r++) + for (int c = sc; c < ec; c++) + { + if (!TableData.IsCellMerged(r, c) && IsCellVisible(r, c)) + result.Add((r, c)); + } + } + + #endregion + + #region 单元格坐标 + + private float GetCellContentX(int col) + { + return GetColLeftX(col) + m_ContentOffset; + } + + private float GetCellContentY(int row) + { + return -GetRowTopY(row) - m_ContentOffset; + } + + #endregion + + #region 单元格内容渲染(统一入口) + + /// 渲染单个单元格的完整内容(选择高亮 + 网格线 + 文本/图片/预制体)。 + private void RenderCellContent(int row, int col, string defaultNs, + bool withGridLines, bool editorMode) + { + XTableCellData data = TableData.GetCell(row, col); + string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) + ? data.StyleNamespace : defaultNs; + + float cellX = GetCellContentX(col); + float cellY = GetCellContentY(row); + float cellW = m_ColWidths[col]; + float cellH = m_RowHeights[row]; + + // 合并源:扩展尺寸 + if (TableData.IsMergeSource(row, col, out int mergeRS, out int mergeCS)) + { + for (int i = 1; i < mergeCS && col + i < m_ColWidths.Length; i++) + cellW += m_ColWidths[col + i]; + for (int i = 1; i < mergeRS && row + i < m_RowHeights.Length; i++) + cellH += m_RowHeights[row + i]; + } + + var objs = new CellTrackedObjs(); + + // ── 选择高亮 ── + ApplySelectionHighlight(row, col, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs); + + // ── 网格线(仅完整刷新时)── + if (withGridLines) + ApplyGridLines(cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs); + + // ── 单元格内容 ── + if (data != null) + { + switch (data.ContentType) + { + case CellContentType.Text: + RenderCellText(data, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs); + break; + case CellContentType.Image: + RenderCellImage(data, cellX, cellY, cellW, cellH, cellNS, ref objs); + break; + case CellContentType.Prefab: + RenderCellPrefab(data, row, col, cellX, cellY, cellW, cellH); + break; + } + } + + m_CellTracked[(row, col)] = objs; + m_LastRenderedCells.Add((row, col)); + } + + private void ApplySelectionHighlight(int row, int col, + float cellX, float cellY, float cellW, float cellH, + string cellNS, bool editorMode, ref CellTrackedObjs objs) + { + bool inRange = IsInSelectionRange(row, col); + bool isFocused = row == SelectionHandle.StartRow && col == SelectionHandle.StartCol; + + if (!inRange && !isFocused) return; + + string key = inRange ? "cell_multiselect" : "cell_select"; + var img = m_Assembler.GetImage(key, cellNS); + img.Rect.anchoredPosition = new Vector2(cellX, cellY); + img.Rect.sizeDelta = new Vector2(cellW, cellH); + + if (editorMode) + { + Color selColor = GetCellSelectionBgColor(cellNS); + if (inRange) + img.Image.color = isFocused + ? new Color(selColor.r + Config.FocusRAdd, selColor.g + Config.FocusGAdd, + selColor.b + Config.FocusBAdd, selColor.a * Config.FocusAlphaMul) + : new Color(selColor.r, selColor.g, selColor.b, + selColor.a * Config.MultiSelectAlphaFactor); + else + img.Image.color = selColor; + } + else + { + if (inRange) + img.Image.color = isFocused ? Config.RuntimeFocusColor : Config.RuntimeMultiSelectColor; + else + img.Image.color = Config.RuntimeSelectionColor; + } + + objs.SelectBg = img; + } + + private void ApplyGridLines(float cellX, float cellY, float cellW, float cellH, + string cellNS, bool editorMode, ref CellTrackedObjs objs) + { + Color borderColor; + float borderWidth; + + if (editorMode) + { + borderColor = GetCellBorderColor(cellNS); + borderWidth = GetCellBorderWidth(cellNS); + } + else + { + borderColor = Config.RuntimeBorderColor; + borderWidth = Config.RuntimeBorderWidth; + } + + // 水平网格线(底部) + var hLine = m_Assembler.GetImage("cell_hline", cellNS); + hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH); + hLine.Rect.sizeDelta = new Vector2(cellW, borderWidth); + hLine.Image.color = borderColor; + objs.HLine = hLine; + + // 垂直网格线(右侧) + var vLine = m_Assembler.GetImage("cell_vline", cellNS); + vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - borderWidth, cellY); + vLine.Rect.sizeDelta = new Vector2(borderWidth, cellH); + vLine.Image.color = borderColor; + objs.VLine = vLine; + } + + private void RenderCellText(XTableCellData data, + float cellX, float cellY, float cellW, float cellH, + string cellNS, bool editorMode, ref CellTrackedObjs objs) + { + if (string.IsNullOrEmpty(data.Text)) return; + + var txt = m_Assembler.GetText("cell_text", cellNS); + txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); + txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); + txt.Text.text = data.Text; + + if (editorMode) + { + txt.Text.color = GetCellFgColor(cellNS); + txt.Text.fontSize = GetCellFontSize(cellNS); + txt.Text.alignment = TMPro.TextAlignmentOptions.Midline; + txt.Text.font = GetCellFontAsset(cellNS) ?? txt.Text.font; + } + + objs.CellText = txt; + } + + private void RenderCellImage(XTableCellData data, + float cellX, float cellY, float cellW, float cellH, + string cellNS, ref CellTrackedObjs objs) + { + if (data.Image == null) return; + + Sprite sprite = GetOrCreateSprite(data.Image); + var img = m_Assembler.GetImage("cell_image", cellNS); + img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); + img.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); + img.Image.sprite = sprite; + + objs.CellImage = img; + } + + private void RenderCellPrefab(XTableCellData data, int row, int col, + float cellX, float cellY, float cellW, float cellH) + { + if (data.Prefab == null) return; + + 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); + } + + #endregion + + #region 完整刷新 + + public void RefreshView() + { + if (m_Assembler == null) + { + if (rectTransform != null) + m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace); + else return; + } + + if (m_RowHeights == null || m_ColWidths == null) return; + if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return; + + CreateBackground(); + +#if UNITY_EDITOR + if (!Application.isPlaying) + Canvas.ForceUpdateCanvases(); +#endif + + RecalculateTotalSize(); + UpdateVisibleRect(); + + // 确保 ScrollRect Content 尺寸正确 + RectTransform ctRt = ContentTransform as RectTransform; + if (ctRt != null) + { + float pad2 = m_ContentOffset * 2f; + ctRt.sizeDelta = new Vector2(m_TotalWidth + pad2, m_TotalHeight + pad2); + } + + // 更新背景尺寸 + if (m_BackgroundGo != null) + { + var bgRt = m_BackgroundGo.GetComponent(); + bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + } + + // 归还所有追踪对象 + 预制体 + 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(); + + // 清理孤立子对象 + CleanOrphanChildren(); + + // 渲染所有可见单元格 +#if UNITY_EDITOR + bool editorMode = !Application.isPlaying; +#else + bool editorMode = false; +#endif + string ns = m_DefaultStyleNamespace; + ComputeVisibleCellsInto(m_LastRenderedCells); + foreach (var (r, c) in m_LastRenderedCells) + RenderCellContent(r, c, ns, withGridLines: true, editorMode: editorMode); + + // 边框 + 冻结 + RebuildBorders(); + RenderFrozenCells(); + + m_DirtyFlag.Clear(); + OnTableRefreshed?.Invoke(); + SetChildrenVisible(m_ChildrenVisible); + } + + #endregion + + #region 轻刷新(增量——仅处理可见性变更) + + private void RefreshViewLight() + { + if (m_Assembler == null) + { + if (rectTransform != null) + m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace); + else return; + } + + if (m_RowHeights == null || m_ColWidths == null) return; + if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return; + + UpdateVisibleRect(); + +#if UNITY_EDITOR + if (!Application.isPlaying) + Canvas.ForceUpdateCanvases(); +#endif + +#if UNITY_EDITOR + bool editorMode = !Application.isPlaying; +#else + bool editorMode = false; +#endif + string ns = m_DefaultStyleNamespace; + + // 选区变更时:所有单元格的选择高亮需要重绘,但预制体不受影响 + bool selectionChanged = m_DirtyFlag.Has(TableDirtyType.SelectionChanged); + if (selectionChanged) + { + ReleaseAllTrackedCells(); + } + + // 1. 计算新一帧的可见单元格(滚动路径用简单范围遍历,避免四叉树/分配开销) + var newVisible = new HashSet<(int, int)>(); + ComputeVisibleCellsScrollInto(newVisible); + + // 2. 回收不再可见的追踪对象 + if (!selectionChanged) + { + m_ReusableCellList.Clear(); + foreach (var cell in m_LastRenderedCells) + if (!newVisible.Contains(cell)) + m_ReusableCellList.Add(cell); + foreach (var cell in m_ReusableCellList) + ReleaseTrackedCell(cell.Item1, cell.Item2); + } + + // 3. 回收不再可见的预制体 + if (m_ActivePrefabMap != null && m_ActivePrefabMap.Count > 0) + { + m_ReusableCellList.Clear(); + foreach (var kv in m_ActivePrefabMap) + if (!newVisible.Contains(kv.Key)) + m_ReusableCellList.Add(kv.Key); + + foreach (var key in m_ReusableCellList) + { + var cellData = TableData.GetCell(key.Item1, key.Item2); + cellData?.OnPrefabRelease?.Invoke(m_ActivePrefabMap[key]); + m_PrefabPool.Release(m_ActivePrefabMap[key]); + m_ActivePrefabMap.Remove(key); + } + } + + // 4. 渲染单元格:选区变更时全量重渲所有可见单元格,否则仅新进入的 + foreach (var cell in newVisible) + { + if (selectionChanged || !m_LastRenderedCells.Contains(cell)) + RenderCellContent(cell.Item1, cell.Item2, ns, + withGridLines: false, editorMode: editorMode); + } + + // 5. 交换:将 newVisible 作为下一帧的比较基准 + (m_LastRenderedCells, newVisible) = (newVisible, m_LastRenderedCells); + + m_DirtyFlag.ClearFlags(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged); + OnTableRefreshed?.Invoke(); + SetChildrenVisible(m_ChildrenVisible); + } + + #endregion + + #region 孤立子对象清理 + + private void CleanOrphanChildren() + { + Transform poolRoot = m_Assembler?.PoolRoot; + Transform prefabPoolRoot = m_PrefabPool?.PoolRoot; + Transform contentT = ContentTransform; + Transform borderT = contentT?.Find("_TableBorders"); + + for (int i = contentT.childCount - 1; i >= 0; i--) + { + var child = contentT.GetChild(i); + if (poolRoot != null && child == poolRoot) continue; + if (prefabPoolRoot != null && child == prefabPoolRoot) continue; + if (m_BackgroundGo != null && child.gameObject == m_BackgroundGo) continue; + if (borderT != null && child == borderT) continue; + + child.gameObject.hideFlags = HideFlags.None; + if (Application.isPlaying) + Destroy(child.gameObject); + else + DestroyImmediate(child.gameObject); + } + } + + #endregion + + #region 背景创建 + + private void CreateBackground() + { + if (m_BackgroundGo != null) return; + + m_BackgroundGo = new GameObject("_TableBackground", + typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); + m_BackgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; + m_BackgroundGo.transform.SetParent(ContentTransform, 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 = new Vector2(m_ContentOffset, -m_ContentOffset); + bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + + var bgImg = m_BackgroundGo.GetComponent(); + bgImg.color = GetCellBgColor(m_DefaultStyleNamespace); + bgImg.raycastTarget = false; + } + + #endregion + } +} diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs.meta b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs.meta new file mode 100644 index 0000000..6adfc82 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7336cd39fbd4ade469db004e3b950446 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.StyleParams.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.StyleParams.cs index 19ed9b6..3485e08 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.StyleParams.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.StyleParams.cs @@ -5,8 +5,7 @@ using XericLibrary.Runtime.SuperStyleSheet; namespace XericUI.XTable.Rendering.Component { /// - /// 表格样式参数——统一管理所有样式路径与回退值。 - /// 修改此处的常量即可全局调整表格外观,无需在各方法中查找硬编码字符串。 + /// 表格样式参数——统一管理所有样式路径、回退值、样式取值方法与默认命名空间初始化。 /// public partial class XericUIActionTable { @@ -95,5 +94,69 @@ namespace XericUI.XTable.Rendering.Component GetStyleFontAsset(ns, STYLE_CELL_FONT_ASSET); #endregion + + #region 底层样式查询方法 + + private Color GetStyleColor(string ns, string path, Color fallback) + { + StyleValue val = StyleManager.GetValue(ns, path); + return val != null ? (Color)val : fallback; + } + + private int GetStyleInt(string ns, string path, int fallback) + { + StyleValue val = StyleManager.GetValue(ns, path); + return val != null ? (int)val : fallback; + } + + private float GetStyleFloat(string ns, string path, float fallback) + { + StyleValue val = StyleManager.GetValue(ns, path); + return val != null ? (float)val : fallback; + } + + private TMP_FontAsset GetStyleFontAsset(string ns, string path, TMP_FontAsset fallback = null) + { + StyleValue val = StyleManager.GetValue(ns, path); + if (val == null) return fallback; + try { return val.GetValueAs(); } + catch { return fallback; } + } + + #endregion + + #region 默认样式命名空间初始化 + + private void EnsureDefaultStyleNamespace() + { + // 检查命名空间是否已存在,避免 HasStyle → GetValue 对不存在的命名空间报错 + bool nsExists = StyleManager.GetAvailableNamespaces()?.Contains(m_DefaultStyleNamespace) == true; + + // 命名空间已存在且样式已加载(来自 .xsss),无需添加回退值 + if (nsExists && StyleManager.HasStyle(m_DefaultStyleNamespace, STYLE_CELL_BG_COLOR)) + return; + + // 回退默认值匹配 DefaultTableStyles.xsss + // AddDynamicStyle 自动创建命名空间(如果尚不存在) + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BG_COLOR, DEFAULT_BG_COLOR); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_FG_COLOR, DEFAULT_FG_COLOR); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_TOP_COLOR, DEFAULT_BORDER_COLOR); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_TOP_WIDTH, DEFAULT_BORDER_WIDTH); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_BOTTOM_COLOR, DEFAULT_BORDER_COLOR); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_BOTTOM_WIDTH, DEFAULT_BORDER_WIDTH); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_LEFT_COLOR, DEFAULT_BORDER_COLOR); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_LEFT_WIDTH, DEFAULT_BORDER_WIDTH); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_RIGHT_COLOR, DEFAULT_BORDER_COLOR); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_RIGHT_WIDTH, DEFAULT_BORDER_WIDTH); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_SELECTION_BG_COLOR, DEFAULT_SELECTION_COLOR); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_FONT_SIZE, DEFAULT_FONT_SIZE); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_FONT_ALIGNMENT, DEFAULT_FONT_ALIGNMENT); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_FONT_RICH_TEXT, DEFAULT_FONT_RICH_TEXT); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_DEFAULT_HEIGHT, DEFAULT_CELL_HEIGHT); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_DEFAULT_WIDTH, DEFAULT_CELL_WIDTH); + StyleManager.ForceRefresh(); + } + + #endregion } } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs index 05ced7f..fc7d73f 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -127,16 +127,6 @@ namespace XericUI.XTable.Rendering.Component /// 冻结列数 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 等属性 @@ -166,16 +156,8 @@ namespace XericUI.XTable.Rendering.Component set => SetChildrenVisible(value); } - /// 判断单元格是否在多选范围内 - public bool IsInSelectionRange(int row, int col) - { - return SelectionHandle.IsInRange(row, col); - } - - /// - /// 动态子对象挂载的父节点。 - /// 若绑定了 ScrollRect 则使用其 Content,否则回退到自身的 rectTransform。 - /// + /// 动态子对象挂载的父节点。 + /// 若绑定了 ScrollRect 则使用其 Content,否则回退到自身的 rectTransform。 public Transform ContentTransform { get @@ -203,212 +185,10 @@ namespace XericUI.XTable.Rendering.Component /// 单元格内容宽度扣除量 private float CellPadding2 => Config.CellHorizontalPadding * 2f; - /// - /// 基于四叉树查询结果渲染可见象限内的单元格内容(文本/预制体/选择高亮)。 - /// 边框由 统一绘制。 - /// - private void RenderQuadrants(List quadrants) + /// 判断单元格是否在多选范围内 + public bool IsInSelectionRange(int row, int col) { - string ns = m_DefaultStyleNamespace; - -#if UNITY_EDITOR - if (!Application.isPlaying) - { - Color selectionColor = GetCellSelectionBgColor(ns); - 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); - - foreach (var q in quadrants) - { - if (string.IsNullOrEmpty(q.Namespace)) continue; - - Rect r = q.Region; - string qNs = q.Namespace; - - // 渲染象限内各单元格的文本/预制体/选择高亮 - if (q.Cells != null) - { - foreach (var (row, col) in q.Cells) - { - // 跳过被合并覆盖的单元格 - if (TableData.IsCellMerged(row, col)) continue; - // 跳过不可见的单元格 - if (!IsCellVisible(row, col)) continue; - - float cellX = GetColLeftX(col) + m_ContentOffset; - float cellY = -GetRowTopY(row) - m_ContentOffset; - float cellW = m_ColWidths[col]; - float cellH = m_RowHeights[row]; - - // 合并源:扩展尺寸 - if (TableData.IsMergeSource(row, col, out int mergeRS, out int mergeCS)) - { - for (int i = 1; i < mergeCS && col + i < m_ColWidths.Length; i++) - cellW += m_ColWidths[col + i]; - for (int i = 1; i < mergeRS && row + i < m_RowHeights.Length; i++) - cellH += m_RowHeights[row + i]; - } - - XTableCellData data = TableData.GetCell(row, col); - string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) - ? data.StyleNamespace : ns; - - // 选择高亮 - if (IsInSelectionRange(row, col)) - { - var selImg = m_Assembler.GetImage("cell_multiselect"); - selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); - selImg.Rect.sizeDelta = new Vector2(cellW, cellH); - selImg.Image.color = (row == SelectionHandle.StartRow && col == SelectionHandle.StartCol) - ? focusColor : multiSelectColor; - } - else if (row == SelectionHandle.StartRow && col == SelectionHandle.StartCol) - { - 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; - } - - if (data == null) continue; - - switch (data.ContentType) - { - case CellContentType.Text: - if (!string.IsNullOrEmpty(data.Text)) - { - var txt = m_Assembler.GetText("cell_text", cellNS); - txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); - txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); - txt.Text.text = data.Text; - txt.Text.color = GetCellFgColor(cellNS); - txt.Text.fontSize = GetCellFontSize(cellNS); - txt.Text.alignment = TextAlignmentOptions.Midline; - txt.Text.font = GetCellFontAsset(cellNS) ?? txt.Text.font; - } - break; - case CellContentType.Image: - if (data.Image != null) - { - Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage("cell_image", cellNS); - 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) - { - 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); - } - break; - } - } - } - } - return; - } -#endif - - // 运行时模式 - Color rtSelectionColor = Config.RuntimeSelectionColor; - Color rtMultiSelectColor = Config.RuntimeMultiSelectColor; - Color rtFocusColor = Config.RuntimeFocusColor; - - foreach (var q in quadrants) - { - if (string.IsNullOrEmpty(q.Namespace)) continue; - - Rect r = q.Region; - string qNs = q.Namespace; - - if (q.Cells != null) - { - foreach (var (row, col) in q.Cells) - { - // 跳过被合并覆盖的单元格 - if (TableData.IsCellMerged(row, col)) continue; - // 跳过不可见的单元格 - if (!IsCellVisible(row, col)) continue; - - float cellX = GetColLeftX(col) + m_ContentOffset; - float cellY = -GetRowTopY(row) - m_ContentOffset; - float cellW = m_ColWidths[col]; - float cellH = m_RowHeights[row]; - - // 合并源:扩展尺寸 - if (TableData.IsMergeSource(row, col, out int mergeRS, out int mergeCS)) - { - for (int i = 1; i < mergeCS && col + i < m_ColWidths.Length; i++) - cellW += m_ColWidths[col + i]; - for (int i = 1; i < mergeRS && row + i < m_RowHeights.Length; i++) - cellH += m_RowHeights[row + i]; - } - - XTableCellData data = TableData.GetCell(row, col); - string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) - ? data.StyleNamespace : ns; - - if (IsInSelectionRange(row, col)) - { - var selImg = m_Assembler.GetImage("cell_multiselect"); - selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); - selImg.Rect.sizeDelta = new Vector2(cellW, cellH); - selImg.Image.color = (row == SelectionHandle.StartRow && col == SelectionHandle.StartCol) - ? rtFocusColor : rtMultiSelectColor; - } - else if (row == SelectionHandle.StartRow && col == SelectionHandle.StartCol) - { - 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; - } - - if (data == null) continue; - - switch (data.ContentType) - { - case CellContentType.Text: - if (!string.IsNullOrEmpty(data.Text)) - { - var txt = m_Assembler.GetText("cell_text", cellNS); - txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); - txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); - txt.Text.text = data.Text; - } - break; - case CellContentType.Image: - if (data.Image != null) - { - Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage("cell_image", cellNS); - 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) - { - 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); - } - break; - } - } - } - } + return SelectionHandle.IsInRange(row, col); } #endregion @@ -453,6 +233,19 @@ namespace XericUI.XTable.Rendering.Component if (m_TableData == null) m_TableData = new XTableData(); + // 强制 Content 的 anchor/pivot 为 (0,1),确保 anchoredPosition 语义一致。 + // 若不固定,ScrollRect 可能改变 anchor 导致坐标计算错误。 + if (m_ScrollRect != null && m_ScrollRect.content != null) + { + RectTransform ct = m_ScrollRect.content as RectTransform; + if (ct != null) + { + ct.anchorMin = new Vector2(0, 1); + ct.anchorMax = new Vector2(0, 1); + ct.pivot = new Vector2(0, 1); + } + } + m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace); m_PrefabPool = new PrefabPrototypePool(ContentTransform); m_ActivePrefabMap = new Dictionary<(int, int), GameObject>(); @@ -562,1437 +355,7 @@ namespace XericUI.XTable.Rendering.Component #endregion - #region 编辑器模式支持 - - [System.NonSerialized] private bool m_EditorUpdateSubscribed; - - private void SubscribeEditorUpdate() - { - if (m_EditorUpdateSubscribed) return; -#if UNITY_EDITOR - UnityEditor.EditorApplication.update += EditorUpdate; - m_EditorUpdateSubscribed = true; -#endif - } - - private void UnsubscribeEditorUpdate() - { - if (!m_EditorUpdateSubscribed) return; -#if UNITY_EDITOR - UnityEditor.EditorApplication.update -= EditorUpdate; - m_EditorUpdateSubscribed = false; -#endif - } - -#if UNITY_EDITOR - private void EditorUpdate() - { - if (this == null || !isActiveAndEnabled) - { - UnsubscribeEditorUpdate(); - return; - } - - if (!m_DirtyFlag.IsDirty) return; - - var flags = m_DirtyFlag.Flags; - if ((flags & ~(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged)) == 0) - RefreshViewLight(); - else - RefreshView(); - } -#endif - - #endregion - - #region 公开方法 - - public void SetCellData(int row, int col, XTableCellData data) - { - TableData.SetCell(row, col, data); - MarkDirty(TableDirtyType.DataChanged); - } - - /// 设置文本内容(ContentType 自动设为 Text) - public XTableCellData SetCellText(int row, int col, string text) - { - var data = TableData.GetOrCreateCell(row, col); - data.Text = text; - data.ContentType = CellContentType.Text; - MarkDirty(TableDirtyType.DataChanged); - return data; - } - - /// 设置图片纹理(ContentType 自动设为 Image) - public XTableCellData SetCellImage(int row, int col, Texture2D image) - { - var data = TableData.GetOrCreateCell(row, col); - data.Image = image; - data.ContentType = CellContentType.Image; - MarkDirty(TableDirtyType.DataChanged); - return data; - } - - /// 设置预制体(ContentType 自动设为 Prefab) - /// 注意是设置预制体而不是实例,设置后订阅单元格预制体生成事件 OnPrefabAcquire 处理获取事件 - public XTableCellData SetCellPrefab(int row, int col, GameObject prefab) - { - var data = TableData.GetOrCreateCell(row, col); - data.Prefab = prefab; - data.ContentType = CellContentType.Prefab; - MarkDirty(TableDirtyType.DataChanged); - return data; - } - - /// 设置单元格的样式命名空间,同时写入边框缓存(谁最后设置归谁) - public XTableCellData SetCellStyle(int row, int col, string styleNamespace, string stylePath) - { - var data = TableData.GetOrCreateCell(row, col); - if (data.CellStyle == null) - data.CellStyle = new XericLibrary.Runtime.SuperStyleSheet.StyleMember(); - data.CellStyle.CurrentNamespace = styleNamespace ?? ""; - data.CellStyle.StylePath = stylePath ?? ""; - MarkDirty(TableDirtyType.StyleChanged); - return data; - } - - /// 设置单元格的样式命名空间(兼容旧版,等同于 StyleNamespace 字段) - public void SetCellStyleNamespace(int row, int col, string styleNamespace) - { - // 合并源:遍历所有被合并单元格,全部写入该样式 - if (TableData.IsMergeSource(row, col, out int rowSpan, out int colSpan)) - { - for (int r = row; r < row + rowSpan; r++) - { - for (int c = col; c < col + colSpan; c++) - { - if (r < m_RowHeights.Length && c < m_ColWidths.Length) - ApplyCellStyleToCell(r, c, styleNamespace); - } - } - } - else - { - ApplyCellStyleToCell(row, col, styleNamespace); - } - - MarkDirty(TableDirtyType.StyleChanged); - } - - /// 在单个单元格上设置样式,同时写入其右侧和底部的边框缓存 - private void ApplyCellStyleToCell(int row, int col, string ns) - { - var data = TableData.GetOrCreateCell(row, col); - data.StyleNamespace = ns; - TableData.SetBorderRightNs(row, col, ns); - TableData.SetBorderBottomNs(row, col, ns); - } - - /// 清除文本内容 - public void ClearCellText(int row, int col) - { - var data = TableData.GetCell(row, col); - if (data == null) return; - data.Text = null; - if (data.ContentType == CellContentType.Text) - data.ContentType = CellContentType.Text; // 保持类型不变,仅清空内容 - MarkDirty(TableDirtyType.DataChanged); - } - - /// 清除图片纹理 - public void ClearCellImage(int row, int col) - { - var data = TableData.GetCell(row, col); - if (data == null) return; - data.Image = null; - MarkDirty(TableDirtyType.DataChanged); - } - - /// 清除预制体及委托 - 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 ClearCellStyle(int row, int col) - { - var data = TableData.GetCell(row, col); - if (data == null) return; - data.CellStyle = null; - MarkDirty(TableDirtyType.StyleChanged); - } - - public XTableCellData GetCellData(int row, int col) - => TableData.GetOrCreateCell(row, col); - - /// 获取单元格的样式命名空间(含回退逻辑) - public string GetCellStyleNamespace(int row, int col) - { - var data = TableData.GetCell(row, col); - return data != null ? data.GetEffectiveStyleNamespace(m_DefaultStyleNamespace) : m_DefaultStyleNamespace; - } - - public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan) - { - TableData.MergeCells(startRow, startCol, rowSpan, colSpan); - MarkDirty(TableDirtyType.DataChanged); - } - - public void UnmergeCells(int startRow, int startCol) - { - TableData.UnmergeCells(startRow, startCol); - MarkDirty(TableDirtyType.DataChanged); - } - - /// 判断指定单元格是否被合并覆盖(非源,会被重定向) - public bool IsCellMerged(int row, int col) => TableData.IsCellMerged(row, col); - - /// 判断指定单元格是否为合并源(锚点),并返回合并跨度 - public bool IsMergeSource(int row, int col, out int rowSpan, out int colSpan) - => TableData.IsMergeSource(row, col, out rowSpan, out colSpan); - - /// 在末尾追加一行,默认行高来自 Config - public void AddRow(float height = -1f) - { - if (height <= 0) height = Config.DefaultRowHeight; -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(this, "Add Table Row"); -#endif - int newLen = m_RowHeights.Length + 1; - var newArr = new float[newLen]; - System.Array.Copy(m_RowHeights, newArr, m_RowHeights.Length); - newArr[newLen - 1] = height; - m_RowHeights = newArr; - MarkDirty(TableDirtyType.LayoutChanged); -#if UNITY_EDITOR - UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 在末尾追加一列,默认列宽来自 Config - public void AddColumn(float width = -1f) - { - if (width <= 0) width = Config.DefaultColumnWidth; -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(this, "Add Table Column"); -#endif - int newLen = m_ColWidths.Length + 1; - var newArr = new float[newLen]; - System.Array.Copy(m_ColWidths, newArr, m_ColWidths.Length); - newArr[newLen - 1] = width; - m_ColWidths = newArr; - MarkDirty(TableDirtyType.LayoutChanged); -#if UNITY_EDITOR - UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 设置指定行的高度 - public void SetRowHeight(int row, float height) - { - if (row < 0 || row >= m_RowHeights.Length) return; - if (height <= 0) return; -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(this, "Set Row Height"); -#endif - m_RowHeights[row] = height; - MarkDirty(TableDirtyType.LayoutChanged); -#if UNITY_EDITOR - if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 设置指定列的宽度 - public void SetColWidth(int col, float width) - { - if (col < 0 || col >= m_ColWidths.Length) return; - if (width <= 0) return; -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(this, "Set Column Width"); -#endif - m_ColWidths[col] = width; - MarkDirty(TableDirtyType.LayoutChanged); -#if UNITY_EDITOR - if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 删除指定行,单元格数据上移 - public void RemoveRow(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; - - // 先解除涉及该行的合并 - for (int c = 0; c < colCount; c++) - TableData.UnmergeCells(row, c); - - // 将下方行数据上移 - for (int r = row + 1; r < m_RowHeights.Length; r++) - for (int c = 0; c < colCount; c++) - { - var data = TableData.GetCell(r, c); - if (data != null) - TableData.SetCell(r - 1, c, data); - else - TableData.SetCell(r - 1, c, null); // 显式清空原位置 - } - - // 清空最后一行数据 - int lastRow = m_RowHeights.Length - 1; - for (int c = 0; c < colCount; c++) - TableData.SetCell(lastRow, c, null); - - // 缩小行高数组 - int newLen = m_RowHeights.Length - 1; - var newArr = new float[newLen]; - for (int i = 0; i < row; i++) - newArr[i] = m_RowHeights[i]; - for (int i = row; i < newLen; i++) - newArr[i] = m_RowHeights[i + 1]; - m_RowHeights = newArr; - - // 裁剪选区到新尺寸 - ClampSelectionToBounds(); - - MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged); -#if UNITY_EDITOR - UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 删除指定列,单元格数据左移 - public void RemoveColumn(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; - - // 先解除涉及该列的合并 - for (int r = 0; r < rowCount; r++) - TableData.UnmergeCells(r, col); - - // 将右侧列数据左移 - for (int r = 0; r < rowCount; r++) - for (int c = col + 1; c < m_ColWidths.Length; c++) - { - var data = TableData.GetCell(r, c); - if (data != null) - TableData.SetCell(r, c - 1, data); - else - TableData.SetCell(r, c - 1, null); - } - - // 清空最后一列数据 - int lastCol = m_ColWidths.Length - 1; - for (int r = 0; r < rowCount; r++) - TableData.SetCell(r, lastCol, null); - - // 缩小列宽数组 - int newLen = m_ColWidths.Length - 1; - var newArr = new float[newLen]; - for (int i = 0; i < col; i++) - newArr[i] = m_ColWidths[i]; - for (int i = col; i < newLen; i++) - newArr[i] = m_ColWidths[i + 1]; - m_ColWidths = newArr; - - // 裁剪选区到新尺寸 - ClampSelectionToBounds(); - - MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged); -#if UNITY_EDITOR - UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 设置外部选择句柄(替换内部句柄) - public void SetSelectionHandle(XTableSelectionHandle handle) - { - if (m_SelectionHandle != null) - m_SelectionHandle.OnSelectionChanged -= OnHandleSelectionChanged; - m_SelectionHandle = handle; - if (m_SelectionHandle != null) - m_SelectionHandle.OnSelectionChanged += OnHandleSelectionChanged; - MarkDirty(TableDirtyType.SelectionChanged); - } - - /// 单选一个单元格 - public void SelectCell(int row, int col) - { - SelectionHandle.Select(row, col); - MarkDirty(TableDirtyType.SelectionChanged); - } - - /// 多选一个范围 - public void SelectCell(int startRow, int startCol, int endRow, int endCol) - { - SelectionHandle.Select(startRow, startCol, endRow, endCol); - MarkDirty(TableDirtyType.SelectionChanged); - } - - /// 清除选择 - public void ClearSelection() - { - SelectionHandle.Clear(); - MarkDirty(TableDirtyType.SelectionChanged); - } - - /// 通过文本选择一个单元格(如 "A0") - public bool SelectCellByText(string text) - { - if (!XTableCoordinateFormat.TryParseCell(text, out int row, out int col)) - return false; - SelectCell(row, col); - return true; - } - - /// 通过文本选择一个范围(如 "A0:B1", "A:A", "0:0") - public bool SelectRangeByText(string text) - { - if (!XTableCoordinateFormat.TryParseRange(text, - out int sr, out int sc, out int er, out int ec)) - return false; - - SelectCell(sr, sc, er, ec); - return true; - } - - /// 获取当前选区的文本描述(单格 "A0",多格 "A0:B1") - public string GetSelectionText() - { - if (!SelectionHandle.HasSelection) return string.Empty; - if (SelectionHandle.HasMultiSelection) - { - return XTableCoordinateFormat.RangeToText( - SelectionHandle.MinRow, SelectionHandle.MinCol, - SelectionHandle.MaxRow, SelectionHandle.MaxCol); - } - - return XTableCoordinateFormat.CellToText( - SelectionHandle.StartRow, SelectionHandle.StartCol); - } - - private void OnHandleSelectionChanged(XTableSelectionHandle handle) - { - MarkDirty(TableDirtyType.SelectionChanged); - } - - /// 裁剪选区到当前行列尺寸范围内 - private void ClampSelectionToBounds() - { - int maxR = (m_RowHeights?.Length ?? 0) - 1; - int maxC = (m_ColWidths?.Length ?? 0) - 1; - SelectionHandle.ClampTo(maxR, maxC); - } - - public void RefreshView() - { - // 编辑模式下 Awake 可能尚未执行,按需回退初始化 - if (m_Assembler == null) - { - if (rectTransform != null) - m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace); - else return; - } - - if (m_RowHeights == null || m_ColWidths == null) return; - if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return; - - CreateBackground(); - -#if UNITY_EDITOR - if (!Application.isPlaying) - Canvas.ForceUpdateCanvases(); -#endif - - RecalculateTotalSize(); - UpdateVisibleRect(); - - // 确保 ScrollRect Content 尺寸正确(含 padding 偏移量,防止 viewport 裁切边缘边框) - RectTransform ctRt = ContentTransform as RectTransform; - if (ctRt != null) - { - float pad2 = m_ContentOffset * 2f; - ctRt.sizeDelta = new Vector2(m_TotalWidth + pad2, m_TotalHeight + pad2); - } - - // 更新背景尺寸(仅覆盖网格区域,无需 padding) - if (m_BackgroundGo != null) - { - var bgRt = m_BackgroundGo.GetComponent(); - bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); - } - - // 归还预制体前,触发所有活跃实例的释放回调 - 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; - Transform borderT = contentT.Find("_TableBorders"); - for (int i = contentT.childCount - 1; i >= 0; i--) - { - var child = contentT.GetChild(i); - if (poolRoot != null && child == poolRoot) continue; - if (prefabPoolRoot != null && child == prefabPoolRoot) continue; - if (m_BackgroundGo != null && child.gameObject == m_BackgroundGo) continue; - if (borderT != null && child == borderT) continue; - - child.gameObject.hideFlags = HideFlags.None; - if (Application.isPlaying) - Destroy(child.gameObject); - else - DestroyImmediate(child.gameObject); - } - - // 渲染可见单元格:优先使用四叉树查询 - if (m_Quadtree != null && m_Quadtree.IsBuilt) - { - var quadrants = new List(); - m_Quadtree.QueryVisible(m_VisibleRect, quadrants); - RenderQuadrants(quadrants); - } - else - { - // 回退:传统行列遍历 - int sr, er, sc, ec; - CalcVisibleRange(out sr, out er, out sc, out ec); - if (er > sr && ec > sc) - RenderCells(sr, er, sc, ec); - } - - // 重建边框线段 - RebuildBorders(); - - // 渲染 Viewport 层冻结单元格 - RenderFrozenCells(); - - m_DirtyFlag.Clear(); - OnTableRefreshed?.Invoke(); - SetChildrenVisible(m_ChildrenVisible); - } - - /// - /// 轻刷新——仅更新可见矩形和可见单元格内容。 - /// 不重建背景、边框、不重置 content.sizeDelta、不做孤儿清理。 - /// 用于滚动和选区变更场景。 - /// - private void RefreshViewLight() - { - if (m_Assembler == null) - { - if (rectTransform != null) - m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace); - else return; - } - - if (m_RowHeights == null || m_ColWidths == null) return; - if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return; - -#if UNITY_EDITOR - if (!Application.isPlaying) - Canvas.ForceUpdateCanvases(); -#endif - - // 1. 回收 CellAssembler 对象(轻量操作:SetActive(false) + 压栈) - m_Assembler.ReturnAll(); - - // 2. 计算新可见单元格集合并回收不再可见的预制体 - HashSet<(int, int)> newVisibleCells; - - if (m_Quadtree != null && m_Quadtree.IsBuilt) - { - var quadrants = new List(); - m_Quadtree.QueryVisible(m_VisibleRect, quadrants); - - newVisibleCells = new HashSet<(int, int)>(); - foreach (var q in quadrants) - { - if (q.Cells != null) - { - foreach (var cell in q.Cells) - newVisibleCells.Add(cell); - } - } - } - else - { - // 回退:计算可见行列范围 - CalcVisibleRange(out int vrSr, out int vrEr, out int vrSc, out int vrEc); - newVisibleCells = new HashSet<(int, int)>(); - for (int vr = vrSr; vr < vrEr; vr++) - for (int vc = vrSc; vc < vrEc; vc++) - newVisibleCells.Add((vr, vc)); - } - - // 回收不再可见的预制体 - if (m_ActivePrefabMap != null && m_ActivePrefabMap.Count > 0) - { - var toRelease = new List<(int, int)>(); - foreach (var kv in m_ActivePrefabMap) - { - if (!newVisibleCells.Contains(kv.Key)) - toRelease.Add(kv.Key); - } - - foreach (var key in toRelease) - { - var data = TableData.GetCell(key.Item1, key.Item2); - data?.OnPrefabRelease?.Invoke(m_ActivePrefabMap[key]); - m_PrefabPool.Release(m_ActivePrefabMap[key]); - m_ActivePrefabMap.Remove(key); - } - } - - // 3. 渲染可见单元格(仅内容 + 选择高亮,无边框/背景) - if (m_Quadtree != null && m_Quadtree.IsBuilt) - { - var quadrants = new List(); - m_Quadtree.QueryVisible(m_VisibleRect, quadrants); - RenderQuadrants(quadrants); - } - else - { - CalcVisibleRange(out int sr, out int er, out int sc, out int ec); - if (er > sr && ec > sc) - RenderCellsLight(sr, er, sc, ec); - } - - // 仅清除滚动和选区脏标记,保留其他标记(如 DataChanged 下次触发重刷新) - m_DirtyFlag.ClearFlags(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged); - OnTableRefreshed?.Invoke(); - SetChildrenVisible(m_ChildrenVisible); - } - - #endregion - - #region 背景 - - private void CreateBackground() - { - if (m_BackgroundGo != null) return; - - m_BackgroundGo = new GameObject("_TableBackground", - typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); - m_BackgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; - m_BackgroundGo.transform.SetParent(ContentTransform, 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 = new Vector2(m_ContentOffset, -m_ContentOffset); - bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); - - var bgImg = m_BackgroundGo.GetComponent(); - bgImg.color = GetCellBgColor(m_DefaultStyleNamespace); - bgImg.raycastTarget = false; - } - - /// 销毁 transform 下所有子对象(用于域重载后清理残留) - private void ClearAllChildren() - { - Transform t = ContentTransform; - if (t == null) return; - -#if UNITY_EDITOR - // 编辑器模式下先重置 HideFlags 再销毁,防止 DontSave 对象拒绝销毁 - while (t.childCount > 0) - { - var child = t.GetChild(0); - child.gameObject.hideFlags = HideFlags.None; - if (Application.isPlaying) - Destroy(child.gameObject); - else - DestroyImmediate(child.gameObject); - } -#else - for (int i = t.childCount - 1; i >= 0; i--) - { - var child = t.GetChild(i); - Destroy(child.gameObject); - } -#endif - } - - /// 设置所有动态子对象的可见性(仅编辑器) - public void SetChildrenVisible(bool visible) - { - m_ChildrenVisible = visible; -#if UNITY_EDITOR - Transform t = ContentTransform; - if (t == null) return; - for (int i = t.childCount - 1; i >= 0; i--) - { - var child = t.GetChild(i); - child.gameObject.hideFlags = visible - ? HideFlags.DontSave - : HideFlags.DontSave | HideFlags.HideInHierarchy; - } -#endif - } - - /// 完全重建表格——销毁所有动态生成的对象并重新初始化 - [ContextMenu("Xeric UI/完全重建表格")] - public void FullRebuild() - { - m_Assembler?.DestroyAll(); - m_PrefabPool?.DestroyAll(); - ClearAllChildren(); - m_BackgroundGo = null; - m_BorderRenderer = null; - - 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(); - CreateViewportRenderer(); - MarkDirty(TableDirtyType.All); - RefreshView(); - SetChildrenVisible(m_ChildrenVisible); - } - - /// 重新生成表格但保留已有文本数据——先保存,重建后再回填 - public void FullRebuildPreservingData() - { - // 保存当前单元格文本 - var savedTexts = new Dictionary<(int, int), string>(); - if (m_TableData != null) - { - int rows = m_RowHeights?.Length ?? 0; - int cols = m_ColWidths?.Length ?? 0; - for (int r = 0; r < rows; r++) - { - for (int c = 0; c < cols; c++) - { - var cell = m_TableData.GetCell(r, c); - if (cell != null && !string.IsNullOrEmpty(cell.Text)) - savedTexts[(r, c)] = cell.Text; - } - } - } - - FullRebuild(); - - // 恢复文本数据 - foreach (var kv in savedTexts) - { - int r = kv.Key.Item1, c = kv.Key.Item2; - if (r < (m_RowHeights?.Length ?? 0) && c < (m_ColWidths?.Length ?? 0)) - m_TableData.GetOrCreateCell(r, c).Text = kv.Value; - } - - RefreshView(); - SetChildrenVisible(m_ChildrenVisible); - } - - /// - /// 清空所有数据(单元格文本、合并信息),但保留行列排布和样式。 - /// 等价于"清空内容但不改变表格结构"。 - /// - public void ClearData() - { - if (m_TableData != null) - { - m_TableData.ClearAllMerges(); - m_TableData.BlockMap.Clear(); - } - m_ActivePrefabMap?.Clear(); - m_PrefabPool?.ReturnAll(); - m_Quadtree?.Clear(); - ClearSelection(); - MarkDirty(TableDirtyType.DataChanged); - RefreshView(); - } - - /// - /// 清空表格所有内容——数据、行列、样式,等同 但不创建默认示例数据。 - /// - public void ClearTable() - { - m_Assembler?.DestroyAll(); - m_PrefabPool?.DestroyAll(); - m_ActivePrefabMap?.Clear(); - m_Quadtree?.Clear(); - ClearAllChildren(); - m_BackgroundGo = null; - m_BorderRenderer = null; - - m_TableData = new XTableData(); - m_RowHeights = null; - m_ColWidths = null; - 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(); - CreateViewportRenderer(); - RefreshView(); - SetChildrenVisible(m_ChildrenVisible); - } - - /// 仅强制刷新样式,不重建数据结构 - public void ForceStyleRefresh() - { - StyleManager.ForceRefresh(); - MarkDirty(TableDirtyType.StyleChanged); - RefreshView(); - } - - /// 构建剪贴板数据对象(尺寸 + 单元格文本 + 合并信息) - public XTableClipboardData BuildClipboardData() - { - return XTableClipboardData.FromTable(this); - } - - /// 将剪贴板数据应用到表格——覆盖尺寸、命名空间、单元格文本、合并信息并刷新 - public void ApplyClipboardData(XTableClipboardData data) - { - if (data == null) return; - - // 确保数据层存在 - if (m_TableData == null) - m_TableData = new XTableData(); - - // 应用尺寸与默认命名空间 - if (data.RowHeights != null && data.RowHeights.Length > 0) - m_RowHeights = data.RowHeights; - if (data.ColWidths != null && data.ColWidths.Length > 0) - m_ColWidths = data.ColWidths; - if (!string.IsNullOrEmpty(data.DefaultStyleNamespace)) - m_DefaultStyleNamespace = data.DefaultStyleNamespace; - - // 写回单元格文本与合并信息 - data.PopulateToTable(this); - - MarkDirty(TableDirtyType.DataChanged); - RecalculateTotalSize(); - RefreshView(); - SetChildrenVisible(m_ChildrenVisible); - } - - #endregion - - #region 单元格渲染 - - private void RenderCells(int sr, int er, int sc, int ec) - { - string ns = m_DefaultStyleNamespace; - - // 编辑器模式:从样式表 pull 所有颜色值 -#if UNITY_EDITOR - if (!Application.isPlaying) - { - Color selectionColor = GetCellSelectionBgColor(ns); - Color borderColor = GetCellBorderColor(ns); - float borderWidth = GetCellBorderWidth(ns); - - 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); - - for (int r = sr; r < er && r < m_RowHeights.Length; r++) - { - for (int c = sc; c < ec && c < m_ColWidths.Length; c++) - { - // 跳过被合并覆盖的单元格 - if (TableData.IsCellMerged(r, c)) continue; - - float cellX = GetColLeftX(c) + m_ContentOffset; - float cellY = -GetRowTopY(r) - m_ContentOffset; - float cellW = m_ColWidths[c]; - float cellH = m_RowHeights[r]; - - // 合并源:扩展尺寸 - if (TableData.IsMergeSource(r, c, out int mergeRS, out int mergeCS)) - { - for (int i = 1; i < mergeCS && c + i < m_ColWidths.Length; i++) - cellW += m_ColWidths[c + i]; - for (int i = 1; i < mergeRS && r + i < m_RowHeights.Length; i++) - cellH += m_RowHeights[r + i]; - } - - XTableCellData data = TableData.GetCell(r, c); - string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) - ? data.StyleNamespace : ns; - - // 多选 / 单选高亮 - if (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 == SelectionHandle.StartRow && c == SelectionHandle.StartCol) - ? focusColor : multiSelectColor; - } - else if (r == SelectionHandle.StartRow && c == SelectionHandle.StartCol) - { - 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; - - switch (data.ContentType) - { - case CellContentType.Text: - if (!string.IsNullOrEmpty(data.Text)) - { - var txt = m_Assembler.GetText("cell_text", cellNS); - txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); - txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); - txt.Text.text = data.Text; - txt.Text.color = GetCellFgColor(cellNS); - txt.Text.fontSize = GetCellFontSize(cellNS); - txt.Text.alignment = TextAlignmentOptions.Midline; - txt.Text.font = GetCellFontAsset(cellNS) ?? txt.Text.font; - } - break; - case CellContentType.Image: - if (data.Image != null) - { - Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage("cell_image", cellNS); - 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) - { - 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); - } - break; - } - } - } - return; - } -#endif - - // 运行时模式 - Color rtSelectionColor = Config.RuntimeSelectionColor; - Color rtMultiSelectColor = Config.RuntimeMultiSelectColor; - Color rtFocusColor = Config.RuntimeFocusColor; - - for (int r = sr; r < er && r < m_RowHeights.Length; r++) - { - for (int c = sc; c < ec && c < m_ColWidths.Length; c++) - { - // 跳过被合并覆盖的单元格 - if (TableData.IsCellMerged(r, c)) continue; - - float cellX = GetColLeftX(c) + m_ContentOffset; - float cellY = -GetRowTopY(r) - m_ContentOffset; - float cellW = m_ColWidths[c]; - float cellH = m_RowHeights[r]; - - // 合并源:扩展尺寸 - if (TableData.IsMergeSource(r, c, out int mergeRS, out int mergeCS)) - { - for (int i = 1; i < mergeCS && c + i < m_ColWidths.Length; i++) - cellW += m_ColWidths[c + i]; - for (int i = 1; i < mergeRS && r + i < m_RowHeights.Length; i++) - cellH += m_RowHeights[r + i]; - } - - XTableCellData data = TableData.GetCell(r, c); - string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) - ? data.StyleNamespace : ns; - - if (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 == SelectionHandle.StartRow && c == SelectionHandle.StartCol) - ? rtFocusColor : rtMultiSelectColor; - } - else if (r == SelectionHandle.StartRow && c == SelectionHandle.StartCol) - { - 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, Config.RuntimeBorderWidth); - hLine.Image.color = Config.RuntimeBorderColor; - - var vLine = m_Assembler.GetImage("cell_vline"); - vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - Config.RuntimeBorderWidth, cellY); - vLine.Rect.sizeDelta = new Vector2(Config.RuntimeBorderWidth, cellH); - vLine.Image.color = Config.RuntimeBorderColor; - - if (data == null) continue; - - switch (data.ContentType) - { - case CellContentType.Text: - if (!string.IsNullOrEmpty(data.Text)) - { - var txt = m_Assembler.GetText("cell_text", cellNS); - txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); - txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); - txt.Text.text = data.Text; - // 运行时样式由 StyleBoundElement 被动更新,此处仅设置文本内容 - } - break; - case CellContentType.Image: - if (data.Image != null) - { - Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage("cell_image", cellNS); - 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) - { - 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); - } - break; - } - } - } - } - - /// - /// 轻渲染——与 RenderCells 相同但跳过网格线 Image 绘制。 - /// 用于滚动场景,网格线由 RebuildBorders 的 UILineRendererV2 统一处理。 - /// - private void RenderCellsLight(int sr, int er, int sc, int ec) - { - string ns = m_DefaultStyleNamespace; - -#if UNITY_EDITOR - if (!Application.isPlaying) - { - Color selectionColor = GetCellSelectionBgColor(ns); - 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); - - for (int r = sr; r < er && r < m_RowHeights.Length; r++) - { - for (int c = sc; c < ec && c < m_ColWidths.Length; c++) - { - if (TableData.IsCellMerged(r, c)) continue; - - float cellX = GetColLeftX(c) + m_ContentOffset; - float cellY = -GetRowTopY(r) - m_ContentOffset; - float cellW = m_ColWidths[c]; - float cellH = m_RowHeights[r]; - - if (TableData.IsMergeSource(r, c, out int mergeRS, out int mergeCS)) - { - for (int i = 1; i < mergeCS && c + i < m_ColWidths.Length; i++) - cellW += m_ColWidths[c + i]; - for (int i = 1; i < mergeRS && r + i < m_RowHeights.Length; i++) - cellH += m_RowHeights[r + i]; - } - - XTableCellData data = TableData.GetCell(r, c); - string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) - ? data.StyleNamespace : ns; - - // 选择高亮(无网格线) - if (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 == SelectionHandle.StartRow && c == SelectionHandle.StartCol) - ? focusColor : multiSelectColor; - } - else if (r == SelectionHandle.StartRow && c == SelectionHandle.StartCol) - { - 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; - } - - if (data == null) continue; - - switch (data.ContentType) - { - case CellContentType.Text: - if (!string.IsNullOrEmpty(data.Text)) - { - var txt = m_Assembler.GetText("cell_text", cellNS); - txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); - txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); - txt.Text.text = data.Text; - txt.Text.color = GetCellFgColor(cellNS); - txt.Text.fontSize = GetCellFontSize(cellNS); - txt.Text.alignment = TextAlignmentOptions.Midline; - txt.Text.font = GetCellFontAsset(cellNS) ?? txt.Text.font; - } - break; - case CellContentType.Image: - if (data.Image != null) - { - Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage("cell_image", cellNS); - 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) - { - 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); - } - break; - } - } - } - return; - } -#endif - - // 运行时模式(无网格线) - Color rtSelectionColor = Config.RuntimeSelectionColor; - Color rtMultiSelectColor = Config.RuntimeMultiSelectColor; - Color rtFocusColor = Config.RuntimeFocusColor; - - for (int r = sr; r < er && r < m_RowHeights.Length; r++) - { - for (int c = sc; c < ec && c < m_ColWidths.Length; c++) - { - if (TableData.IsCellMerged(r, c)) continue; - - float cellX = GetColLeftX(c) + m_ContentOffset; - float cellY = -GetRowTopY(r) - m_ContentOffset; - float cellW = m_ColWidths[c]; - float cellH = m_RowHeights[r]; - - if (TableData.IsMergeSource(r, c, out int mergeRS, out int mergeCS)) - { - for (int i = 1; i < mergeCS && c + i < m_ColWidths.Length; i++) - cellW += m_ColWidths[c + i]; - for (int i = 1; i < mergeRS && r + i < m_RowHeights.Length; i++) - cellH += m_RowHeights[r + i]; - } - - XTableCellData data = TableData.GetCell(r, c); - string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) - ? data.StyleNamespace : ns; - - if (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 == SelectionHandle.StartRow && c == SelectionHandle.StartCol) - ? rtFocusColor : rtMultiSelectColor; - } - else if (r == SelectionHandle.StartRow && c == SelectionHandle.StartCol) - { - 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; - } - - if (data == null) continue; - - switch (data.ContentType) - { - case CellContentType.Text: - if (!string.IsNullOrEmpty(data.Text)) - { - var txt = m_Assembler.GetText("cell_text", cellNS); - txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); - txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); - txt.Text.text = data.Text; - } - break; - case CellContentType.Image: - if (data.Image != null) - { - Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage("cell_image", cellNS); - 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) - { - 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); - } - break; - } - } - } - } - - #endregion - - #region 坐标计算 - - /// 判断指定单元格是否与当前可见矩形有重叠(用于 RenderQuadrants 裁剪) - private bool IsCellVisible(int row, int col) - { - float cellTop = GetRowTopY(row); - float cellBottom = cellTop + m_RowHeights[row]; - float cellLeft = GetColLeftX(col); - float cellRight = cellLeft + m_ColWidths[col]; - - return cellBottom > m_VisibleRect.y && cellTop < m_VisibleRect.yMax && - cellRight > m_VisibleRect.x && cellLeft < m_VisibleRect.xMax; - } - - private float GetRowTopY(int row) - { - float y = 0; - for (int i = 0; i < row && i < m_RowHeights.Length; i++) - y += m_RowHeights[i]; - return y; - } - - private float GetColLeftX(int col) - { - float x = 0; - for (int i = 0; i < col && i < m_ColWidths.Length; i++) - x += m_ColWidths[i]; - return x; - } - - /// 将 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; - } - return sprite; - } - - private void RecalculateTotalSize() - { - m_TotalWidth = 0; - if (m_ColWidths != null) - foreach (float w in m_ColWidths) - m_TotalWidth += w; - - m_TotalHeight = 0; - if (m_RowHeights != null) - foreach (float h in m_RowHeights) - m_TotalHeight += h; - - m_ContentOffset = GetCellBorderWidth(m_DefaultStyleNamespace) * 0.5f; - float pad = m_ContentOffset * 2f; - - if (m_ScrollRect != null && m_ScrollRect.content != null) - m_ScrollRect.content.sizeDelta = new Vector2( - m_TotalWidth + pad, m_TotalHeight + pad); - } - - private void UpdateVisibleRect() - { - m_VisibleRect = new Rect(0, 0, m_TotalWidth, m_TotalHeight); - - if (m_ScrollRect != null && m_ScrollRect.viewport != null) - { - RectTransform vt = m_ScrollRect.viewport; - RectTransform ct = m_ScrollRect.content; - if (vt != null && ct != null) - { - float sx = Mathf.Max(0, -ct.anchoredPosition.x); - float sy = Mathf.Max(0, -ct.anchoredPosition.y); - m_VisibleRect = new Rect(sx, sy, vt.rect.width, vt.rect.height); - } - } - } - - private void CalcVisibleRange(out int sr, out int er, out int sc, out int ec) - { - sr = 0; - er = 0; - sc = 0; - ec = 0; - - float accY = 0; - for (int r = 0; r < m_RowHeights.Length; r++) - { - float rowBottom = accY + m_RowHeights[r]; - if (rowBottom > m_VisibleRect.y && accY < m_VisibleRect.yMax) - { - if (er == 0 && sr == 0) sr = r; - er = r + 1; - } - - accY = rowBottom; - if (accY > m_VisibleRect.yMax) break; - } - - float accX = 0; - for (int c = 0; c < m_ColWidths.Length; c++) - { - float colRight = accX + m_ColWidths[c]; - if (colRight > m_VisibleRect.x && accX < m_VisibleRect.xMax) - { - if (ec == 0 && sc == 0) sc = c; - ec = c + 1; - } - - accX = colRight; - if (accX > m_VisibleRect.xMax) break; - } - - if (sr < 0) sr = 0; - if (sc < 0) sc = 0; - } - - #endregion - - #region 样式 - - private Color GetStyleColor(string ns, string path, Color fallback) - { - StyleValue val = StyleManager.GetValue(ns, path); - return val != null ? (Color)val : fallback; - } - - private int GetStyleInt(string ns, string path, int fallback) - { - StyleValue val = StyleManager.GetValue(ns, path); - return val != null ? (int)val : fallback; - } - - private float GetStyleFloat(string ns, string path, float fallback) - { - StyleValue val = StyleManager.GetValue(ns, path); - return val != null ? (float)val : fallback; - } - - private TMP_FontAsset GetStyleFontAsset(string ns, string path, TMP_FontAsset fallback = null) - { - StyleValue val = StyleManager.GetValue(ns, path); - if (val == null) return fallback; - try { return val.GetValueAs(); } - catch { return fallback; } - } - - private void EnsureDefaultStyleNamespace() - { - // 检查命名空间是否已存在,避免 HasStyle → GetValue 对不存在的命名空间报错 - bool nsExists = StyleManager.GetAvailableNamespaces()?.Contains(m_DefaultStyleNamespace) == true; - - // 命名空间已存在且样式已加载(来自 .xsss),无需添加回退值 - if (nsExists && StyleManager.HasStyle(m_DefaultStyleNamespace, STYLE_CELL_BG_COLOR)) - return; - - // 回退默认值匹配 DefaultTableStyles.xsss - // AddDynamicStyle 自动创建命名空间(如果尚不存在) - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BG_COLOR, DEFAULT_BG_COLOR); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_FG_COLOR, DEFAULT_FG_COLOR); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_TOP_COLOR, DEFAULT_BORDER_COLOR); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_TOP_WIDTH, DEFAULT_BORDER_WIDTH); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_BOTTOM_COLOR, DEFAULT_BORDER_COLOR); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_BOTTOM_WIDTH, DEFAULT_BORDER_WIDTH); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_LEFT_COLOR, DEFAULT_BORDER_COLOR); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_LEFT_WIDTH, DEFAULT_BORDER_WIDTH); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_RIGHT_COLOR, DEFAULT_BORDER_COLOR); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_BORDER_RIGHT_WIDTH, DEFAULT_BORDER_WIDTH); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_SELECTION_BG_COLOR, DEFAULT_SELECTION_COLOR); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_FONT_SIZE, DEFAULT_FONT_SIZE); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_FONT_ALIGNMENT, DEFAULT_FONT_ALIGNMENT); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_FONT_RICH_TEXT, DEFAULT_FONT_RICH_TEXT); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_DEFAULT_HEIGHT, DEFAULT_CELL_HEIGHT); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, STYLE_CELL_DEFAULT_WIDTH, DEFAULT_CELL_WIDTH); - StyleManager.ForceRefresh(); - } - - #endregion - - #region 其他 + #region 脏标记与滚动回调 public void MarkDirty(TableDirtyType type) { @@ -2016,9 +379,8 @@ namespace XericUI.XTable.Rendering.Component try { - // 滚动时仅做轻刷新:更新可见矩形 + 渲染可见单元格内容, + // 滚动时仅做轻刷新(RefreshViewLight 内部会调用 UpdateVisibleRect), // 不重建背景、边框、不重置 content.sizeDelta、不做孤儿清理。 - UpdateVisibleRect(); RefreshViewLight(); } finally @@ -2029,337 +391,6 @@ namespace XericUI.XTable.Rendering.Component #endregion - #region 四叉树 - - /// 强制重建四叉树(外部调用入口,适合编辑器调试或手动刷新) - public void ForceRebuildQuadtree() - { - if (m_Quadtree == null) - m_Quadtree = new XTableQuadtree(); - MaintainQuadtree(); - } - - /// 从当前 XTableData 收集单元格信息并重建四叉树 - private void MaintainQuadtree() - { - if (m_TableData == null || m_RowHeights == null || m_ColWidths == null) - return; - if (m_Quadtree == null) - m_Quadtree = new XTableQuadtree(); - - // 收集所有有数据的单元格(跳过被合并覆盖的子单元格) - var cells = new List(); - for (int r = 0; r < m_RowHeights.Length; r++) - { - for (int c = 0; c < m_ColWidths.Length; c++) - { - if (m_TableData.IsCellMerged(r, c)) continue; - - var data = m_TableData.GetCell(r, c); - if (data == null) continue; - - string ns = !string.IsNullOrEmpty(data.StyleNamespace) - ? data.StyleNamespace : m_DefaultStyleNamespace; - cells.Add(new XTableQuadtreeCell { Row = r, Col = c, Namespace = ns }); - } - } - - m_Quadtree.Build(m_TotalWidth, m_TotalHeight, m_RowHeights, m_ColWidths, cells, Config.QuadtreeMaxCellsPerNode); - } - - #endregion - - #region 边框渲染 - - /// - /// 获取或创建边框渲染器 GameObject 和 组件。 - /// - private UILineRendererV2 GetOrCreateBorderRenderer() - { - if (m_BorderRenderer != null) - return m_BorderRenderer; - - Transform contentT = ContentTransform; - // 查找已有对象 - var existing = contentT.Find("_TableBorders"); - if (existing != null) - { - m_BorderRenderer = existing.GetComponent(); - if (m_BorderRenderer != null) return m_BorderRenderer; - } - - var go = new GameObject("_TableBorders", - typeof(RectTransform), typeof(CanvasRenderer), typeof(UILineRendererV2)); - go.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; - go.transform.SetParent(contentT, false); - - RectTransform rt = go.GetComponent(); - rt.anchorMin = new Vector2(0, 1); - rt.anchorMax = new Vector2(0, 1); - rt.pivot = new Vector2(0, 1); - float pad = m_ContentOffset * 2f; - rt.anchoredPosition = new Vector2(m_ContentOffset, -m_ContentOffset); - rt.sizeDelta = new Vector2(m_TotalWidth + pad, m_TotalHeight + pad); - - m_BorderRenderer = go.GetComponent(); - return m_BorderRenderer; - } - - /// 销毁边框渲染器 - private void DestroyBorderRenderer() - { - if (m_BorderRenderer != null) - { - if (Application.isPlaying) - Destroy(m_BorderRenderer.gameObject); - else - DestroyImmediate(m_BorderRenderer.gameObject); - m_BorderRenderer = null; - } - } - - /// - /// 重建表格的全部边框线段。 - /// 第一阶段:从 OuterBorderNs 绘制 -1 行(顶部外框)和 -1 列(左侧外框)。 - /// 第二阶段:遍历非合并覆盖单元格,从边框缓存读取样式命名空间,按 per-cell 样式绘制右侧和底部边框。 - /// 合并源在最外沿绘制,内部边框自动跳过。 - /// - private void RebuildBorders() - { - if (m_RowHeights == null || m_ColWidths == null) return; - if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return; - - var renderer = GetOrCreateBorderRenderer(); - renderer.ClearAll(); - - var borderRt = renderer.GetComponent(); - borderRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); - -#if UNITY_EDITOR - Color borderColor; - float borderWidth; - if (!Application.isPlaying) - { - borderColor = GetCellBorderColor(m_DefaultStyleNamespace); - borderWidth = GetCellBorderWidth(m_DefaultStyleNamespace); - } - else - { - borderColor = Config.RuntimeBorderColor; - borderWidth = Config.RuntimeBorderWidth; - } -#else - Color borderColor = Config.RuntimeBorderColor; - float borderWidth = Config.RuntimeBorderWidth; -#endif - - int rows = m_RowHeights.Length; - int cols = m_ColWidths.Length; - - // ════════════════════════════════════════════ - // 阶段 1:-1 行(顶部外框)和 -1 列(左侧外框) - // ════════════════════════════════════════════ - - // 顶部外框:从 col=0 到 col=cols-1,每列一段 - float topY = 0; - for (int c = 0; c < cols; c++) - { - string ns = m_TableData?.GetOuterBorderNs(-1, c); - if (string.IsNullOrEmpty(ns)) ns = m_DefaultStyleNamespace; - Color color = GetCellBorderColor(ns); - float width = GetCellBorderWidth(ns); - float leftX = GetColLeftX(c); - renderer.DrawLine( - new Vector2(leftX, topY), - new Vector2(leftX + m_ColWidths[c], topY), - color, width, UVMode.ByDistance); - } - - // 左侧外框:从 row=0 到 row=rows-1,每行一段 - float xEdge = 0; - for (int r = 0; r < rows; r++) - { - string ns = m_TableData?.GetOuterBorderNs(r, -1); - if (string.IsNullOrEmpty(ns)) ns = m_DefaultStyleNamespace; - Color color = GetCellBorderColor(ns); - float width = GetCellBorderWidth(ns); - float rowTopY = -GetRowTopY(r); - renderer.DrawLine( - new Vector2(xEdge, rowTopY), - new Vector2(xEdge, rowTopY - m_RowHeights[r]), - color, width, UVMode.ByDistance); - } - - // ════════════════════════════════════════════ - // 阶段 2:每个单元格的右侧和底部边框 - // ════════════════════════════════════════════ - - for (int r = 0; r < rows; r++) - { - for (int c = 0; c < cols; c++) - { - // 跳过被合并覆盖的单元格 - if (m_TableData != null && m_TableData.IsCellMerged(r, c)) - continue; - - float cellX = GetColLeftX(c); - float cellY = -GetRowTopY(r); - float cellW = m_ColWidths[c]; - float cellH = m_RowHeights[r]; - - int mergeRS = 1, mergeCS = 1; - if (m_TableData != null) - m_TableData.IsMergeSource(r, c, out mergeRS, out mergeCS); - - // 扩展合并尺寸 - for (int i = 1; i < mergeCS && c + i < cols; i++) - cellW += m_ColWidths[c + i]; - for (int i = 1; i < mergeRS && r + i < rows; i++) - cellH += m_RowHeights[r + i]; - - float rightX = cellX + cellW; - float bottomY = cellY - cellH; - - // —— 右侧边框 —— - string rightNs = m_TableData?.GetBorderRightNs(r, c); - if (string.IsNullOrEmpty(rightNs)) rightNs = m_DefaultStyleNamespace; - Color rightColor = GetCellBorderColor(rightNs); - float rightWidth = GetCellBorderWidth(rightNs); - - int rightCol = c + mergeCS; - if (rightCol <= cols) - { - renderer.DrawLine( - new Vector2(rightX, cellY), - new Vector2(rightX, bottomY), - rightColor, rightWidth, UVMode.ByDistance); - } - - // —— 底部边框 —— - string bottomNs = m_TableData?.GetBorderBottomNs(r, c); - if (string.IsNullOrEmpty(bottomNs)) bottomNs = m_DefaultStyleNamespace; - Color bottomColor = GetCellBorderColor(bottomNs); - float bottomWidth = GetCellBorderWidth(bottomNs); - - int bottomRow = r + mergeRS; - if (bottomRow <= rows) - { - renderer.DrawLine( - new Vector2(cellX, bottomY), - new Vector2(rightX, bottomY), - bottomColor, bottomWidth, UVMode.ByDistance); - } - } - } - } - - #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; - - // 共享预制体对象池 - m_ViewportRenderer.PrefabPool = m_PrefabPool; - m_ViewportRenderer.ActivePrefabMap = m_ActivePrefabMap; - m_ViewportRenderer.CellPadding = Config.CellHorizontalPadding; - m_ViewportRenderer.Config = Config; - } - - /// 渲染 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) @@ -2410,4 +441,4 @@ namespace XericUI.XTable.Rendering.Component #endregion } -} \ No newline at end of file +} diff --git a/Runtime/XTable/Rendering/Elements/CellAssembler.cs b/Runtime/XTable/Rendering/Elements/CellAssembler.cs index a9da1d5..6faee5c 100644 --- a/Runtime/XTable/Rendering/Elements/CellAssembler.cs +++ b/Runtime/XTable/Rendering/Elements/CellAssembler.cs @@ -195,6 +195,22 @@ namespace XericUI.XTable.Rendering.Elements m_ActiveTexts.Clear(); } + /// 归还单个 Image 池对象(从活跃列表移除并推入休眠栈) + public void ReleaseImage(PooledImage item) + { + if (item == null) return; + m_ActiveImages.Remove(item); + ReturnImage(item); + } + + /// 归还单个 Text 池对象(从活跃列表移除并推入休眠栈) + public void ReleaseText(PooledText item) + { + if (item == null) return; + m_ActiveTexts.Remove(item); + ReturnText(item); + } + private void ReturnImage(PooledImage item) { item.Style?.Unbind();