diff --git a/Runtime/XTable/Rendering/Component/XTableDirtyFlag.cs b/Runtime/XTable/Rendering/Component/XTableDirtyFlag.cs
index 8a6d5aa..689a6d8 100644
--- a/Runtime/XTable/Rendering/Component/XTableDirtyFlag.cs
+++ b/Runtime/XTable/Rendering/Component/XTableDirtyFlag.cs
@@ -19,8 +19,10 @@ namespace XericUI.XTable.Rendering.Component
SelectionChanged = 1 << 3,
/// 行列尺寸变更
LayoutChanged = 1 << 4,
+ /// 滚动偏移变更(轻刷新:仅更新可见单元格内容,不重建边框/背景)
+ ScrollChanged = 1 << 5,
/// 全部脏(强制完整刷新)
- All = DataChanged | ViewChanged | StyleChanged | SelectionChanged | LayoutChanged
+ All = DataChanged | ViewChanged | StyleChanged | SelectionChanged | LayoutChanged | ScrollChanged
}
///
@@ -46,6 +48,12 @@ namespace XericUI.XTable.Rendering.Component
Flags = TableDirtyType.None;
}
+ /// 清除指定的脏标记(保留其他标记)
+ public void ClearFlags(TableDirtyType type)
+ {
+ Flags &= ~type;
+ }
+
/// 检查是否包含特定脏类型
public bool Has(TableDirtyType type)
{
diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs
index 009fe7f..05ced7f 100644
--- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs
+++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs
@@ -527,7 +527,14 @@ namespace XericUI.XTable.Rendering.Component
private void Update()
{
- if (m_DirtyFlag.IsDirty) RefreshView();
+ if (!m_DirtyFlag.IsDirty) return;
+
+ var flags = m_DirtyFlag.Flags;
+ // 仅滚动或选区变更 → 轻刷新(不重建边框/背景/预制体全回收)
+ if ((flags & ~(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged)) == 0)
+ RefreshViewLight();
+ else
+ RefreshView();
}
#endregion
@@ -586,7 +593,12 @@ namespace XericUI.XTable.Rendering.Component
return;
}
- if (m_DirtyFlag.IsDirty)
+ if (!m_DirtyFlag.IsDirty) return;
+
+ var flags = m_DirtyFlag.Flags;
+ if ((flags & ~(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged)) == 0)
+ RefreshViewLight();
+ else
RefreshView();
}
#endif
@@ -1087,6 +1099,98 @@ namespace XericUI.XTable.Rendering.Component
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 背景
@@ -1516,6 +1620,192 @@ namespace XericUI.XTable.Rendering.Component
}
}
+ ///
+ /// 轻渲染——与 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 坐标计算
@@ -1708,8 +1998,8 @@ namespace XericUI.XTable.Rendering.Component
{
m_DirtyFlag.Mark(type);
- // 数据或布局变更时重建四叉树
- if ((type & (TableDirtyType.DataChanged | TableDirtyType.LayoutChanged)) != 0)
+ // 仅数据或布局变更时重建四叉树;滚动/选区变更无需重建
+ if ((type & (TableDirtyType.DataChanged | TableDirtyType.LayoutChanged | TableDirtyType.StyleChanged)) != 0)
MaintainQuadtree();
}
@@ -1720,17 +2010,16 @@ namespace XericUI.XTable.Rendering.Component
private void OnScrollValueChanged(Vector2 _)
{
- // 防重入:RefreshView 中修改 content.sizeDelta 可能触发 ScrollRect 再次回调
+ // 防重入:RefreshViewLight 中操作 content 可能触发 ScrollRect 再次回调
if (m_IsRefreshingFromScroll) return;
m_IsRefreshingFromScroll = true;
try
{
- // 滚动时立即更新可见矩形并刷新视图,不依赖脏标记延迟机制。
- // 表格本身没有移动能力,完全依赖 ScrollRect 的 content 偏移和 Mask 裁剪,
- // 因此必须在 ScrollRect 值变化时立即重算可见范围。
+ // 滚动时仅做轻刷新:更新可见矩形 + 渲染可见单元格内容,
+ // 不重建背景、边框、不重置 content.sizeDelta、不做孤儿清理。
UpdateVisibleRect();
- RefreshView();
+ RefreshViewLight();
}
finally
{