添加一个表格组件,原理上贴近现代表格系统,理论上可以应对超大型表格以及离散数据集合。
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7813b6f36b85854b8bfcae0874499d1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.XTable.Rendering.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 脏标记类型——标识表格需要刷新的变更类型。
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum TableDirtyType
|
||||
{
|
||||
None = 0,
|
||||
/// <summary>数据内容变更</summary>
|
||||
DataChanged = 1 << 0,
|
||||
/// <summary>视图范围变更(滚动/缩放)</summary>
|
||||
ViewChanged = 1 << 1,
|
||||
/// <summary>样式表变更</summary>
|
||||
StyleChanged = 1 << 2,
|
||||
/// <summary>选中状态变更</summary>
|
||||
SelectionChanged = 1 << 3,
|
||||
/// <summary>行列尺寸变更</summary>
|
||||
LayoutChanged = 1 << 4,
|
||||
/// <summary>全部脏(强制完整刷新)</summary>
|
||||
All = DataChanged | ViewChanged | StyleChanged | SelectionChanged | LayoutChanged
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 脏标记结构——追踪表格的刷新状态。
|
||||
/// </summary>
|
||||
public struct XTableDirtyFlag
|
||||
{
|
||||
/// <summary>当前脏标记集合</summary>
|
||||
public TableDirtyType Flags;
|
||||
|
||||
/// <summary>是否有任何脏标记</summary>
|
||||
public bool IsDirty => Flags != TableDirtyType.None;
|
||||
|
||||
/// <summary>标记一种脏类型</summary>
|
||||
public void Mark(TableDirtyType type)
|
||||
{
|
||||
Flags |= type;
|
||||
}
|
||||
|
||||
/// <summary>清除所有脏标记</summary>
|
||||
public void Clear()
|
||||
{
|
||||
Flags = TableDirtyType.None;
|
||||
}
|
||||
|
||||
/// <summary>检查是否包含特定脏类型</summary>
|
||||
public bool Has(TableDirtyType type)
|
||||
{
|
||||
return (Flags & type) == type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f87a16b53fa84c4a97b8ffd44aa2868
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.XTable.Rendering.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 单元格对象池——复用表格单元格 GameObject,避免频繁 Instantiate/Destroy。
|
||||
/// </summary>
|
||||
public class XTableObjectPool
|
||||
{
|
||||
/// <summary>回收池</summary>
|
||||
private readonly Stack<GameObject> m_Pool = new Stack<GameObject>();
|
||||
|
||||
/// <summary>当前活跃对象(已借出)</summary>
|
||||
private readonly HashSet<GameObject> m_Active = new HashSet<GameObject>();
|
||||
|
||||
/// <summary>预制体模板</summary>
|
||||
private readonly GameObject m_Prefab;
|
||||
|
||||
/// <summary>父级 Transform</summary>
|
||||
private readonly Transform m_Parent;
|
||||
|
||||
/// <summary>初始池容量</summary>
|
||||
private readonly int m_InitialCapacity;
|
||||
|
||||
public XTableObjectPool(GameObject prefab, Transform parent, int initialCapacity = 64)
|
||||
{
|
||||
m_Prefab = prefab;
|
||||
m_Parent = parent;
|
||||
m_InitialCapacity = initialCapacity;
|
||||
|
||||
// 预创建对象
|
||||
for (int i = 0; i < initialCapacity; i++)
|
||||
{
|
||||
GameObject obj = CreateNew();
|
||||
obj.SetActive(false);
|
||||
m_Pool.Push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从池中获取一个对象
|
||||
/// </summary>
|
||||
public GameObject Get()
|
||||
{
|
||||
GameObject obj;
|
||||
if (m_Pool.Count > 0)
|
||||
{
|
||||
obj = m_Pool.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
obj = CreateNew();
|
||||
}
|
||||
|
||||
obj.SetActive(true);
|
||||
m_Active.Add(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将对象归还到池中
|
||||
/// </summary>
|
||||
public void Return(GameObject obj)
|
||||
{
|
||||
if (obj == null) return;
|
||||
|
||||
obj.SetActive(false);
|
||||
m_Active.Remove(obj);
|
||||
m_Pool.Push(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 归还所有活跃对象
|
||||
/// </summary>
|
||||
public void ReturnAll()
|
||||
{
|
||||
foreach (var obj in m_Active)
|
||||
{
|
||||
if (obj != null)
|
||||
{
|
||||
obj.SetActive(false);
|
||||
m_Pool.Push(obj);
|
||||
}
|
||||
}
|
||||
m_Active.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理所有对象
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
ReturnAll();
|
||||
while (m_Pool.Count > 0)
|
||||
{
|
||||
var obj = m_Pool.Pop();
|
||||
if (obj != null)
|
||||
Object.Destroy(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>当前活跃对象数</summary>
|
||||
public int ActiveCount => m_Active.Count;
|
||||
|
||||
/// <summary>池中空闲对象数</summary>
|
||||
public int PoolCount => m_Pool.Count;
|
||||
|
||||
private GameObject CreateNew()
|
||||
{
|
||||
GameObject obj = m_Prefab != null
|
||||
? Object.Instantiate(m_Prefab, m_Parent)
|
||||
: new GameObject("TableCell", typeof(RectTransform));
|
||||
|
||||
obj.transform.SetParent(m_Parent, false);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae4a5754316035e4489de05e40389c9e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13944afb118d5de499192b64114ec4f2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 238b49eb1d61170498956f869ee62303
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,268 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using TMPro;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace XericUI.XTable.Rendering.Elements
|
||||
{
|
||||
/// <summary>
|
||||
/// 表格单元动态拼装器——通过对象池管理 Image + TMP_Text 的组合创建与回收。
|
||||
/// 所有元素统一使用 anchor=(0,1) pivot=(0,1) 左上角坐标系。
|
||||
/// 所有动态生成对象标记 HideFlags.DontSave | HideFlags.HideInHierarchy,防止泄漏到场景。
|
||||
/// </summary>
|
||||
public class CellAssembler
|
||||
{
|
||||
#region 对象池定义
|
||||
|
||||
public class PooledImage
|
||||
{
|
||||
public GameObject Root;
|
||||
public RectTransform Rect;
|
||||
public Image Image;
|
||||
public StyleBoundElement Style;
|
||||
}
|
||||
|
||||
public class PooledText
|
||||
{
|
||||
public GameObject Root;
|
||||
public RectTransform Rect;
|
||||
public TMP_Text Text;
|
||||
public StyleBoundElement Style;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 字段
|
||||
|
||||
private Transform m_Parent;
|
||||
private Transform m_PoolRoot;
|
||||
|
||||
private Stack<PooledImage> m_ImagePool = new Stack<PooledImage>();
|
||||
private Stack<PooledText> m_TextPool = new Stack<PooledText>();
|
||||
private List<PooledImage> m_ActiveImages = new List<PooledImage>();
|
||||
private List<PooledText> m_ActiveTexts = new List<PooledText>();
|
||||
|
||||
/// <summary>HideFlags 应用于所有动态创建的对象(不保存、不在 Hierarchy 显示)</summary>
|
||||
private const HideFlags POOL_FLAGS = HideFlags.DontSave | HideFlags.HideInHierarchy;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数与析构
|
||||
|
||||
public CellAssembler(Transform parent)
|
||||
{
|
||||
m_Parent = parent;
|
||||
|
||||
GameObject poolRoot = new GameObject("_CellPool");
|
||||
poolRoot.hideFlags = POOL_FLAGS;
|
||||
poolRoot.transform.SetParent(parent, false);
|
||||
poolRoot.SetActive(false);
|
||||
m_PoolRoot = poolRoot.transform;
|
||||
}
|
||||
|
||||
/// <summary>彻底销毁所有池对象(包括活跃和休眠的)</summary>
|
||||
public void DestroyAll()
|
||||
{
|
||||
// 杀死活跃对象
|
||||
for (int i = m_ActiveImages.Count - 1; i >= 0; i--)
|
||||
{
|
||||
KillObject(m_ActiveImages[i].Root);
|
||||
}
|
||||
m_ActiveImages.Clear();
|
||||
|
||||
for (int i = m_ActiveTexts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
KillObject(m_ActiveTexts[i].Root);
|
||||
}
|
||||
m_ActiveTexts.Clear();
|
||||
|
||||
// 杀死池中对象
|
||||
while (m_ImagePool.Count > 0)
|
||||
{
|
||||
KillObject(m_ImagePool.Pop().Root);
|
||||
}
|
||||
while (m_TextPool.Count > 0)
|
||||
{
|
||||
KillObject(m_TextPool.Pop().Root);
|
||||
}
|
||||
|
||||
// 杀死池根
|
||||
if (m_PoolRoot != null)
|
||||
{
|
||||
KillObject(m_PoolRoot.gameObject);
|
||||
m_PoolRoot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void KillObject(GameObject go)
|
||||
{
|
||||
if (go == null) return;
|
||||
if (Application.isPlaying)
|
||||
Object.Destroy(go);
|
||||
else
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 常量
|
||||
|
||||
private static readonly Vector2 ANCHOR_TOPLEFT_MIN = new Vector2(0, 1);
|
||||
private static readonly Vector2 ANCHOR_TOPLEFT_MAX = new Vector2(0, 1);
|
||||
private static readonly Vector2 PIVOT_TOPLEFT = new Vector2(0, 1);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 池操作
|
||||
|
||||
public PooledImage GetImage(string name = "cell_img")
|
||||
{
|
||||
PooledImage item;
|
||||
if (m_ImagePool.Count > 0)
|
||||
{
|
||||
item = m_ImagePool.Pop();
|
||||
item.Root.SetActive(true);
|
||||
item.Root.transform.SetParent(m_Parent, false);
|
||||
ResetImageDefaults(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
item = CreateImage(name);
|
||||
}
|
||||
m_ActiveImages.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public PooledText GetText(string name = "cell_text")
|
||||
{
|
||||
PooledText item;
|
||||
if (m_TextPool.Count > 0)
|
||||
{
|
||||
item = m_TextPool.Pop();
|
||||
item.Root.SetActive(true);
|
||||
item.Root.transform.SetParent(m_Parent, false);
|
||||
ResetTextDefaults(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
item = CreateText(name);
|
||||
}
|
||||
m_ActiveTexts.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public void ReturnAll()
|
||||
{
|
||||
foreach (var img in m_ActiveImages) ReturnImage(img);
|
||||
m_ActiveImages.Clear();
|
||||
|
||||
foreach (var txt in m_ActiveTexts) ReturnText(txt);
|
||||
m_ActiveTexts.Clear();
|
||||
}
|
||||
|
||||
private void ReturnImage(PooledImage item)
|
||||
{
|
||||
item.Style?.Unbind();
|
||||
item.Root.SetActive(false);
|
||||
item.Root.transform.SetParent(m_PoolRoot, false);
|
||||
m_ImagePool.Push(item);
|
||||
}
|
||||
|
||||
private void ReturnText(PooledText item)
|
||||
{
|
||||
item.Style?.Unbind();
|
||||
item.Text.text = "";
|
||||
item.Root.SetActive(false);
|
||||
item.Root.transform.SetParent(m_PoolRoot, false);
|
||||
m_TextPool.Push(item);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 默认值重置
|
||||
|
||||
private void ResetImageDefaults(PooledImage item)
|
||||
{
|
||||
item.Rect.anchorMin = ANCHOR_TOPLEFT_MIN;
|
||||
item.Rect.anchorMax = ANCHOR_TOPLEFT_MAX;
|
||||
item.Rect.pivot = PIVOT_TOPLEFT;
|
||||
item.Rect.anchoredPosition = Vector2.zero;
|
||||
item.Rect.sizeDelta = Vector2.zero;
|
||||
item.Image.color = Color.white;
|
||||
item.Image.sprite = null;
|
||||
item.Image.raycastTarget = false;
|
||||
}
|
||||
|
||||
private void ResetTextDefaults(PooledText item)
|
||||
{
|
||||
item.Rect.anchorMin = ANCHOR_TOPLEFT_MIN;
|
||||
item.Rect.anchorMax = ANCHOR_TOPLEFT_MAX;
|
||||
item.Rect.pivot = PIVOT_TOPLEFT;
|
||||
item.Rect.anchoredPosition = Vector2.zero;
|
||||
item.Rect.sizeDelta = Vector2.zero;
|
||||
item.Text.text = "";
|
||||
item.Text.color = Color.black;
|
||||
item.Text.fontSize = 14;
|
||||
item.Text.alignment = TextAlignmentOptions.Midline;
|
||||
item.Text.raycastTarget = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 创建
|
||||
|
||||
private PooledImage CreateImage(string name)
|
||||
{
|
||||
GameObject go = new GameObject(name,
|
||||
typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
go.hideFlags = POOL_FLAGS;
|
||||
go.transform.SetParent(m_Parent, false);
|
||||
|
||||
Image img = go.GetComponent<Image>();
|
||||
img.raycastTarget = false;
|
||||
|
||||
RectTransform rect = go.GetComponent<RectTransform>();
|
||||
rect.anchorMin = ANCHOR_TOPLEFT_MIN;
|
||||
rect.anchorMax = ANCHOR_TOPLEFT_MAX;
|
||||
rect.pivot = PIVOT_TOPLEFT;
|
||||
|
||||
return new PooledImage
|
||||
{
|
||||
Root = go,
|
||||
Rect = rect,
|
||||
Image = img,
|
||||
Style = new StyleBoundElement { ImageComponent = img }
|
||||
};
|
||||
}
|
||||
|
||||
private PooledText CreateText(string name)
|
||||
{
|
||||
GameObject go = new GameObject(name,
|
||||
typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
|
||||
go.hideFlags = POOL_FLAGS;
|
||||
go.transform.SetParent(m_Parent, false);
|
||||
|
||||
TMP_Text text = go.GetComponent<TMP_Text>();
|
||||
text.raycastTarget = false;
|
||||
text.alignment = TextAlignmentOptions.Midline;
|
||||
text.fontSize = 14;
|
||||
|
||||
RectTransform rect = go.GetComponent<RectTransform>();
|
||||
rect.anchorMin = ANCHOR_TOPLEFT_MIN;
|
||||
rect.anchorMax = ANCHOR_TOPLEFT_MAX;
|
||||
rect.pivot = PIVOT_TOPLEFT;
|
||||
|
||||
return new PooledText
|
||||
{
|
||||
Root = go,
|
||||
Rect = rect,
|
||||
Text = text,
|
||||
Style = new StyleBoundElement { TextComponent = text }
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9dd186d27001684f897c8ca38815c74
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,170 @@
|
||||
using TMPro;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericLibrary.Runtime.SuperStyleSheet;
|
||||
|
||||
namespace XericUI.XTable.Rendering.Elements
|
||||
{
|
||||
/// <summary>
|
||||
/// 样式绑定元素——将 Image 或 TMP_Text 挂勾到样式表路径。
|
||||
/// 自动从 StyleManager 拉取值并应用,样式变更时自动刷新。
|
||||
/// 通过对象池复用时调用 Bind/Unbind 切换绑定目标。
|
||||
/// </summary>
|
||||
public class StyleBoundElement
|
||||
{
|
||||
#region 字段
|
||||
|
||||
/// <summary>绑定的 Image 组件(可能为 null)</summary>
|
||||
public Image ImageComponent;
|
||||
|
||||
/// <summary>绑定的 TMP_Text 组件(可能为 null)</summary>
|
||||
public TMP_Text TextComponent;
|
||||
|
||||
/// <summary>样式命名空间</summary>
|
||||
private string m_StyleNamespace;
|
||||
|
||||
/// <summary>各路径的 StyleField 缓存</summary>
|
||||
private System.Collections.Generic.Dictionary<string, StyleField> m_Fields
|
||||
= new System.Collections.Generic.Dictionary<string, StyleField>();
|
||||
|
||||
/// <summary>是否已绑定</summary>
|
||||
private bool m_IsBound;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公开方法
|
||||
|
||||
/// <summary>
|
||||
/// 绑定到指定命名空间并开始监听样式。
|
||||
/// </summary>
|
||||
public void Bind(string styleNamespace)
|
||||
{
|
||||
if (m_IsBound) Unbind();
|
||||
m_StyleNamespace = styleNamespace;
|
||||
|
||||
if (string.IsNullOrEmpty(styleNamespace)) return;
|
||||
|
||||
// 预注册常用路径
|
||||
RegisterField("/cell/bg/color", OnStyleChanged);
|
||||
RegisterField("/cell/fg/color", OnStyleChanged);
|
||||
RegisterField("/cell/border/top/color", OnStyleChanged);
|
||||
RegisterField("/cell/border/bottom/color", OnStyleChanged);
|
||||
RegisterField("/cell/border/left/color", OnStyleChanged);
|
||||
RegisterField("/cell/border/right/color", OnStyleChanged);
|
||||
RegisterField("/cell/font/size", OnStyleChanged);
|
||||
RegisterField("/cell/font/alignment", OnStyleChanged);
|
||||
RegisterField("/cell/font/richText", OnStyleChanged);
|
||||
|
||||
m_IsBound = true;
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解绑并停止监听样式。
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
if (!m_IsBound) return;
|
||||
|
||||
foreach (var kvp in m_Fields)
|
||||
{
|
||||
if (kvp.Value != null)
|
||||
{
|
||||
kvp.Value.OnValueChanged -= OnStyleChanged;
|
||||
kvp.Value.Unregister();
|
||||
}
|
||||
}
|
||||
m_Fields.Clear();
|
||||
m_IsBound = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新全部样式值到组件。
|
||||
/// </summary>
|
||||
public void RefreshAll()
|
||||
{
|
||||
if (!m_IsBound || string.IsNullOrEmpty(m_StyleNamespace)) return;
|
||||
|
||||
// 背景颜色 → Image.color
|
||||
StyleValue bgVal = GetStyleValue("/cell/bg/color");
|
||||
if (bgVal != null && ImageComponent != null)
|
||||
ImageComponent.color = bgVal;
|
||||
|
||||
// 前景颜色 → Text.color
|
||||
StyleValue fgVal = GetStyleValue("/cell/fg/color");
|
||||
if (fgVal != null && TextComponent != null)
|
||||
TextComponent.color = fgVal;
|
||||
|
||||
// 字体大小
|
||||
StyleValue sizeVal = GetStyleValue("/cell/font/size");
|
||||
if (sizeVal != null && TextComponent != null)
|
||||
TextComponent.fontSize = (int)sizeVal;
|
||||
|
||||
// 对齐
|
||||
StyleValue alignVal = GetStyleValue("/cell/font/alignment");
|
||||
if (alignVal != null && TextComponent != null)
|
||||
TextComponent.alignment = alignVal.Convert<TextAlignmentOptions>();
|
||||
|
||||
// 富文本
|
||||
StyleValue richVal = GetStyleValue("/cell/font/richText");
|
||||
if (richVal != null && TextComponent != null)
|
||||
TextComponent.richText = (bool)richVal;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有方法
|
||||
|
||||
private void RegisterField(string path, System.Action<StyleValue> callback)
|
||||
{
|
||||
var field = new StyleField
|
||||
{
|
||||
Namespace = m_StyleNamespace,
|
||||
StylePath = path
|
||||
};
|
||||
field.Register();
|
||||
field.OnValueChanged += callback;
|
||||
m_Fields[path] = field;
|
||||
}
|
||||
|
||||
private StyleValue GetStyleValue(string path)
|
||||
{
|
||||
return StyleManager.GetValue(m_StyleNamespace, path);
|
||||
}
|
||||
|
||||
private void OnStyleChanged(StyleValue val)
|
||||
{
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于 TextAnchor → TextAlignmentOptions 的转换扩展。
|
||||
/// </summary>
|
||||
internal static class StyleBoundElementExtensions
|
||||
{
|
||||
public static TextAlignmentOptions Convert<T>(this StyleValue val)
|
||||
{
|
||||
string s = val?.GetValueAs<string>();
|
||||
if (string.IsNullOrEmpty(s)) return TextAlignmentOptions.Midline;
|
||||
|
||||
switch (s)
|
||||
{
|
||||
case "UpperLeft": return TextAlignmentOptions.TopLeft;
|
||||
case "UpperCenter": return TextAlignmentOptions.Top;
|
||||
case "UpperRight": return TextAlignmentOptions.TopRight;
|
||||
case "MiddleLeft": return TextAlignmentOptions.MidlineLeft;
|
||||
case "MiddleCenter": return TextAlignmentOptions.Midline;
|
||||
case "MiddleRight": return TextAlignmentOptions.MidlineRight;
|
||||
case "LowerLeft": return TextAlignmentOptions.BottomLeft;
|
||||
case "LowerCenter": return TextAlignmentOptions.Bottom;
|
||||
case "LowerRight": return TextAlignmentOptions.BottomRight;
|
||||
default: return TextAlignmentOptions.Midline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd19dc2a8b3f43348a76e2d1cb8e228e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7367518f7495815439e1a32ee820f1c5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.XTable.Rendering.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// UI Toolkit 渲染器(预留)——后续实现表格在 UI Toolkit 上的渲染。
|
||||
/// 使用 Unity 原生 CSS 样式表。
|
||||
/// </summary>
|
||||
public class XTableUITKRenderer
|
||||
{
|
||||
// TODO: 实现 UI Toolkit 渲染逻辑
|
||||
// - 使用 VisualElement 构建表格
|
||||
// - 通过 USS (Unity Style Sheet) 控制样式
|
||||
// - 实现虚拟化滚动
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b2e42673c88a2e40853175ade86ec7b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,107 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.XTable.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// 左上角坐标系的视图矩形。
|
||||
/// Unity 的 Rect 是左下角原点(Y-up),表格数据索引是左上角原点(Y-down)。
|
||||
/// 此类负责坐标转换和可见单元格范围计算。
|
||||
/// </summary>
|
||||
public struct XTableViewRect
|
||||
{
|
||||
/// <summary>左上角 X 坐标</summary>
|
||||
public float X;
|
||||
|
||||
/// <summary>左上角 Y 坐标(从上到下递增)</summary>
|
||||
public float Y;
|
||||
|
||||
/// <summary>视图宽度</summary>
|
||||
public float Width;
|
||||
|
||||
/// <summary>视图高度</summary>
|
||||
public float Height;
|
||||
|
||||
/// <summary>右边界</summary>
|
||||
public float XMax => X + Width;
|
||||
|
||||
/// <summary>下边界</summary>
|
||||
public float YMax => Y + Height;
|
||||
|
||||
/// <summary>
|
||||
/// 从 Unity Rect(左下角原点)转换为 TableViewRect(左上角原点)
|
||||
/// </summary>
|
||||
/// <param name="unityRect">Unity 坐标系下的矩形</param>
|
||||
/// <param name="containerHeight">容器总高度(用于 Y 轴翻转)</param>
|
||||
public static XTableViewRect FromUnityRect(Rect unityRect, float containerHeight)
|
||||
{
|
||||
return new XTableViewRect
|
||||
{
|
||||
X = unityRect.xMin,
|
||||
Y = containerHeight - unityRect.yMax,
|
||||
Width = unityRect.width,
|
||||
Height = unityRect.height
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为 Unity Rect(左下角原点)
|
||||
/// </summary>
|
||||
public Rect ToUnityRect(float containerHeight)
|
||||
{
|
||||
return new Rect(X, containerHeight - Y - Height, Width, Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过行列尺寸计算可见单元格范围。
|
||||
/// 累加行高/列宽直到超出视图范围。
|
||||
/// </summary>
|
||||
/// <param name="rowHeights">每行高度数组</param>
|
||||
/// <param name="colWidths">每列宽度数组</param>
|
||||
/// <param name="startRow">可见起始行(包含)</param>
|
||||
/// <param name="endRow">可见结束行(不包含)</param>
|
||||
/// <param name="startCol">可见起始列(包含)</param>
|
||||
/// <param name="endCol">可见结束列(不包含)</param>
|
||||
public void GetVisibleCellRange(
|
||||
float[] rowHeights, float[] colWidths,
|
||||
out int startRow, out int endRow,
|
||||
out int startCol, out int endCol)
|
||||
{
|
||||
startRow = 0;
|
||||
endRow = 0;
|
||||
startCol = 0;
|
||||
endCol = 0;
|
||||
|
||||
if (rowHeights == null || colWidths == null) return;
|
||||
|
||||
// 计算行范围(Y 方向,从上到下)
|
||||
float accumulatedY = 0f;
|
||||
for (int r = 0; r < rowHeights.Length; r++)
|
||||
{
|
||||
float rowBottom = accumulatedY + rowHeights[r];
|
||||
if (rowBottom > Y && accumulatedY < YMax)
|
||||
{
|
||||
if (startRow == 0 && accumulatedY < YMax)
|
||||
startRow = r;
|
||||
endRow = r + 1;
|
||||
}
|
||||
accumulatedY = rowBottom;
|
||||
if (accumulatedY > YMax) break;
|
||||
}
|
||||
|
||||
// 计算列范围(X 方向,从左到右)
|
||||
float accumulatedX = 0f;
|
||||
for (int c = 0; c < colWidths.Length; c++)
|
||||
{
|
||||
float colRight = accumulatedX + colWidths[c];
|
||||
if (colRight > X && accumulatedX < XMax)
|
||||
{
|
||||
if (startCol == 0 && accumulatedX < XMax)
|
||||
startCol = c;
|
||||
endCol = c + 1;
|
||||
}
|
||||
accumulatedX = colRight;
|
||||
if (accumulatedX > XMax) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f64af1d0c0781de499619595c05d0061
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user