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)