优化表格性能,拆分代码文件职责

This commit is contained in:
2026-07-28 08:26:58 +08:00
parent a948de308e
commit 28a850e342
15 changed files with 1868 additions and 1992 deletions
@@ -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
{
/// <summary>设置冻结行列数并重建视图</summary>
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);
}
/// <summary>设置文本内容(ContentType 自动设为 Text)</summary>
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;
}
/// <summary>设置图片纹理(ContentType 自动设为 Image)</summary>
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;
}
/// <summary>设置预制体(ContentType 自动设为 Prefab)</summary>
/// <remarks>注意是设置预制体而不是实例,设置后订阅单元格预制体生成事件 OnPrefabAcquire 处理获取事件</remarks>
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;
}
/// <summary>设置单元格的样式命名空间,同时写入边框缓存(谁最后设置归谁)</summary>
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;
}
/// <summary>设置单元格的样式命名空间(兼容旧版,等同于 StyleNamespace 字段)</summary>
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);
}
/// <summary>在单个单元格上设置样式,同时写入其右侧和底部的边框缓存</summary>
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);
}
/// <summary>清除文本内容</summary>
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);
}
/// <summary>清除图片纹理</summary>
public void ClearCellImage(int row, int col)
{
var data = TableData.GetCell(row, col);
if (data == null) return;
data.Image = null;
MarkDirty(TableDirtyType.DataChanged);
}
/// <summary>清除预制体及委托</summary>
public void ClearCellPrefab(int row, int col)
{
var data = TableData.GetCell(row, col);
if (data == null) return;
data.Prefab = null;
data.OnPrefabAcquire = null;
data.OnPrefabRelease = null;
MarkDirty(TableDirtyType.DataChanged);
}
/// <summary>清除单元格样式覆盖</summary>
public void 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);
/// <summary>获取单元格的样式命名空间(含回退逻辑)</summary>
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);
}
/// <summary>判断指定单元格是否被合并覆盖(非源,会被重定向)</summary>
public bool IsCellMerged(int row, int col) => TableData.IsCellMerged(row, col);
/// <summary>判断指定单元格是否为合并源(锚点),并返回合并跨度</summary>
public bool IsMergeSource(int row, int col, out int rowSpan, out int colSpan)
=> TableData.IsMergeSource(row, col, out rowSpan, out colSpan);
/// <summary>在末尾追加一行,默认行高来自 Config</summary>
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
}
/// <summary>在末尾追加一列,默认列宽来自 Config</summary>
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
}
/// <summary>设置指定行的高度</summary>
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
}
/// <summary>设置指定列的宽度</summary>
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
}
/// <summary>删除指定行,单元格数据上移</summary>
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
}
/// <summary>删除指定列,单元格数据左移</summary>
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
}
/// <summary>设置外部选择句柄(替换内部句柄)</summary>
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);
}
/// <summary>单选一个单元格</summary>
public void SelectCell(int row, int col)
{
SelectionHandle.Select(row, col);
MarkDirty(TableDirtyType.SelectionChanged);
}
/// <summary>多选一个范围</summary>
public void SelectCell(int startRow, int startCol, int endRow, int endCol)
{
SelectionHandle.Select(startRow, startCol, endRow, endCol);
MarkDirty(TableDirtyType.SelectionChanged);
}
/// <summary>清除选择</summary>
public void ClearSelection()
{
SelectionHandle.Clear();
MarkDirty(TableDirtyType.SelectionChanged);
}
/// <summary>通过文本选择一个单元格(如 "A0")</summary>
public bool SelectCellByText(string text)
{
if (!XTableCoordinateFormat.TryParseCell(text, out int row, out int col))
return false;
SelectCell(row, col);
return true;
}
/// <summary>通过文本选择一个范围(如 "A0:B1", "A:A", "0:0")</summary>
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;
}
/// <summary>获取当前选区的文本描述(单格 "A0",多格 "A0:B1")</summary>
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);
}
/// <summary>裁剪选区到当前行列尺寸范围内</summary>
private void ClampSelectionToBounds()
{
int maxR = (m_RowHeights?.Length ?? 0) - 1;
int maxC = (m_ColWidths?.Length ?? 0) - 1;
SelectionHandle.ClampTo(maxR, maxC);
}
/// <summary>完全重建表格——销毁所有动态生成的对象并重新初始化</summary>
[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);
}
/// <summary>重新生成表格但保留已有文本数据——先保存,重建后再回填</summary>
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);
}
/// <summary>
/// 清空所有数据(单元格文本、合并信息),但保留行列排布和样式。
/// 等价于"清空内容但不改变表格结构"。
/// </summary>
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();
}
/// <summary>
/// 清空表格所有内容——数据、行列、样式,等同 <see cref="FullRebuild"/> 但不创建默认示例数据。
/// </summary>
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);
}
/// <summary>仅强制刷新样式,不重建数据结构</summary>
public void ForceStyleRefresh()
{
StyleManager.ForceRefresh();
MarkDirty(TableDirtyType.StyleChanged);
RefreshView();
}
/// <summary>构建剪贴板数据对象(尺寸 + 单元格文本 + 合并信息)</summary>
public XTableClipboardData BuildClipboardData()
{
return XTableClipboardData.FromTable(this);
}
/// <summary>将剪贴板数据应用到表格——覆盖尺寸、命名空间、单元格文本、合并信息并刷新</summary>
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);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c49b16a58833a040ab95beac397acb2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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
{
/// <summary>
/// 获取或创建边框渲染器 GameObject 和 <see cref="UILineRendererV2"/> 组件。
/// </summary>
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<UILineRendererV2>();
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<RectTransform>();
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<UILineRendererV2>();
return m_BorderRenderer;
}
/// <summary>销毁边框渲染器</summary>
private void DestroyBorderRenderer()
{
if (m_BorderRenderer != null)
{
if (Application.isPlaying)
Destroy(m_BorderRenderer.gameObject);
else
DestroyImmediate(m_BorderRenderer.gameObject);
m_BorderRenderer = null;
}
}
/// <summary>
/// 重建表格的全部边框线段。
/// 第一阶段:从 OuterBorderNs 绘制 -1 行(顶部外框)和 -1 列(左侧外框)。
/// 第二阶段:遍历非合并覆盖单元格,从边框缓存读取样式命名空间,按 per-cell 样式绘制右侧和底部边框。
/// 合并源在最外沿绘制,内部边框自动跳过。
/// </summary>
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<RectTransform>();
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);
}
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8357070a070775e42954b1783029541e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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
{
/// <summary>
/// 表格数据与坐标计算——可见性判定、坐标公式、尺寸重算、四叉树维护。
/// </summary>
public partial class XericUIActionTable
{
#region 可见性判定
/// <summary>判断指定单元格是否与当前可见矩形有重叠(用于裁剪剔除)</summary>
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 缓存
/// <summary>将 Texture2D 转为 Sprite(带缓存),用于 Image 类型单元格渲染</summary>
private Sprite GetOrCreateSprite(Texture2D texture)
{
if (texture == null) return null;
if (m_SpriteCache == null)
m_SpriteCache = new Dictionary<Texture2D, Sprite>();
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 四叉树
/// <summary>强制重建四叉树(外部调用入口,适合编辑器调试或手动刷新)</summary>
public void ForceRebuildQuadtree()
{
if (m_Quadtree == null)
m_Quadtree = new XTableQuadtree();
MaintainQuadtree();
}
/// <summary>从当前 XTableData 收集单元格信息并重建四叉树</summary>
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<XTableQuadtreeCell>();
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
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af4d46cbfa5975f4da50bee5277e679e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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
/// <summary>销毁 transform 下所有子对象(用于域重载后清理残留)</summary>
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
}
/// <summary>设置所有动态子对象的可见性(仅编辑器)</summary>
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
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b8508d58614894145a342cc75c9bc1d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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 冻结渲染
/// <summary>创建或重新创建 Viewport 层冻结渲染器</summary>
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;
}
/// <summary>渲染 Viewport 层冻结单元格,随滚动偏移调整位置</summary>
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<RectTransform>();
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
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8dc33902374469e4aac60318ee862346
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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();
/// <summary>可复用列表,避免每帧分配 GC</summary>
[NonSerialized] private List<(int, int)> m_ReusableCellList = new(capacity: 128);
[NonSerialized] private List<QuadrantResult> m_ReusableQuadrants = new(capacity: 32);
/// <summary>控制滚动时是否使用四叉树(大表格推荐)</summary>
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 可见单元格计算
/// <summary>计算当前可见矩形内所有未被合并覆盖的单元格</summary>
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));
}
}
}
/// <summary>[Scroll] 仅用简单范围遍历计算可见单元格(避免四叉树遍历及 QuadrantResult 分配)</summary>
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 单元格内容渲染(统一入口)
/// <summary>渲染单个单元格的完整内容(选择高亮 + 网格线 + 文本/图片/预制体)。</summary>
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<RectTransform>();
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<RectTransform>();
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<RectTransform>();
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<Image>();
bgImg.color = GetCellBgColor(m_DefaultStyleNamespace);
bgImg.raycastTarget = false;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7336cd39fbd4ade469db004e3b950446
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -5,8 +5,7 @@ using XericLibrary.Runtime.SuperStyleSheet;
namespace XericUI.XTable.Rendering.Component
{
/// <summary>
/// 表格样式参数——统一管理所有样式路径与回退值。
/// 修改此处的常量即可全局调整表格外观,无需在各方法中查找硬编码字符串。
/// 表格样式参数——统一管理所有样式路径、回退值、样式取值方法与默认命名空间初始化。
/// </summary>
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<TMP_FontAsset>(); }
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
}
}
File diff suppressed because it is too large Load Diff
@@ -195,6 +195,22 @@ namespace XericUI.XTable.Rendering.Elements
m_ActiveTexts.Clear();
}
/// <summary>归还单个 Image 池对象(从活跃列表移除并推入休眠栈)</summary>
public void ReleaseImage(PooledImage item)
{
if (item == null) return;
m_ActiveImages.Remove(item);
ReturnImage(item);
}
/// <summary>归还单个 Text 池对象(从活跃列表移除并推入休眠栈)</summary>
public void ReleaseText(PooledText item)
{
if (item == null) return;
m_ActiveTexts.Remove(item);
ReturnText(item);
}
private void ReturnImage(PooledImage item)
{
item.Style?.Unbind();