修复表格冻结

This commit is contained in:
2026-07-29 10:10:46 +08:00
parent 98c2173ade
commit 5c51d35d8e
9 changed files with 453 additions and 551 deletions
@@ -4,449 +4,253 @@ using UnityEngine;
using UnityEngine.UI;
using XericUI.XTable.Core;
using XericUI.XTable.Rendering.Elements;
using XericLibrary.Runtime.SuperStyleSheet;
namespace XericUI.XTable.Rendering.Component
{
/// <summary>
/// 表格渲染管线封装——将 CellAssembler 对象池、背景创建、单元格渲染抽离为可复用单元。
/// 同一表格可持有多个 TableRenderer 实例,分别渲染到不同父节点(如 Content 和 Viewport)。
/// </summary>
public class TableRenderer
/// <summary>指定父级和索引范围的表格区域渲染器。池由表格全局持有,区域仅持有可见对象追踪。</summary>
public sealed class TableRenderer
{
#region 内部字段
private struct Tracked
{
public CellAssembler.PooledText Text;
public CellAssembler.PooledImage Image;
public CellAssembler.PooledImage Selection;
}
private Transform m_Parent;
private CellAssembler m_Assembler;
private GameObject m_BackgroundGo;
private readonly Transform m_Parent;
private readonly Dictionary<(int, int), Tracked> m_Tracked = new();
private readonly HashSet<(int, int)> m_Rendered = new();
private readonly Dictionary<Texture2D, Sprite> m_SpriteCache = new();
private GameObject m_BackgroundGo;
/// <summary>共享预制体对象池引用(由 XericUIActionTable 传入)</summary>
public PrefabPrototypePool PrefabPool;
public CellAssembler Assembler;
public PrefabPrototypePool PrefabPool;
public Dictionary<(int, int), GameObject> ActivePrefabMap;
public float CellPadding;
public XTableConfig Config;
/// <summary>共享活跃预制体映射表(由 XericUIActionTable 传入)</summary>
public Dictionary<(int, int), GameObject> ActivePrefabMap;
/// <summary>Texture2D → Sprite 缓存,避免每帧分配</summary>
private Dictionary<Texture2D, Sprite> m_SpriteCache;
/// <summary>单元格内容水平内边距(单侧),由 XericUIActionTable 设置</summary>
public float CellPadding;
private float CellPadding2 => CellPadding * 2f;
/// <summary>表格全局配置资源,由 XericUIActionTable 设置</summary>
public XTableConfig Config;
#endregion
#region 公开属性
public CellAssembler Assembler => m_Assembler;
public GameObject BackgroundGo => m_BackgroundGo;
public Transform Parent => m_Parent;
/// <summary>起始行偏移(Viewport 渲染器通常为 0;Content 渲染器通常为 freezeRowCount)</summary>
public int StartRow;
/// <summary>起始列偏移</summary>
public int StartCol;
/// <summary>终止行(不含),0 表示不受限</summary>
public int EndRow;
/// <summary>终止列(不含),0 表示不受限</summary>
public int EndCol;
#endregion
#region 样式委托
/// <summary>从样式表读取颜色值</summary>
public System.Func<string, string, Color, Color> GetStyleColor;
/// <summary>从样式表读取浮点值</summary>
public System.Func<string, string, float, float> GetStyleFloat;
/// <summary>从样式表读取整数值</summary>
public System.Func<string, string, int, int> GetStyleInt;
/// <summary>从样式表读取 TMP_FontAsset</summary>
public System.Func<string, string, TMP_FontAsset> GetStyleFontAsset;
/// <summary>获取行高数组</summary>
public System.Func<float[]> GetRowHeights;
/// <summary>获取列宽数组</summary>
public System.Func<float[]> GetColWidths;
/// <summary>获取表格数据</summary>
public System.Func<XTableData> GetTableData;
/// <summary>默认样式命名空间</summary>
public System.Func<string> GetDefaultStyleNamespace;
/// <summary>判断单元格是否在多选范围内</summary>
public System.Func<int, int, bool> IsInSelectionRange;
/// <summary>获取选中的起始行列</summary>
public System.Func<int> GetSelectedRow;
/// <summary>获取选中的起始列</summary>
public System.Func<int> GetSelectedCol;
/// <summary>运行时样式变更回调</summary>
public event System.Action OnStyleChangeDetected;
public Transform Parent => m_Parent;
public GameObject BackgroundGo => m_BackgroundGo;
#endregion
#region 构造 / 销毁
public TableRenderer(Transform parent, string defaultStyleNamespace = null,
System.Action styleChangeHandler = null)
public TableRenderer(Transform parent)
{
m_Parent = parent;
m_Assembler = new CellAssembler(parent, defaultStyleNamespace);
if (styleChangeHandler != null)
m_Assembler.OnStyleChangeDetected += styleChangeHandler;
}
/// <summary>彻底销毁渲染器中所有动态生成的对象</summary>
public void DestroyAll()
{
m_Assembler?.DestroyAll();
if (m_BackgroundGo != null)
{
if (Application.isPlaying)
Object.Destroy(m_BackgroundGo);
else
Object.DestroyImmediate(m_BackgroundGo);
m_BackgroundGo = null;
}
ReleaseAll();
if (m_BackgroundGo == null) return;
if (Application.isPlaying) Object.Destroy(m_BackgroundGo);
else Object.DestroyImmediate(m_BackgroundGo);
m_BackgroundGo = null;
}
#endregion
#region 背景
/// <summary>创建或获取背景 Image(锚定左上角,覆盖整个渲染范围)</summary>
public void CreateBackground(float totalWidth, float totalHeight, string styleNamespace)
public void CreateBackground(float width, float height, string styleNamespace, Vector2 position = default)
{
if (m_BackgroundGo != null)
if (m_BackgroundGo == null)
{
var rt = m_BackgroundGo.GetComponent<RectTransform>();
rt.sizeDelta = new Vector2(totalWidth, totalHeight);
m_BackgroundGo = new GameObject("_TableBackground", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
m_BackgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
m_BackgroundGo.transform.SetParent(m_Parent, false);
m_BackgroundGo.transform.SetAsFirstSibling();
var image = m_BackgroundGo.GetComponent<Image>();
image.raycastTarget = false;
image.color = GetStyleColor?.Invoke(styleNamespace, XericUIActionTable.STYLE_CELL_BG_COLOR,
XericUIActionTable.BG_COLOR_FALLBACK) ?? XericUIActionTable.BG_COLOR_FALLBACK;
}
var rt = m_BackgroundGo.GetComponent<RectTransform>();
rt.anchorMin = new Vector2(0, 1);
rt.anchorMax = new Vector2(0, 1);
rt.pivot = new Vector2(0, 1);
rt.anchoredPosition = position;
rt.sizeDelta = new Vector2(width, height);
}
public void Render(int sr, int er, int sc, int ec, bool editorMode, bool fullRefresh)
{
var rows = GetRowHeights?.Invoke();
var cols = GetColWidths?.Invoke();
if (Assembler == null || rows == null || cols == null) return;
sr = Mathf.Max(sr, StartRow);
sc = Mathf.Max(sc, StartCol);
er = Mathf.Min(er, EndRow > 0 ? EndRow : rows.Length);
ec = Mathf.Min(ec, EndCol > 0 ? EndCol : cols.Length);
if (sr >= er || sc >= ec)
{
if (fullRefresh) ReleaseAll();
return;
}
m_BackgroundGo = new GameObject("_TableBackground",
typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
m_BackgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
m_BackgroundGo.transform.SetParent(m_Parent, false);
m_BackgroundGo.transform.SetAsFirstSibling();
var target = new HashSet<(int, int)>();
for (int r = sr; r < er; r++)
for (int c = sc; c < ec; c++)
target.Add((r, c));
RectTransform bgRt = m_BackgroundGo.GetComponent<RectTransform>();
bgRt.anchorMin = new Vector2(0, 1);
bgRt.anchorMax = new Vector2(0, 1);
bgRt.pivot = new Vector2(0, 1);
bgRt.anchoredPosition = Vector2.zero;
bgRt.sizeDelta = new Vector2(totalWidth, totalHeight);
var bgImg = m_BackgroundGo.GetComponent<Image>();
bgImg.color = GetStyleColor?.Invoke(styleNamespace,
XericUIActionTable.STYLE_CELL_BG_COLOR,
XericUIActionTable.BG_COLOR_FALLBACK)
?? XericUIActionTable.BG_COLOR_FALLBACK;
bgImg.raycastTarget = false;
}
#endregion
#region 单元格渲染
/// <summary>回收所有池对象</summary>
public void ReturnAll() => m_Assembler.ReturnAll();
/// <summary>
/// 渲染指定范围内的单元格。
/// 行范围为 [sr, er),列范围为 [sc, ec)。
/// </summary>
public void RenderCells(int sr, int er, int sc, int ec, bool isEditor)
{
float[] rowHeights = GetRowHeights?.Invoke();
float[] colWidths = GetColWidths?.Invoke();
XTableData tableData = GetTableData?.Invoke();
string defaultNs = GetDefaultStyleNamespace?.Invoke() ?? "xeric_table_default";
if (rowHeights == null || colWidths == null) return;
if (rowHeights.Length == 0 || colWidths.Length == 0) return;
// 应用渲染范围限制
int renderStartRow = StartRow;
int renderEndRow = EndRow > 0 ? Mathf.Min(EndRow, rowHeights.Length) : rowHeights.Length;
int renderStartCol = StartCol;
int renderEndCol = EndCol > 0 ? Mathf.Min(EndCol, colWidths.Length) : colWidths.Length;
sr = Mathf.Max(sr, renderStartRow);
er = Mathf.Min(er, renderEndRow);
sc = Mathf.Max(sc, renderStartCol);
ec = Mathf.Min(ec, renderEndCol);
if (sr >= er || sc >= ec) return;
if (isEditor)
{
RenderCellsEditor(sr, er, sc, ec, rowHeights, colWidths, tableData, defaultNs);
}
if (fullRefresh)
ReleaseAll();
else
ReleaseExcept(target);
foreach (var cell in target)
if (fullRefresh || !m_Rendered.Contains(cell))
RenderCell(cell.Item1, cell.Item2, rows, cols, editorMode);
m_Rendered.Clear();
foreach (var cell in target) m_Rendered.Add(cell);
}
public void ReleaseAll()
{
foreach (var pair in m_Tracked)
ReleaseTracked(pair.Value);
m_Tracked.Clear();
m_Rendered.Clear();
if (ActivePrefabMap == null) return;
var keys = new List<(int, int)>();
foreach (var pair in ActivePrefabMap) keys.Add(pair.Key);
foreach (var key in keys) ActivePrefabMap.Remove(key);
}
private void ReleaseExcept(HashSet<(int, int)> target)
{
var remove = new List<(int, int)>();
foreach (var pair in m_Tracked)
if (!target.Contains(pair.Key)) remove.Add(pair.Key);
foreach (var key in remove)
{
RenderCellsRuntime(sr, er, sc, ec, rowHeights, colWidths, tableData, defaultNs);
ReleaseTracked(m_Tracked[key]);
m_Tracked.Remove(key);
}
}
private void RenderCellsEditor(int sr, int er, int sc, int ec,
float[] rowHeights, float[] colWidths, XTableData tableData, string defaultNs)
private void ReleaseTracked(Tracked tracked)
{
Color selectionColor = GetStyleColor(defaultNs, XericUIActionTable.STYLE_CELL_SELECTION_BG_COLOR,
XericUIActionTable.SELECTION_COLOR_FALLBACK);
Assembler.ReleaseText(tracked.Text);
Assembler.ReleaseImage(tracked.Image);
Assembler.ReleaseImage(tracked.Selection);
}
Color multiSelectColor = new Color(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * Config.MultiSelectAlphaFactor);
Color focusColor = new Color(selectionColor.r + Config.FocusRAdd, selectionColor.g + Config.FocusGAdd, selectionColor.b + Config.FocusBAdd, selectionColor.a * Config.FocusAlphaMul);
int selRow = GetSelectedRow();
int selCol = GetSelectedCol();
for (int r = sr; r < er && r < rowHeights.Length; r++)
private void RenderCell(int row, int col, float[] rows, float[] cols, bool editorMode)
{
var data = GetTableData?.Invoke();
if (data != null && data.IsCellMerged(row, col)) return;
string defaultNs = GetDefaultStyleNamespace?.Invoke() ?? "xeric_table_default";
string ns = data?.GetCell(row, col)?.GetEffectiveStyleNamespace(defaultNs) ?? defaultNs;
float x = CalcColLeftX(col, cols);
float y = -CalcRowTopY(row, rows);
float width = cols[col];
float height = rows[row];
if (data != null && data.IsMergeSource(row, col, out int mergeRows, out int mergeCols))
{
for (int c = sc; c < ec && c < colWidths.Length; c++)
for (int i = 1; i < mergeCols && col + i < cols.Length; i++) width += cols[col + i];
for (int i = 1; i < mergeRows && row + i < rows.Length; i++) height += rows[row + i];
}
int cellId = row * cols.Length + col;
var tracked = new Tracked();
bool selected = IsInSelectionRange?.Invoke(row, col) == true ||
(row == GetSelectedRow?.Invoke() && col == GetSelectedCol?.Invoke());
if (selected)
{
var selection = Assembler.GetImage(cellId, "cell_selection", ns, m_Parent);
if (selection != null)
{
if (tableData != null && tableData.IsCellMerged(r, c)) continue;
selection.Rect.anchoredPosition = new Vector2(x, y);
selection.Rect.sizeDelta = new Vector2(width, height);
selection.Image.color = editorMode
? GetStyleColor(ns, XericUIActionTable.STYLE_CELL_SELECTION_BG_COLOR, XericUIActionTable.SELECTION_COLOR_FALLBACK)
: Config.RuntimeSelectionColor;
tracked.Selection = selection;
}
}
float cellX = CalcColLeftX(c, colWidths);
float cellY = -CalcRowTopY(r, rowHeights);
float cellW = colWidths[c];
float cellH = rowHeights[r];
if (tableData != null && tableData.IsMergeSource(r, c, out int mergeRS, out int mergeCS))
var cell = data?.GetCell(row, col);
if (cell != null)
{
if (cell.ContentType == CellContentType.Text && !string.IsNullOrEmpty(cell.Text))
{
var text = Assembler.GetText(cellId, "cell_text", ns, m_Parent);
if (text != null)
{
for (int i = 1; i < mergeCS && c + i < colWidths.Length; i++)
cellW += colWidths[c + i];
for (int i = 1; i < mergeRS && r + i < rowHeights.Length; i++)
cellH += rowHeights[r + i];
}
XTableCellData data = tableData?.GetCell(r, c);
string cellNS = data != null ? data.GetEffectiveStyleNamespace(defaultNs) : defaultNs;
int cellId = r * colWidths.Length + c;
// 选择高亮
if (IsInSelectionRange != null && IsInSelectionRange(r, c))
{
var selImg = m_Assembler.GetImage(cellId, "cell_multiselect");
if (selImg != null)
text.Rect.anchoredPosition = new Vector2(x + CellPadding, y);
text.Rect.sizeDelta = new Vector2(width - CellPadding * 2f, height);
text.Text.SetText(cell.Text);
if (editorMode)
{
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
selImg.Image.color = (r == selRow && c == selCol) ? focusColor : multiSelectColor;
text.Text.color = GetStyleColor(ns, XericUIActionTable.STYLE_CELL_FG_COLOR, XericUIActionTable.FG_COLOR_FALLBACK);
text.Text.fontSize = GetStyleInt(ns, XericUIActionTable.STYLE_CELL_FONT_SIZE, XericUIActionTable.FONT_SIZE_FALLBACK);
text.Text.font = GetStyleFontAsset?.Invoke(ns, XericUIActionTable.STYLE_CELL_FONT_ASSET) ?? text.Text.font;
}
tracked.Text = text;
}
else if (r == selRow && c == selCol)
}
else if (cell.ContentType == CellContentType.Image && cell.Image != null)
{
var image = Assembler.GetImage(cellId, "cell_image", ns, m_Parent);
if (image != null)
{
var selImg = m_Assembler.GetImage(cellId, "cell_select");
if (selImg != null)
{
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
selImg.Image.color = selectionColor;
}
image.Rect.anchoredPosition = new Vector2(x + CellPadding, y);
image.Rect.sizeDelta = new Vector2(width - CellPadding * 2f, height);
image.Image.sprite = GetSprite(cell.Image);
tracked.Image = image;
}
if (data == null) continue;
switch (data.ContentType)
}
else if (cell.ContentType == CellContentType.Prefab && cell.Prefab != null && PrefabPool != null)
{
var prefab = PrefabPool.Acquire(cell.Prefab, cellId, m_Parent);
if (prefab != null)
{
case CellContentType.Text:
if (!string.IsNullOrEmpty(data.Text))
{
var txt = m_Assembler.GetText(cellId, "cell_text", cellNS);
if (txt != null)
{
txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY);
txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH);
txt.Text.SetText(data.Text);
txt.Text.color = GetStyleColor(cellNS, XericUIActionTable.STYLE_CELL_FG_COLOR,
XericUIActionTable.FG_COLOR_FALLBACK);
txt.Text.fontSize = GetStyleInt(cellNS, XericUIActionTable.STYLE_CELL_FONT_SIZE,
XericUIActionTable.FONT_SIZE_FALLBACK);
txt.Text.alignment = TextAlignmentOptions.Midline;
txt.Text.font = GetStyleFontAsset?.Invoke(cellNS,
XericUIActionTable.STYLE_CELL_FONT_ASSET) ?? txt.Text.font;
}
}
break;
case CellContentType.Image:
if (data.Image != null)
{
Sprite sprite = GetOrCreateSprite(data.Image);
var img = m_Assembler.GetImage(cellId, "cell_image", cellNS);
if (img != null)
{
img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY);
img.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH);
img.Image.sprite = sprite;
}
}
break;
case CellContentType.Prefab:
if (data.Prefab != null && PrefabPool != null)
{
GameObject instance = PrefabPool.Acquire(data.Prefab, cellId);
if (instance != null)
{
RectTransform prt = instance.GetComponent<RectTransform>();
prt.anchoredPosition = new Vector2(cellX, cellY);
prt.sizeDelta = new Vector2(cellW, cellH);
if (ActivePrefabMap != null)
ActivePrefabMap[(r, c)] = instance;
data.OnPrefabAcquire?.Invoke(instance);
}
}
break;
var rt = prefab.GetComponent<RectTransform>();
rt.anchoredPosition = new Vector2(x, y);
rt.sizeDelta = new Vector2(width, height);
if (ActivePrefabMap != null)
ActivePrefabMap[(row, col)] = prefab;
cell.OnPrefabAcquire?.Invoke(prefab);
}
}
}
m_Tracked[(row, col)] = tracked;
}
private void RenderCellsRuntime(int sr, int er, int sc, int ec,
float[] rowHeights, float[] colWidths, XTableData tableData, string defaultNs)
private Sprite GetSprite(Texture2D texture)
{
Color rtSelectionColor = Config.RuntimeSelectionColor;
Color rtMultiSelectColor = Config.RuntimeMultiSelectColor;
Color rtFocusColor = Config.RuntimeFocusColor;
int selRow = GetSelectedRow();
int selCol = GetSelectedCol();
for (int r = sr; r < er && r < rowHeights.Length; r++)
if (!m_SpriteCache.TryGetValue(texture, out var sprite))
{
for (int c = sc; c < ec && c < colWidths.Length; c++)
{
if (tableData != null && tableData.IsCellMerged(r, c)) continue;
float cellX = CalcColLeftX(c, colWidths);
float cellY = -CalcRowTopY(r, rowHeights);
float cellW = colWidths[c];
float cellH = rowHeights[r];
if (tableData != null && tableData.IsMergeSource(r, c, out int mergeRS, out int mergeCS))
{
for (int i = 1; i < mergeCS && c + i < colWidths.Length; i++)
cellW += colWidths[c + i];
for (int i = 1; i < mergeRS && r + i < rowHeights.Length; i++)
cellH += rowHeights[r + i];
}
XTableCellData data = tableData?.GetCell(r, c);
string cellNS = data != null ? data.GetEffectiveStyleNamespace(defaultNs) : defaultNs;
int cellId = r * colWidths.Length + c;
if (IsInSelectionRange != null && IsInSelectionRange(r, c))
{
var selImg = m_Assembler.GetImage(cellId, "cell_multiselect");
if (selImg != null)
{
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
selImg.Image.color = (r == selRow && c == selCol) ? rtFocusColor : rtMultiSelectColor;
}
}
else if (r == selRow && c == selCol)
{
var selImg = m_Assembler.GetImage(cellId, "cell_select");
if (selImg != null)
{
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
selImg.Image.color = rtSelectionColor;
}
}
if (data == null) continue;
switch (data.ContentType)
{
case CellContentType.Text:
if (!string.IsNullOrEmpty(data.Text))
{
var txt = m_Assembler.GetText(cellId, "cell_text", cellNS);
if (txt != null)
{
txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY);
txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH);
txt.Text.SetText(data.Text);
}
}
break;
case CellContentType.Image:
if (data.Image != null)
{
Sprite sprite = GetOrCreateSprite(data.Image);
var img = m_Assembler.GetImage(cellId, "cell_image", cellNS);
if (img != null)
{
img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY);
img.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH);
img.Image.sprite = sprite;
}
}
break;
case CellContentType.Prefab:
if (data.Prefab != null && PrefabPool != null)
{
GameObject instance = PrefabPool.Acquire(data.Prefab, cellId);
if (instance != null)
{
RectTransform prt = instance.GetComponent<RectTransform>();
prt.anchoredPosition = new Vector2(cellX, cellY);
prt.sizeDelta = new Vector2(cellW, cellH);
if (ActivePrefabMap != null)
ActivePrefabMap[(r, c)] = instance;
data.OnPrefabAcquire?.Invoke(instance);
}
}
break;
}
}
}
}
#endregion
#region 辅助方法
/// <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;
sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(.5f, .5f));
m_SpriteCache.Add(texture, sprite);
}
return sprite;
}
#endregion
#region 坐标计算
private float CalcRowTopY(int row, float[] rowHeights)
private static float CalcRowTopY(int row, float[] values)
{
float y = 0;
for (int i = 0; i < row && i < rowHeights.Length; i++)
y += rowHeights[i];
return y;
float result = 0;
for (int i = 0; i < row; i++) result += values[i];
return result;
}
private float CalcColLeftX(int col, float[] colWidths)
private static float CalcColLeftX(int col, float[] values)
{
float x = 0;
for (int i = 0; i < col && i < colWidths.Length; i++)
x += colWidths[i];
return x;
float result = 0;
for (int i = 0; i < col; i++) result += values[i];
return result;
}
#endregion
}
}