From ff8f049e30c87ba42ac4d8ea48efe59841950d2d Mon Sep 17 00:00:00 2001
From: LiRuoChen <571244399@qq.com>
Date: Thu, 9 Jul 2026 11:40:19 +0800
Subject: [PATCH 1/4] =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E5=8D=95=E5=85=83=E6=A0=BC=E5=86=BB=E7=BB=93=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Editor/XTable/XericUIActionTableEditor.cs | 9 +
.../Rendering/Component/TableRenderer.cs | 418 ++++++++++++++++++
.../Rendering/Component/TableRenderer.cs.meta | 11 +
.../Rendering/Component/XericUIActionTable.cs | 137 ++++++
4 files changed, 575 insertions(+)
create mode 100644 Runtime/XTable/Rendering/Component/TableRenderer.cs
create mode 100644 Runtime/XTable/Rendering/Component/TableRenderer.cs.meta
diff --git a/Editor/XTable/XericUIActionTableEditor.cs b/Editor/XTable/XericUIActionTableEditor.cs
index 745aa19..1d2d89f 100644
--- a/Editor/XTable/XericUIActionTableEditor.cs
+++ b/Editor/XTable/XericUIActionTableEditor.cs
@@ -70,6 +70,15 @@ namespace XericUIEditor.XTable
EditorGUILayout.PropertyField(m_RowHeightsProp, new GUIContent("行高"), true);
if (m_ColWidthsProp != null)
EditorGUILayout.PropertyField(m_ColWidthsProp, new GUIContent("列宽"), true);
+
+ // 冻结行列
+ var freezeRowProp = serializedObject.FindProperty("m_FreezeRowCount");
+ var freezeColProp = serializedObject.FindProperty("m_FreezeColCount");
+ if (freezeRowProp != null)
+ EditorGUILayout.PropertyField(freezeRowProp, new GUIContent("冻结行数"));
+ if (freezeColProp != null)
+ EditorGUILayout.PropertyField(freezeColProp, new GUIContent("冻结列数"));
+
EditorGUI.indentLevel--;
}
diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs b/Runtime/XTable/Rendering/Component/TableRenderer.cs
new file mode 100644
index 0000000..315300c
--- /dev/null
+++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs
@@ -0,0 +1,418 @@
+using System.Collections.Generic;
+using TMPro;
+using UnityEngine;
+using UnityEngine.UI;
+using XericUI.XTable.Core;
+using XericUI.XTable.Rendering.Elements;
+using XericLibrary.Runtime.SuperStyleSheet;
+
+namespace XericUI.XTable.Rendering.Component
+{
+ ///
+ /// 表格渲染管线封装——将 CellAssembler 对象池、背景创建、单元格渲染抽离为可复用单元。
+ /// 同一表格可持有多个 TableRenderer 实例,分别渲染到不同父节点(如 Content 和 Viewport)。
+ ///
+ public class TableRenderer
+ {
+ #region 内部字段
+
+ private Transform m_Parent;
+ private CellAssembler m_Assembler;
+ private GameObject m_BackgroundGo;
+
+ /// 当前渲染器中所有预制体实例的缓存
+ private Dictionary<(int, int), GameObject> m_PrefabInstances;
+
+ #endregion
+
+ #region 公开属性
+
+ public CellAssembler Assembler => m_Assembler;
+ public GameObject BackgroundGo => m_BackgroundGo;
+ public Transform Parent => m_Parent;
+
+ /// 起始行偏移(Viewport 渲染器通常为 0;Content 渲染器通常为 freezeRowCount)
+ public int StartRow;
+ /// 起始列偏移
+ public int StartCol;
+ /// 终止行(不含),0 表示不受限
+ public int EndRow;
+ /// 终止列(不含),0 表示不受限
+ public int EndCol;
+
+ #endregion
+
+ #region 样式委托
+
+ /// 从样式表读取颜色值
+ public System.Func GetStyleColor;
+ /// 从样式表读取浮点值
+ public System.Func GetStyleFloat;
+ /// 从样式表读取整数值
+ public System.Func GetStyleInt;
+ /// 从样式表读取 TMP_FontAsset
+ public System.Func GetStyleFontAsset;
+
+ /// 获取行高数组
+ public System.Func GetRowHeights;
+ /// 获取列宽数组
+ public System.Func GetColWidths;
+ /// 获取表格数据
+ public System.Func GetTableData;
+ /// 默认样式命名空间
+ public System.Func GetDefaultStyleNamespace;
+ /// 判断单元格是否在多选范围内
+ public System.Func IsInSelectionRange;
+ /// 获取选中的起始行列
+ public System.Func GetSelectedRow;
+ /// 获取选中的起始列
+ public System.Func GetSelectedCol;
+
+ /// 运行时样式变更回调
+ public event System.Action OnStyleChangeDetected;
+
+ #endregion
+
+ #region 构造 / 销毁
+
+ public TableRenderer(Transform parent, string defaultStyleNamespace = null,
+ System.Action styleChangeHandler = null)
+ {
+ m_Parent = parent;
+ m_Assembler = new CellAssembler(parent, defaultStyleNamespace);
+ if (styleChangeHandler != null)
+ m_Assembler.OnStyleChangeDetected += styleChangeHandler;
+ }
+
+ /// 彻底销毁渲染器中所有动态生成的对象
+ public void DestroyAll()
+ {
+ m_Assembler?.DestroyAll();
+ ClearPrefabInstances();
+
+ if (m_BackgroundGo != null)
+ {
+ if (Application.isPlaying)
+ Object.Destroy(m_BackgroundGo);
+ else
+ Object.DestroyImmediate(m_BackgroundGo);
+ m_BackgroundGo = null;
+ }
+ }
+
+ #endregion
+
+ #region 背景
+
+ /// 创建或获取背景 Image(锚定左上角,覆盖整个渲染范围)
+ public void CreateBackground(float totalWidth, float totalHeight, string styleNamespace)
+ {
+ if (m_BackgroundGo != null)
+ {
+ var rt = m_BackgroundGo.GetComponent();
+ rt.sizeDelta = new Vector2(totalWidth, totalHeight);
+ 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();
+
+ 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 = Vector2.zero;
+ bgRt.sizeDelta = new Vector2(totalWidth, totalHeight);
+
+ var bgImg = m_BackgroundGo.GetComponent();
+ bgImg.color = GetStyleColor?.Invoke(styleNamespace,
+ XericUIActionTable.STYLE_CELL_BG_COLOR,
+ XericUIActionTable.BG_COLOR_FALLBACK)
+ ?? XericUIActionTable.BG_COLOR_FALLBACK;
+ bgImg.raycastTarget = false;
+ }
+
+ #endregion
+
+ #region 单元格渲染
+
+ /// 回收所有池对象
+ public void ReturnAll() => m_Assembler.ReturnAll();
+
+ ///
+ /// 渲染指定范围内的单元格。
+ /// 行范围为 [sr, er),列范围为 [sc, ec)。
+ ///
+ 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);
+ }
+ else
+ {
+ RenderCellsRuntime(sr, er, sc, ec, rowHeights, colWidths, tableData, defaultNs);
+ }
+ }
+
+ private void RenderCellsEditor(int sr, int er, int sc, int ec,
+ float[] rowHeights, float[] colWidths, XTableData tableData, string defaultNs)
+ {
+ Color selectionColor = GetStyleColor(defaultNs, XericUIActionTable.STYLE_CELL_SELECTION_BG_COLOR,
+ XericUIActionTable.SELECTION_COLOR_FALLBACK);
+ Color borderColor = GetStyleColor(defaultNs, XericUIActionTable.STYLE_CELL_BORDER_BOTTOM_COLOR,
+ XericUIActionTable.BORDER_COLOR_FALLBACK);
+ float borderWidth = GetStyleFloat(defaultNs, XericUIActionTable.STYLE_CELL_BORDER_BOTTOM_WIDTH,
+ XericUIActionTable.BORDER_WIDTH_FALLBACK);
+
+ Color multiSelectColor = new Color(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * 0.4f);
+ Color focusColor = new Color(selectionColor.r + 0.04f, selectionColor.g + 0.13f, selectionColor.b + 0.13f, selectionColor.a * 1.4f);
+
+ int selRow = GetSelectedRow();
+ int selCol = GetSelectedCol();
+
+ for (int r = sr; r < er && r < rowHeights.Length; r++)
+ {
+ 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 && !string.IsNullOrEmpty(data.StyleNamespace))
+ ? data.StyleNamespace : defaultNs;
+
+ // 选择高亮
+ if (IsInSelectionRange != null && 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 == selRow && c == selCol) ? focusColor : multiSelectColor;
+ }
+ else if (r == selRow && c == selCol)
+ {
+ 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;
+
+ if (!string.IsNullOrEmpty(data.Text))
+ {
+ var txt = m_Assembler.GetText("cell_text", cellNS);
+ txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY);
+ txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH);
+ txt.Text.text = 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;
+ }
+
+ if (data.Prefab != null)
+ PositionPrefabInstance(r, c, data.Prefab, cellX, cellY, cellW, cellH);
+ }
+ }
+ }
+
+ private void RenderCellsRuntime(int sr, int er, int sc, int ec,
+ float[] rowHeights, float[] colWidths, XTableData tableData, string defaultNs)
+ {
+ Color rtSelectionColor = XericUIActionTable.RT_SELECTION_COLOR;
+ Color rtMultiSelectColor = XericUIActionTable.RT_MULTI_SELECT_COLOR;
+ Color rtFocusColor = XericUIActionTable.RT_FOCUS_COLOR;
+
+ int selRow = GetSelectedRow();
+ int selCol = GetSelectedCol();
+
+ for (int r = sr; r < er && r < rowHeights.Length; r++)
+ {
+ 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 && !string.IsNullOrEmpty(data.StyleNamespace))
+ ? data.StyleNamespace : defaultNs;
+
+ if (IsInSelectionRange != null && 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 == selRow && c == selCol) ? rtFocusColor : rtMultiSelectColor;
+ }
+ else if (r == selRow && c == selCol)
+ {
+ 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, XericUIActionTable.RT_BORDER_WIDTH);
+ hLine.Image.color = XericUIActionTable.RT_BORDER_COLOR;
+
+ var vLine = m_Assembler.GetImage("cell_vline");
+ vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - XericUIActionTable.RT_BORDER_WIDTH, cellY);
+ vLine.Rect.sizeDelta = new Vector2(XericUIActionTable.RT_BORDER_WIDTH, cellH);
+ vLine.Image.color = XericUIActionTable.RT_BORDER_COLOR;
+
+ if (data == null) continue;
+
+ if (!string.IsNullOrEmpty(data.Text))
+ {
+ var txt = m_Assembler.GetText("cell_text", cellNS);
+ txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY);
+ txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH);
+ txt.Text.text = data.Text;
+ }
+
+ if (data.Prefab != null)
+ PositionPrefabInstance(r, c, data.Prefab, cellX, cellY, cellW, cellH);
+ }
+ }
+ }
+
+ #endregion
+
+ #region 预制体管理
+
+ private GameObject GetOrCreatePrefabInstance(int row, int col, GameObject prefab)
+ {
+ if (m_PrefabInstances == null)
+ m_PrefabInstances = new Dictionary<(int, int), GameObject>();
+
+ var key = (row, col);
+ if (m_PrefabInstances.TryGetValue(key, out GameObject existing) && existing != null)
+ return existing;
+
+ GameObject instance = Object.Instantiate(prefab, m_Parent);
+ instance.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
+ instance.name = $"_prefab_{row}_{col}";
+ m_PrefabInstances[key] = instance;
+ return instance;
+ }
+
+ private void PositionPrefabInstance(int row, int col, GameObject prefab,
+ float cellX, float cellY, float cellW, float cellH)
+ {
+ GameObject instance = GetOrCreatePrefabInstance(row, col, prefab);
+ if (instance == null) return;
+
+ RectTransform prt = instance.GetComponent();
+ if (prt == null) prt = instance.AddComponent();
+
+ prt.anchorMin = new Vector2(0, 1);
+ prt.anchorMax = new Vector2(0, 1);
+ prt.pivot = new Vector2(0, 1);
+ prt.anchoredPosition = new Vector2(cellX, cellY);
+ prt.sizeDelta = new Vector2(cellW, cellH);
+ }
+
+ /// 销毁所有缓存预制体实例
+ public void ClearPrefabInstances()
+ {
+ if (m_PrefabInstances == null) return;
+ foreach (var kv in m_PrefabInstances)
+ {
+ if (kv.Value != null)
+ {
+ if (Application.isPlaying) Object.Destroy(kv.Value);
+ else Object.DestroyImmediate(kv.Value);
+ }
+ }
+ m_PrefabInstances.Clear();
+ }
+
+ #endregion
+
+ #region 坐标计算
+
+ private float CalcRowTopY(int row, float[] rowHeights)
+ {
+ float y = 0;
+ for (int i = 0; i < row && i < rowHeights.Length; i++)
+ y += rowHeights[i];
+ return y;
+ }
+
+ private float CalcColLeftX(int col, float[] colWidths)
+ {
+ float x = 0;
+ for (int i = 0; i < col && i < colWidths.Length; i++)
+ x += colWidths[i];
+ return x;
+ }
+
+ #endregion
+ }
+}
diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs.meta b/Runtime/XTable/Rendering/Component/TableRenderer.cs.meta
new file mode 100644
index 0000000..90ebb96
--- /dev/null
+++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: de1e4b2ad4f55bc48a57a820276024b1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs
index 3440dca..ccd9f0d 100644
--- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs
+++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs
@@ -44,6 +44,14 @@ namespace XericUI.XTable.Rendering.Component
[SerializeField] private ScrollRect m_ScrollRect;
+ [SerializeField]
+ [Tooltip("冻结行数——顶部固定不滚动的行数")]
+ private int m_FreezeRowCount;
+
+ [SerializeField]
+ [Tooltip("冻结列数——左侧固定不滚动的列数")]
+ private int m_FreezeColCount;
+
[SerializeField]
[Tooltip("在 Hierarchy 中显示动态生成的子对象(仅编辑器,不保存到场景)")]
private bool m_ChildrenVisible;
@@ -55,6 +63,9 @@ namespace XericUI.XTable.Rendering.Component
[System.NonSerialized] private CellAssembler m_Assembler;
[System.NonSerialized] private XTableDirtyFlag m_DirtyFlag;
+ /// Viewport 层渲染器——用于绘制冻结行列单元格
+ [System.NonSerialized] private TableRenderer m_ViewportRenderer;
+
[System.NonSerialized] private XTableSelectionHandle m_SelectionHandle;
[System.NonSerialized] private GameObject m_BackgroundGo;
@@ -105,6 +116,21 @@ namespace XericUI.XTable.Rendering.Component
public float[] RowHeights => m_RowHeights;
public float[] ColWidths => m_ColWidths;
+ /// 冻结行数
+ public int FreezeRowCount => m_FreezeRowCount;
+ /// 冻结列数
+ 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 等属性
@@ -332,6 +358,9 @@ namespace XericUI.XTable.Rendering.Component
CreateDefaultSampleData();
CreateBackground();
+ // 创建 Viewport 层渲染器(用于冻结单元格)
+ CreateViewportRenderer();
+
if (m_ScrollRect != null)
m_ScrollRect.onValueChanged.AddListener(OnScrollValueChanged);
@@ -385,6 +414,10 @@ namespace XericUI.XTable.Rendering.Component
m_BackgroundGo = null;
}
+ // 销毁 Viewport 冻结渲染器
+ m_ViewportRenderer?.DestroyAll();
+ m_ViewportRenderer = null;
+
base.OnDestroy();
}
@@ -816,6 +849,9 @@ namespace XericUI.XTable.Rendering.Component
// 重建边框线段
RebuildBorders();
+ // 渲染 Viewport 层冻结单元格
+ RenderFrozenCells();
+
m_DirtyFlag.Clear();
OnTableRefreshed?.Invoke();
SetChildrenVisible(m_ChildrenVisible);
@@ -905,6 +941,7 @@ namespace XericUI.XTable.Rendering.Component
RecalculateTotalSize();
CreateDefaultSampleData();
CreateBackground();
+ CreateViewportRenderer();
MarkDirty(TableDirtyType.All);
RefreshView();
SetChildrenVisible(m_ChildrenVisible);
@@ -983,6 +1020,7 @@ namespace XericUI.XTable.Rendering.Component
MarkDirty(TableDirtyType.All);
RecalculateTotalSize();
CreateBackground();
+ CreateViewportRenderer();
RefreshView();
SetChildrenVisible(m_ChildrenVisible);
}
@@ -1597,6 +1635,105 @@ namespace XericUI.XTable.Rendering.Component
#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;
+ }
+
+ /// 渲染 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)
From 15bf34edaf7bd52c2e6649aea9246d1d613ee5c7 Mon Sep 17 00:00:00 2001
From: LiRuoChen <571244399@qq.com>
Date: Thu, 9 Jul 2026 11:40:43 +0800
Subject: [PATCH 2/4] =?UTF-8?q?ui=E5=9F=BA=E7=B1=BB=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E6=98=BE=E5=BD=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Runtime/Core/Base/XericUIBehaciour.cs | 46 +++++++++++++++++++--------
1 file changed, 32 insertions(+), 14 deletions(-)
diff --git a/Runtime/Core/Base/XericUIBehaciour.cs b/Runtime/Core/Base/XericUIBehaciour.cs
index 84cc67a..f68fc0e 100644
--- a/Runtime/Core/Base/XericUIBehaciour.cs
+++ b/Runtime/Core/Base/XericUIBehaciour.cs
@@ -1,4 +1,5 @@
using System;
+using Deconstruction.Runtime;
#if ODIN_INSPECTOR
using Sirenix.OdinInspector;
#endif
@@ -36,11 +37,9 @@ namespace XericUI.Core.Base
{
get
{
- if (_canvas == null)
- _canvas = GetComponentInParent