添加一个表格组件,原理上贴近现代表格系统,理论上可以应对超大型表格以及离散数据集合。
This commit is contained in:
@@ -0,0 +1,572 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using TMPro;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericUI.Core.Base;
|
||||
using XericUI.XTable.Core;
|
||||
using XericUI.XTable.Rendering.Elements;
|
||||
using XericLibrary.Runtime.SuperStyleSheet;
|
||||
|
||||
namespace XericUI.XTable.Rendering.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 表格组件——纯数据渲染器。
|
||||
/// 一张全幅背景 + 按需拼装单元格文本/图片。
|
||||
/// 无行列标题、无样式差异分割线——所有内容由数据定义。
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
[AddComponentMenu("Xeric UI Vessel/Table/Xeric UI Action Table", 60)]
|
||||
public class XericUIActionTable : XericUIBehaciour, IPointerClickHandler
|
||||
{
|
||||
#region 常量
|
||||
|
||||
private const string DEFAULT_STYLE_NS = "xeric_table_default";
|
||||
|
||||
#endregion
|
||||
|
||||
#region 序列化字段
|
||||
|
||||
[SerializeField] private XTableData m_TableData;
|
||||
|
||||
[SerializeField] private float[] m_RowHeights = new float[] { 40f, 30f };
|
||||
|
||||
[SerializeField] private float[] m_ColWidths = new float[] { 100f, 80f, 120f };
|
||||
|
||||
[SerializeField] private string m_DefaultStyleNamespace = DEFAULT_STYLE_NS;
|
||||
|
||||
[SerializeField] private ScrollRect m_ScrollRect;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有字段
|
||||
|
||||
[System.NonSerialized] private CellAssembler m_Assembler;
|
||||
[System.NonSerialized] private XTableDirtyFlag m_DirtyFlag;
|
||||
|
||||
private int m_SelectedRow = -1;
|
||||
private int m_SelectedCol = -1;
|
||||
[System.NonSerialized] private GameObject m_BackgroundGo;
|
||||
|
||||
private float m_TotalWidth;
|
||||
private float m_TotalHeight;
|
||||
|
||||
// ScrollRect 可见范围
|
||||
private Rect m_VisibleRect;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件
|
||||
|
||||
public event Action<int, int> OnCellSelected;
|
||||
public event Action OnTableRefreshed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公开属性
|
||||
|
||||
public XTableData TableData
|
||||
{
|
||||
get => m_TableData;
|
||||
set { m_TableData = value; MarkDirty(TableDirtyType.DataChanged); }
|
||||
}
|
||||
|
||||
public float[] RowHeights => m_RowHeights;
|
||||
public float[] ColWidths => m_ColWidths;
|
||||
|
||||
public string DefaultStyleNamespace => m_DefaultStyleNamespace;
|
||||
public int SelectedRow => m_SelectedRow;
|
||||
public int SelectedCol => m_SelectedCol;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
if (m_TableData == null)
|
||||
m_TableData = new XTableData();
|
||||
|
||||
m_Assembler = new CellAssembler(rectTransform);
|
||||
RecalculateTotalSize();
|
||||
EnsureDefaultStyleNamespace();
|
||||
CreateDefaultSampleData();
|
||||
CreateBackground();
|
||||
|
||||
if (m_ScrollRect != null)
|
||||
m_ScrollRect.onValueChanged.AddListener(OnScrollValueChanged);
|
||||
|
||||
if (!Application.isPlaying)
|
||||
SubscribeEditorUpdate();
|
||||
|
||||
MarkDirty(TableDirtyType.All);
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
EnsureDefaultStyleNamespace();
|
||||
MarkDirty(TableDirtyType.All);
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
base.OnDisable();
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
if (m_ScrollRect != null)
|
||||
m_ScrollRect.onValueChanged.RemoveListener(OnScrollValueChanged);
|
||||
UnsubscribeEditorUpdate();
|
||||
|
||||
// 销毁所有动态生成的对象
|
||||
if (m_Assembler != null)
|
||||
m_Assembler.DestroyAll();
|
||||
if (m_BackgroundGo != null)
|
||||
{
|
||||
if (Application.isPlaying) Destroy(m_BackgroundGo);
|
||||
else DestroyImmediate(m_BackgroundGo);
|
||||
m_BackgroundGo = null;
|
||||
}
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (m_DirtyFlag.IsDirty) RefreshView();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 默认示例数据
|
||||
|
||||
private bool m_DefaultDataCreated;
|
||||
|
||||
private void CreateDefaultSampleData()
|
||||
{
|
||||
if (m_DefaultDataCreated) return;
|
||||
m_DefaultDataCreated = true;
|
||||
|
||||
string[,] sample = new string[2, 3]
|
||||
{
|
||||
{ "Name", "Age", "City" },
|
||||
{ "Alice", "25", "NYC" }
|
||||
};
|
||||
|
||||
for (int r = 0; r < 2; r++)
|
||||
for (int c = 0; c < 3; c++)
|
||||
SetCellTextInternal(r, c, sample[r, c], skipDirty: true);
|
||||
}
|
||||
|
||||
private void SetCellTextInternal(int row, int col, string text, bool skipDirty)
|
||||
{
|
||||
var data = m_TableData.GetCell(row, col);
|
||||
if (data == null)
|
||||
{
|
||||
data = new XTableCellData();
|
||||
m_TableData.SetCell(row, col, data);
|
||||
}
|
||||
data.Text = text;
|
||||
if (!skipDirty) MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 编辑器模式支持
|
||||
|
||||
[System.NonSerialized] private bool m_EditorUpdateSubscribed;
|
||||
|
||||
private void SubscribeEditorUpdate()
|
||||
{
|
||||
if (m_EditorUpdateSubscribed) return;
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.update += EditorUpdate;
|
||||
m_EditorUpdateSubscribed = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
private void UnsubscribeEditorUpdate()
|
||||
{
|
||||
if (!m_EditorUpdateSubscribed) return;
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.update -= EditorUpdate;
|
||||
m_EditorUpdateSubscribed = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void EditorUpdate()
|
||||
{
|
||||
if (this == null || !isActiveAndEnabled)
|
||||
{
|
||||
UnsubscribeEditorUpdate();
|
||||
return;
|
||||
}
|
||||
if (m_DirtyFlag.IsDirty)
|
||||
RefreshView();
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公开方法
|
||||
|
||||
public void SetCellData(int row, int col, XTableCellData data)
|
||||
{
|
||||
m_TableData.SetCell(row, col, data);
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
public void SetCellText(int row, int col, string text)
|
||||
{
|
||||
SetCellTextInternal(row, col, text, skipDirty: false);
|
||||
}
|
||||
|
||||
public XTableCellData GetCellData(int row, int col)
|
||||
=> m_TableData?.GetCell(row, col);
|
||||
|
||||
public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan)
|
||||
{
|
||||
m_TableData.MergeCells(startRow, startCol, rowSpan, colSpan);
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
public void UnmergeCells(int startRow, int startCol)
|
||||
{
|
||||
m_TableData.UnmergeCells(startRow, startCol);
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
public void SelectCell(int row, int col)
|
||||
{
|
||||
m_SelectedRow = row;
|
||||
m_SelectedCol = col;
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
m_SelectedRow = -1;
|
||||
m_SelectedCol = -1;
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
public void RefreshView()
|
||||
{
|
||||
if (m_TableData == null || m_Assembler == null) 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
|
||||
|
||||
RecalculateTotalSize();
|
||||
UpdateVisibleRect();
|
||||
|
||||
// 更新背景尺寸
|
||||
if (m_BackgroundGo != null)
|
||||
{
|
||||
var bgRt = m_BackgroundGo.GetComponent<RectTransform>();
|
||||
bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight);
|
||||
}
|
||||
|
||||
// 回收池
|
||||
m_Assembler.ReturnAll();
|
||||
|
||||
// 计算可见范围
|
||||
int sr, er, sc, ec;
|
||||
CalcVisibleRange(out sr, out er, out sc, out ec);
|
||||
|
||||
if (er <= sr || ec <= sc) return;
|
||||
|
||||
// 渲染每个可见单元格
|
||||
RenderCells(sr, er, sc, ec);
|
||||
|
||||
m_DirtyFlag.Clear();
|
||||
OnTableRefreshed?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 背景
|
||||
|
||||
private void CreateBackground()
|
||||
{
|
||||
if (m_BackgroundGo != null) return;
|
||||
|
||||
m_BackgroundGo = new GameObject("_TableBackground",
|
||||
typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
m_BackgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
|
||||
m_BackgroundGo.transform.SetParent(rectTransform, false);
|
||||
|
||||
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(m_TotalWidth, m_TotalHeight);
|
||||
|
||||
var bgImg = m_BackgroundGo.GetComponent<Image>();
|
||||
bgImg.color = new Color(0.18f, 0.18f, 0.18f, 1f);
|
||||
bgImg.raycastTarget = false;
|
||||
}
|
||||
|
||||
/// <summary>完全重建表格——销毁所有动态生成的对象并重新初始化</summary>
|
||||
[ContextMenu("Xeric UI/完全重建表格")]
|
||||
public void FullRebuild()
|
||||
{
|
||||
m_Assembler?.DestroyAll();
|
||||
m_DefaultDataCreated = false;
|
||||
|
||||
if (m_BackgroundGo != null)
|
||||
{
|
||||
if (Application.isPlaying) Destroy(m_BackgroundGo);
|
||||
else DestroyImmediate(m_BackgroundGo);
|
||||
m_BackgroundGo = null;
|
||||
}
|
||||
|
||||
m_TableData = new XTableData();
|
||||
|
||||
m_Assembler = new CellAssembler(rectTransform);
|
||||
RecalculateTotalSize();
|
||||
CreateDefaultSampleData();
|
||||
CreateBackground();
|
||||
MarkDirty(TableDirtyType.All);
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 单元格渲染
|
||||
|
||||
private void RenderCells(int sr, int er, int sc, int ec)
|
||||
{
|
||||
Color fgColor = GetStyleColor(m_DefaultStyleNamespace, "/cell/fg/color",
|
||||
new Color(0.05f, 0.05f, 0.05f));
|
||||
Color selectionColor = new Color(0.18f, 0.42f, 0.82f, 0.25f);
|
||||
int fontSize = GetStyleInt(m_DefaultStyleNamespace, "/cell/font/size", 14);
|
||||
|
||||
for (int r = sr; r < er && r < m_RowHeights.Length; r++)
|
||||
{
|
||||
for (int c = sc; c < ec && c < m_ColWidths.Length; c++)
|
||||
{
|
||||
float cellX = GetColLeftX(c);
|
||||
float cellY = -GetRowTopY(r);
|
||||
float cellW = m_ColWidths[c];
|
||||
float cellH = m_RowHeights[r];
|
||||
XTableCellData data = m_TableData.GetCell(r, c);
|
||||
|
||||
// 选中高亮背景
|
||||
if (r == m_SelectedRow && c == m_SelectedCol)
|
||||
{
|
||||
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, 1f);
|
||||
hLine.Image.color = new Color(0.35f, 0.35f, 0.35f, 0.5f);
|
||||
|
||||
// 垂直网格线
|
||||
var vLine = m_Assembler.GetImage("cell_vline");
|
||||
vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - 1, cellY);
|
||||
vLine.Rect.sizeDelta = new Vector2(1f, cellH);
|
||||
vLine.Image.color = new Color(0.35f, 0.35f, 0.35f, 0.5f);
|
||||
|
||||
if (data == null) continue;
|
||||
|
||||
// 文本:与 cell 同位置同尺寸,TMP 内部 Midline 对齐实现居中
|
||||
if (!string.IsNullOrEmpty(data.Text))
|
||||
{
|
||||
var txt = m_Assembler.GetText("cell_text");
|
||||
txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY);
|
||||
txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH);
|
||||
txt.Text.text = data.Text;
|
||||
txt.Text.color = fgColor;
|
||||
txt.Text.fontSize = fontSize;
|
||||
txt.Text.alignment = TextAlignmentOptions.Midline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 坐标计算
|
||||
|
||||
private float GetRowTopY(int row)
|
||||
{
|
||||
float y = 0;
|
||||
for (int i = 0; i < row && i < m_RowHeights.Length; i++)
|
||||
y += m_RowHeights[i];
|
||||
return y;
|
||||
}
|
||||
|
||||
private float GetColLeftX(int col)
|
||||
{
|
||||
float x = 0;
|
||||
for (int i = 0; i < col && i < m_ColWidths.Length; i++)
|
||||
x += m_ColWidths[i];
|
||||
return x;
|
||||
}
|
||||
|
||||
private void RecalculateTotalSize()
|
||||
{
|
||||
m_TotalWidth = 0;
|
||||
if (m_ColWidths != null)
|
||||
foreach (float w in m_ColWidths) m_TotalWidth += w;
|
||||
|
||||
m_TotalHeight = 0;
|
||||
if (m_RowHeights != null)
|
||||
foreach (float h in m_RowHeights) m_TotalHeight += h;
|
||||
|
||||
if (m_ScrollRect != null && m_ScrollRect.content != null)
|
||||
m_ScrollRect.content.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight);
|
||||
}
|
||||
|
||||
private void UpdateVisibleRect()
|
||||
{
|
||||
m_VisibleRect = new Rect(0, 0, m_TotalWidth, m_TotalHeight);
|
||||
|
||||
if (m_ScrollRect != null && m_ScrollRect.viewport != null)
|
||||
{
|
||||
RectTransform vt = m_ScrollRect.viewport;
|
||||
RectTransform ct = m_ScrollRect.content;
|
||||
if (vt != null && ct != null)
|
||||
{
|
||||
float sx = Mathf.Max(0, -ct.anchoredPosition.x);
|
||||
float sy = Mathf.Max(0, ct.anchoredPosition.y);
|
||||
m_VisibleRect = new Rect(sx, sy, vt.rect.width, vt.rect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CalcVisibleRange(out int sr, out int er, out int sc, out int ec)
|
||||
{
|
||||
sr = 0; er = 0; sc = 0; ec = 0;
|
||||
|
||||
float accY = 0;
|
||||
for (int r = 0; r < m_RowHeights.Length; r++)
|
||||
{
|
||||
float rowBottom = accY + m_RowHeights[r];
|
||||
if (rowBottom > m_VisibleRect.y && accY < m_VisibleRect.yMax)
|
||||
{
|
||||
if (er == 0 && sr == 0) sr = r;
|
||||
er = r + 1;
|
||||
}
|
||||
accY = rowBottom;
|
||||
if (accY > m_VisibleRect.yMax) break;
|
||||
}
|
||||
|
||||
float accX = 0;
|
||||
for (int c = 0; c < m_ColWidths.Length; c++)
|
||||
{
|
||||
float colRight = accX + m_ColWidths[c];
|
||||
if (colRight > m_VisibleRect.x && accX < m_VisibleRect.xMax)
|
||||
{
|
||||
if (ec == 0 && sc == 0) sc = c;
|
||||
ec = c + 1;
|
||||
}
|
||||
accX = colRight;
|
||||
if (accX > m_VisibleRect.xMax) break;
|
||||
}
|
||||
|
||||
if (sr < 0) sr = 0;
|
||||
if (sc < 0) sc = 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 样式
|
||||
|
||||
private Color GetStyleColor(string ns, string path, Color fallback)
|
||||
{
|
||||
StyleValue val = StyleManager.GetValue(ns, path);
|
||||
return val != null ? (Color)val : fallback;
|
||||
}
|
||||
|
||||
private int GetStyleInt(string ns, string path, int fallback)
|
||||
{
|
||||
StyleValue val = StyleManager.GetValue(ns, path);
|
||||
return val != null ? (int)val : fallback;
|
||||
}
|
||||
|
||||
private void EnsureDefaultStyleNamespace()
|
||||
{
|
||||
if (!StyleManager.HasStyle(m_DefaultStyleNamespace, "/cell/bg/color"))
|
||||
{
|
||||
StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/bg/color",
|
||||
new Color(0.18f, 0.18f, 0.18f));
|
||||
StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/fg/color",
|
||||
new Color(0.05f, 0.05f, 0.05f));
|
||||
StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/font/size", 14);
|
||||
StyleManager.ForceRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 其他
|
||||
|
||||
private void MarkDirty(TableDirtyType type) => m_DirtyFlag.Mark(type);
|
||||
|
||||
private void OnScrollValueChanged(Vector2 _) => MarkDirty(TableDirtyType.ViewChanged);
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPointerClickHandler
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
rectTransform, eventData.position, eventData.pressEventCamera, out Vector2 localPoint);
|
||||
|
||||
// rectTransform 锚点在左上角,localPoint 以中心为原点
|
||||
float tableX = localPoint.x - rectTransform.rect.xMin;
|
||||
float tableY = rectTransform.rect.yMax - localPoint.y;
|
||||
|
||||
float scrollY = m_ScrollRect != null ? Mathf.Max(0, m_ScrollRect.content.anchoredPosition.y) : 0;
|
||||
float scrollX = m_ScrollRect != null ? Mathf.Max(0, -m_ScrollRect.content.anchoredPosition.x) : 0;
|
||||
tableX += scrollX;
|
||||
tableY += scrollY;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user