From 2fb8af52a6157d1408e2918ec02cd498cbf5d7d0 Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Wed, 29 Jul 2026 11:27:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A1=A8=E6=A0=BC=E6=A1=86?= =?UTF-8?q?=E9=80=89=E5=8F=AF=E8=83=BD=E9=9A=8F=E6=9C=BA=E5=A4=9A=E9=80=89?= =?UTF-8?q?=EF=BC=8C=E8=A1=A5=E5=85=85=E8=A1=A8=E6=A0=BC=E6=8B=96=E6=8B=BD?= =?UTF-8?q?=E5=A4=9A=E9=80=89=EF=BC=8C=E8=A7=86=E5=9B=BE=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=81=8F=E7=A7=BB=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Rendering/Component/TableRenderer.cs | 21 --- .../Component/XTableSelectionOverlay.cs | 120 +++++++++++++++ .../Component/XTableSelectionOverlay.cs.meta | 11 ++ .../Component/XericUIActionTable.Api.cs | 1 + .../Component/XericUIActionTable.Freeze.cs | 51 ++++++- .../XericUIActionTable.RendererFilter.cs | 68 +-------- .../Component/XericUIActionTable.Selection.cs | 141 ++++++++++++++++++ .../XericUIActionTable.Selection.cs.meta | 11 ++ .../Rendering/Component/XericUIActionTable.cs | 71 +++------ 9 files changed, 361 insertions(+), 134 deletions(-) create mode 100644 Runtime/XTable/Rendering/Component/XTableSelectionOverlay.cs create mode 100644 Runtime/XTable/Rendering/Component/XTableSelectionOverlay.cs.meta create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Selection.cs create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.Selection.cs.meta diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs b/Runtime/XTable/Rendering/Component/TableRenderer.cs index 7a91345..bbfba69 100644 --- a/Runtime/XTable/Rendering/Component/TableRenderer.cs +++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs @@ -14,7 +14,6 @@ namespace XericUI.XTable.Rendering.Component { public CellAssembler.PooledText Text; public CellAssembler.PooledImage Image; - public CellAssembler.PooledImage Selection; } private readonly Transform m_Parent; @@ -41,9 +40,6 @@ namespace XericUI.XTable.Rendering.Component 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 Transform Parent => m_Parent; public GameObject BackgroundGo => m_BackgroundGo; @@ -144,7 +140,6 @@ namespace XericUI.XTable.Rendering.Component { Assembler.ReleaseText(tracked.Text); Assembler.ReleaseImage(tracked.Image); - Assembler.ReleaseImage(tracked.Selection); } private void RenderCell(int row, int col, float[] rows, float[] cols, bool editorMode) @@ -165,22 +160,6 @@ namespace XericUI.XTable.Rendering.Component 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) - { - 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; - } - } - var cell = data?.GetCell(row, col); if (cell != null) { diff --git a/Runtime/XTable/Rendering/Component/XTableSelectionOverlay.cs b/Runtime/XTable/Rendering/Component/XTableSelectionOverlay.cs new file mode 100644 index 0000000..96b335b --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XTableSelectionOverlay.cs @@ -0,0 +1,120 @@ +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using XericLibrary.Runtime.MacroLibrary; + +namespace XericUI.XTable.Rendering.Component +{ + /// 单个渲染层的固定框选输入与视觉覆盖层。 + [RequireComponent(typeof(RectTransform), typeof(Image))] + public sealed class XTableSelectionOverlay : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler, IEndDragHandler + { + private XericUIActionTable m_Table; + private Image m_Fill; + private int m_RowStart; + private int m_RowEnd; + private int m_ColStart; + private int m_ColEnd; + private bool m_Dragging; + + public void Configure(XericUIActionTable table, int rowStart, int rowEnd, int colStart, int colEnd) + { + m_Table = table; + m_RowStart = rowStart; + m_RowEnd = rowEnd; + m_ColStart = colStart; + m_ColEnd = colEnd; + Rect region = table.GetTableRect(rowStart, rowEnd, colStart, colEnd); + var rt = transform.RectTransform(); + rt.anchorMin = new Vector2(0, 1); + rt.anchorMax = new Vector2(0, 1); + rt.pivot = new Vector2(0, 1); + rt.anchoredPosition = new Vector2(region.x, -region.y); + rt.sizeDelta = region.size; + EnsureVisuals(); + RefreshVisual(); + } + + public void RefreshVisual() + { + EnsureVisuals(); + if (m_Table == null || !m_Table.SelectionHandle.HasSelection || m_RowStart >= m_RowEnd || m_ColStart >= m_ColEnd) + { + m_Fill.gameObject.SetActive(false); + return; + } + + int rs = Mathf.Max(m_RowStart, m_Table.SelectionHandle.MinRow); + int re = Mathf.Min(m_RowEnd - 1, m_Table.SelectionHandle.MaxRow); + int cs = Mathf.Max(m_ColStart, m_Table.SelectionHandle.MinCol); + int ce = Mathf.Min(m_ColEnd - 1, m_Table.SelectionHandle.MaxCol); + if (rs > re || cs > ce) + { + m_Fill.gameObject.SetActive(false); + return; + } + + Rect rect = m_Table.GetTableRect(rs, re + 1, cs, ce + 1); + Rect region = m_Table.GetTableRect(m_RowStart, m_RowEnd, m_ColStart, m_ColEnd); + var fillRt = m_Fill.rectTransform; + fillRt.anchoredPosition = new Vector2(rect.x - region.x, region.y - rect.y); + fillRt.sizeDelta = rect.size; + m_Fill.color = m_Table.SelectionOverlayColor; + m_Fill.gameObject.SetActive(true); + } + + public void OnPointerDown(PointerEventData eventData) + { + if (m_Table == null || !m_Table.TryGetCellFromScreenPoint(transform.RectTransform(), eventData.position, eventData.pressEventCamera, true, out int row, out int col)) + return; + m_Dragging = true; + m_Table.BeginSelection(row, col); + eventData.Use(); + } + + public void OnDrag(PointerEventData eventData) + { + if (!m_Dragging || m_Table == null) return; + m_Table.UpdateSelectionDrag(transform.RectTransform(), eventData.position, eventData.pressEventCamera); + eventData.Use(); + } + + public void OnPointerUp(PointerEventData eventData) + { + EndDrag(); + eventData.Use(); + } + + public void OnEndDrag(PointerEventData eventData) => EndDrag(); + + private void OnDisable() => EndDrag(); + private void OnDestroy() => EndDrag(); + + private void EndDrag() + { + if (!m_Dragging) return; + m_Dragging = false; + m_Table?.EndSelection(); + } + + private void EnsureVisuals() + { + var input = GetComponent(); + input.color = Color.clear; + input.raycastTarget = true; + if (m_Fill != null) return; + var go = new GameObject("_SelectionRect", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Outline)); + go.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; + go.transform.SetParent(transform, false); + m_Fill = go.GetComponent(); + m_Fill.raycastTarget = false; + var outline = go.GetComponent(); + outline.effectColor = new Color(0.2f, 0.55f, 1f, 1f); + outline.effectDistance = new Vector2(1f, -1f); + var rt = m_Fill.rectTransform; + rt.anchorMin = new Vector2(0, 1); + rt.anchorMax = new Vector2(0, 1); + rt.pivot = new Vector2(0, 1); + } + } +} diff --git a/Runtime/XTable/Rendering/Component/XTableSelectionOverlay.cs.meta b/Runtime/XTable/Rendering/Component/XTableSelectionOverlay.cs.meta new file mode 100644 index 0000000..061608c --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XTableSelectionOverlay.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2746af0dac5753f4289a900b400c5c9e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs index 2a5e60b..2d2e808 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs @@ -416,6 +416,7 @@ namespace XericUI.XTable.Rendering.Component private void OnHandleSelectionChanged(XTableSelectionHandle handle) { MarkDirty(TableDirtyType.SelectionChanged); + RefreshSelectionOverlays(); } /// 裁剪选区到当前行列尺寸范围内 diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs index df362ec..1cd9195 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs @@ -63,9 +63,6 @@ namespace XericUI.XTable.Rendering.Component GetColWidths = () => m_ColWidths, GetTableData = () => m_TableData, GetDefaultStyleNamespace = () => m_DefaultStyleNamespace, - IsInSelectionRange = (r, c) => IsInSelectionRange(r, c), - GetSelectedRow = () => SelectionHandle.StartRow, - GetSelectedCol = () => SelectionHandle.StartCol, }; return renderer; } @@ -79,11 +76,22 @@ namespace XericUI.XTable.Rendering.Component m_FrozenColumnRenderer = null; m_FrozenRowRenderer = null; m_FrozenCornerRenderer = null; + DestroySelectionOverlay(ref m_FrozenColumnSelectionOverlay); + DestroySelectionOverlay(ref m_FrozenRowSelectionOverlay); + DestroySelectionOverlay(ref m_FrozenCornerSelectionOverlay); DestroyFrozenContainer(ref m_FrozenColumnsContainer); DestroyFrozenContainer(ref m_FrozenRowsContainer); DestroyFrozenContainer(ref m_FrozenCornerContainer); } + private static void DestroySelectionOverlay(ref XTableSelectionOverlay overlay) + { + if (overlay == null) return; + if (Application.isPlaying) Object.Destroy(overlay.gameObject); + else Object.DestroyImmediate(overlay.gameObject); + overlay = null; + } + private static void DestroyFrozenContainer(ref RectTransform container) { if (container == null) return; @@ -108,11 +116,14 @@ namespace XericUI.XTable.Rendering.Component if (freezeRows == 0 && freezeCols == 0) { DestroyFrozenRenderers(); + if (m_RowHeights != null && m_ColWidths != null) + CreateOrUpdateSelectionOverlays(0, 0); return; } if (m_FrozenColumnRenderer == null) CreateViewportRenderer(); if (m_FrozenColumnRenderer == null) return; + CreateOrUpdateSelectionOverlays(freezeRows, freezeCols); CreateFrozenBackgrounds(freezeRows, freezeCols); SyncFrozenContainers(); CalcVisibleRange(out int visibleStartRow, out int visibleEndRow, out int visibleStartCol, out int visibleEndCol); @@ -145,6 +156,40 @@ namespace XericUI.XTable.Rendering.Component m_FrozenCornerRenderer.CreateBackground(freezeWidth, freezeHeight, m_DefaultStyleNamespace); } + private void CreateOrUpdateSelectionOverlays(int freezeRows, int freezeCols) + { + int rows = m_RowHeights.Length; + int cols = m_ColWidths.Length; + m_NormalSelectionOverlay = CreateOrUpdateSelectionOverlay(ContentTransform, m_NormalSelectionOverlay, freezeRows, rows, freezeCols, cols, "_NormalSelectionOverlay"); + m_FrozenColumnSelectionOverlay = CreateOrUpdateSelectionOverlay(m_FrozenColumnsContainer, m_FrozenColumnSelectionOverlay, freezeRows, rows, 0, freezeCols, "_FrozenColumnSelectionOverlay"); + m_FrozenRowSelectionOverlay = CreateOrUpdateSelectionOverlay(m_FrozenRowsContainer, m_FrozenRowSelectionOverlay, 0, freezeRows, freezeCols, cols, "_FrozenRowSelectionOverlay"); + m_FrozenCornerSelectionOverlay = CreateOrUpdateSelectionOverlay(m_FrozenCornerContainer, m_FrozenCornerSelectionOverlay, 0, freezeRows, 0, freezeCols, "_FrozenCornerSelectionOverlay"); + } + + private XTableSelectionOverlay CreateOrUpdateSelectionOverlay(Transform parent, XTableSelectionOverlay overlay, + int rowStart, int rowEnd, int colStart, int colEnd, string name) + { + if (parent == null) return overlay; + if (overlay == null) + { + var go = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(UnityEngine.UI.Image), typeof(XTableSelectionOverlay)); + go.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; + go.transform.SetParent(parent, false); + overlay = go.GetComponent(); + } + overlay.transform.SetAsLastSibling(); + overlay.Configure(this, rowStart, rowEnd, colStart, colEnd); + return overlay; + } + + private void RefreshSelectionOverlays() + { + m_NormalSelectionOverlay?.RefreshVisual(); + m_FrozenColumnSelectionOverlay?.RefreshVisual(); + m_FrozenRowSelectionOverlay?.RefreshVisual(); + m_FrozenCornerSelectionOverlay?.RefreshVisual(); + } + private void SyncFrozenContainers() { if (m_ScrollRect == null || m_ScrollRect.content == null) return; diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs index b4d643d..1963a4f 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs @@ -29,7 +29,6 @@ namespace XericUI.XTable.Rendering.Component { public CellAssembler.PooledText CellText; public CellAssembler.PooledImage CellImage; - public CellAssembler.PooledImage SelectBg; } #endregion @@ -42,7 +41,6 @@ namespace XericUI.XTable.Rendering.Component 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_CellTracked.Remove(key); } @@ -53,7 +51,6 @@ namespace XericUI.XTable.Rendering.Component var objs = kv.Value; m_Assembler?.ReleaseText(objs.CellText); m_Assembler?.ReleaseImage(objs.CellImage); - m_Assembler?.ReleaseImage(objs.SelectBg); } m_CellTracked.Clear(); m_LastRenderedCells.Clear(); @@ -150,9 +147,6 @@ namespace XericUI.XTable.Rendering.Component int cellId = row * m_ColWidths.Length + col; var objs = new CellTrackedObjs(); - // ── 选择高亮 ── - ApplySelectionHighlight(cellId, row, col, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs); - // ── 单元格内容 ── if (data != null) { @@ -174,44 +168,6 @@ namespace XericUI.XTable.Rendering.Component m_LastRenderedCells.Add((row, col)); } - private void ApplySelectionHighlight(int cellId, 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(cellId, key, cellNS); - if (img == null) return; - 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 RenderCellText(int cellId, XTableCellData data, float cellX, float cellY, float cellW, float cellH, string cellNS, bool editorMode, ref CellTrackedObjs objs) @@ -368,27 +324,19 @@ namespace XericUI.XTable.Rendering.Component #endif string ns = m_DefaultStyleNamespace; - // 选区变更时:所有单元格的选择高亮需要重绘,但预制体不受影响 - bool selectionChanged = m_DirtyFlag.Has(TableDirtyType.SelectionChanged); - if (selectionChanged) - { - ReleaseAllTrackedCells(); - } + // 选区由固定 Overlay 绘制,选择变化无需回收或重绘单元格内容。 // 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); - } + 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) @@ -408,7 +356,7 @@ namespace XericUI.XTable.Rendering.Component // 4. 渲染单元格:选区变更时全量重渲所有可见单元格,否则仅新进入的 foreach (var cell in newVisible) { - if (selectionChanged || !m_LastRenderedCells.Contains(cell)) + if (!m_LastRenderedCells.Contains(cell)) RenderCellContent(cell.Item1, cell.Item2, ns, editorMode); } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Selection.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Selection.cs new file mode 100644 index 0000000..6f64a72 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Selection.cs @@ -0,0 +1,141 @@ +using UnityEngine; + +namespace XericUI.XTable.Rendering.Component +{ + public partial class XericUIActionTable + { + internal float TotalWidth => m_TotalWidth; + internal float TotalHeight => m_TotalHeight; + internal Color SelectionOverlayColor => Application.isPlaying ? Config.RuntimeSelectionColor : GetCellSelectionBgColor(m_DefaultStyleNamespace); + + internal void BeginSelection(int row, int col) + { + m_SelectionDragActive = true; + SelectionHandle.Select(row, col); + OnCellSelected?.Invoke(row, col); + } + + internal void UpdateSelectionDrag(RectTransform source, Vector2 screenPoint, Camera eventCamera) + { + if (!m_SelectionDragActive) return; + m_LastSelectionPointer = screenPoint; + m_SelectionPointerCamera = eventCamera; + if (TryGetCellFromScreenPoint(source, screenPoint, eventCamera, true, out int row, out int col)) + SelectionHandle.ExtendTo(row, col); + } + + internal void EndSelection() + { + m_SelectionDragActive = false; + m_SelectionPointerCamera = null; + } + + internal bool TryGetCellFromScreenPoint(RectTransform source, Vector2 screenPoint, Camera eventCamera, + bool clamp, out int row, out int col) + { + row = -1; + col = -1; + if (source == null || ContentTransform is not RectTransform contentRt) return false; + if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(source, screenPoint, eventCamera, out Vector2 sourceLocal)) + return false; + Vector3 world = source.TransformPoint(sourceLocal); + Vector3 contentLocal = contentRt.InverseTransformPoint(world); + Vector2 point = new( + contentLocal.x - contentRt.rect.xMin - m_ContentOffset, + contentRt.rect.yMax - contentLocal.y - m_ContentOffset); + return TryGetCellFromTablePoint(point, clamp, out row, out col); + } + + internal bool TryGetCellFromTablePoint(Vector2 point, bool clamp, out int row, out int col) + { + row = -1; + col = -1; + if (m_RowHeights == null || m_ColWidths == null || m_RowHeights.Length == 0 || m_ColWidths.Length == 0) + return false; + if (clamp) + { + point.x = Mathf.Clamp(point.x, 0f, Mathf.Max(0f, m_TotalWidth - .001f)); + point.y = Mathf.Clamp(point.y, 0f, Mathf.Max(0f, m_TotalHeight - .001f)); + } + else if (point.x < 0f || point.x >= m_TotalWidth || point.y < 0f || point.y >= m_TotalHeight) + return false; + + float cursor = 0f; + for (col = 0; col < m_ColWidths.Length; col++) + { + cursor += m_ColWidths[col]; + if (point.x < cursor) break; + } + cursor = 0f; + for (row = 0; row < m_RowHeights.Length; row++) + { + cursor += m_RowHeights[row]; + if (point.y < cursor) break; + } + if (row >= m_RowHeights.Length || col >= m_ColWidths.Length) return false; + NormalizeMergeSource(ref row, ref col); + return true; + } + + internal Rect GetTableRect(int rowStart, int rowEnd, int colStart, int colEnd) + { + return new Rect(GetColLeftX(colStart), GetRowTopY(rowStart), + GetColLeftX(colEnd) - GetColLeftX(colStart), GetRowTopY(rowEnd) - GetRowTopY(rowStart)); + } + + private void NormalizeMergeSource(ref int row, ref int col) + { + if (m_TableData == null || !m_TableData.IsCellMerged(row, col)) return; + for (int r = 0; r <= row; r++) + for (int c = 0; c <= col; c++) + if (m_TableData.IsMergeSource(r, c, out int rowSpan, out int colSpan) + && row < r + rowSpan && col < c + colSpan) + { + row = r; + col = c; + return; + } + } + + private void UpdateSelectionAutoScroll() + { + if (!m_SelectionDragActive || !m_EnableSelectionAutoScroll || m_ScrollRect?.viewport == null || m_ScrollRect.content == null) + return; + RectTransform viewport = m_ScrollRect.viewport; + if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(viewport, m_LastSelectionPointer, m_SelectionPointerCamera, out Vector2 local)) + return; + Rect rect = viewport.rect; + if (!rect.Contains(local)) return; + float x = local.x - rect.xMin; + float y = rect.yMax - local.y; + float freezeWidth = GetColLeftX(Mathf.Clamp(m_FreezeColCount, 0, m_ColWidths.Length)); + float freezeHeight = GetRowTopY(Mathf.Clamp(m_FreezeRowCount, 0, m_RowHeights.Length)); + if (x < freezeWidth || y < freezeHeight) return; + + float width = rect.width - freezeWidth; + float height = rect.height - freezeHeight; + float px = x - freezeWidth; + float py = y - freezeHeight; + float threshold = Mathf.Min(m_SelectionAutoScrollEdgeThreshold, Mathf.Min(width, height) * .5f); + float sx = GetAutoScrollSpeed(px, width, threshold); + float sy = GetAutoScrollSpeed(py, height, threshold); + if (Mathf.Approximately(sx, 0f) && Mathf.Approximately(sy, 0f)) return; + + var content = m_ScrollRect.content; + Vector2 position = content.anchoredPosition; + position.x = Mathf.Clamp(position.x + sx * Time.unscaledDeltaTime, 0f, Mathf.Max(0f, m_TotalWidth - width)); + position.y = Mathf.Clamp(position.y + sy * Time.unscaledDeltaTime, 0f, Mathf.Max(0f, m_TotalHeight - height)); + content.anchoredPosition = position; + UpdateSelectionDrag(viewport, m_LastSelectionPointer, m_SelectionPointerCamera); + } + + private float GetAutoScrollSpeed(float point, float length, float threshold) + { + float direction = point < threshold ? -1f : point > length - threshold ? 1f : 0f; + if (direction == 0f) return 0f; + float distance = direction < 0f ? point : length - point; + float strength = 1f - Mathf.Clamp01(distance / threshold); + return direction * m_SelectionAutoScrollMaxSpeed * Mathf.Pow(strength, m_SelectionAutoScrollAcceleration); + } + } +} diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Selection.cs.meta b/Runtime/XTable/Rendering/Component/XericUIActionTable.Selection.cs.meta new file mode 100644 index 0000000..0f30607 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Selection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 625bc0906554ad34eac6487aa103746c +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 5ba26d2..12c0bcb 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -23,7 +23,7 @@ namespace XericUI.XTable.Rendering.Component /// [ExecuteAlways] [AddComponentMenu("Xeric UI Vessel/Table/Xeric UI Action Table", 60)] - public partial class XericUIActionTable : XericUIBehaviour, IPointerClickHandler + public partial class XericUIActionTable : XericUIBehaviour { #region 序列化字段 @@ -53,6 +53,14 @@ namespace XericUI.XTable.Rendering.Component [Tooltip("在 Hierarchy 中显示动态生成的子对象(仅编辑器,不保存到场景)")] private bool m_ChildrenVisible; + [SerializeField] private bool m_EnableSelectionAutoScroll = true; + [SerializeField, Min(1f), Tooltip("框选自动滚动的 Content 边缘阈值(像素)")] + private float m_SelectionAutoScrollEdgeThreshold = 32f; + [SerializeField, Min(1f), Tooltip("框选自动滚动的最大速度(像素/秒)")] + private float m_SelectionAutoScrollMaxSpeed = 600f; + [SerializeField, Min(1f), Tooltip("框选自动滚动的边缘加速度指数")] + private float m_SelectionAutoScrollAcceleration = 1.5f; + #endregion #region 私有字段 @@ -69,6 +77,13 @@ namespace XericUI.XTable.Rendering.Component [System.NonSerialized] private RectTransform m_FrozenCornerContainer; [System.NonSerialized] private XTableSelectionHandle m_SelectionHandle; + [System.NonSerialized] private XTableSelectionOverlay m_NormalSelectionOverlay; + [System.NonSerialized] private XTableSelectionOverlay m_FrozenColumnSelectionOverlay; + [System.NonSerialized] private XTableSelectionOverlay m_FrozenRowSelectionOverlay; + [System.NonSerialized] private XTableSelectionOverlay m_FrozenCornerSelectionOverlay; + [System.NonSerialized] private bool m_SelectionDragActive; + [System.NonSerialized] private Vector2 m_LastSelectionPointer; + [System.NonSerialized] private Camera m_SelectionPointerCamera; [System.NonSerialized] private XTableBackgroundRenderer m_BackgroundRenderer; @@ -146,7 +161,10 @@ namespace XericUI.XTable.Rendering.Component get { if (m_SelectionHandle == null) + { m_SelectionHandle = new XTableSelectionHandle(); + m_SelectionHandle.OnSelectionChanged += OnHandleSelectionChanged; + } return m_SelectionHandle; } } @@ -318,6 +336,7 @@ namespace XericUI.XTable.Rendering.Component DestroyBackgroundRenderer(); DestroyFrozenRenderers(); + DestroySelectionOverlay(ref m_NormalSelectionOverlay); base.OnDestroy(); } @@ -327,6 +346,7 @@ namespace XericUI.XTable.Rendering.Component private void Update() { + UpdateSelectionAutoScroll(); if (m_IsRebuilding || !m_DirtyFlag.IsDirty) return; var flags = m_DirtyFlag.Flags; @@ -400,54 +420,5 @@ namespace XericUI.XTable.Rendering.Component #endregion - #region IPointerClickHandler - - public void OnPointerClick(PointerEventData eventData) - { - // 直接用 ContentTransform 做坐标转换:单元格是 Content 的子对象, - // 其 anchoredPosition 就在 Content 局部空间中,无需手工换算 scroll 偏移。 - RectTransform contentRt = ContentTransform as RectTransform; - if (contentRt == null) return; - - if (!RectTransformUtility.ScreenPointToLocalPointInRectangle( - contentRt, eventData.position, eventData.pressEventCamera, out Vector2 localPoint)) - return; - - // 转换为左上角坐标系(Content 的 anchor/pivot 都是 (0,1)) - float tableX = localPoint.x - contentRt.rect.xMin - m_ContentOffset; - float tableY = contentRt.rect.yMax - localPoint.y - m_ContentOffset; - - int col = -1; - float ax = 0; - for (int c = 0; c < m_ColWidths.Length; c++) - { - if (tableX >= ax && tableX < ax + m_ColWidths[c]) - { - col = c; - break; - } - ax += m_ColWidths[c]; - } - - int row = -1; - float ay = 0; - for (int r = 0; r < m_RowHeights.Length; r++) - { - if (tableY >= ay && tableY < ay + m_RowHeights[r]) - { - row = r; - break; - } - ay += m_RowHeights[r]; - } - - if (row >= 0 && col >= 0) - { - SelectCell(row, col); - OnCellSelected?.Invoke(row, col); - } - } - - #endregion } }