添加一个表格组件,原理上贴近现代表格系统,理论上可以应对超大型表格以及离散数据集合。

This commit is contained in:
2026-06-26 19:14:48 +08:00
parent 55ace8d02b
commit 61bb0b012e
78 changed files with 4409 additions and 57 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 186fffef629f713499774b802be4f587
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+90
View File
@@ -0,0 +1,90 @@
using System.Collections.Generic;
namespace XericUI.XTable.Core
{
/// <summary>
/// 单个数据块——保存 BlockSizeX × BlockSizeY 个单元格数据。
/// 数据使用线性数组存储,索引 = localRow * BlockSizeX + localCol。
/// </summary>
public class XTableBlock
{
/// <summary>块在全局块坐标系中的行号</summary>
public int BlockRow;
/// <summary>块在全局块坐标系中的列号</summary>
public int BlockCol;
/// <summary>块内单元格列数</summary>
public int BlockSizeX;
/// <summary>块内单元格行数</summary>
public int BlockSizeY;
/// <summary>
/// 数据存储——线性数组。
/// 索引 = localRow * BlockSizeX + localCol。
/// </summary>
public XTableCellData[] Cells;
/// <summary>合并单元格描述列表</summary>
public List<XTableMergeDescriptor> MergeDescriptors;
/// <summary>
/// 创建指定尺寸的块
/// </summary>
public XTableBlock(int blockSizeX, int blockSizeY)
{
BlockSizeX = blockSizeX;
BlockSizeY = blockSizeY;
Cells = new XTableCellData[blockSizeX * blockSizeY];
MergeDescriptors = new List<XTableMergeDescriptor>();
}
/// <summary>
/// 创建指定尺寸和位置的块
/// </summary>
public XTableBlock(int blockSizeX, int blockSizeY, int blockRow, int blockCol)
: this(blockSizeX, blockSizeY)
{
BlockRow = blockRow;
BlockCol = blockCol;
}
/// <summary>
/// 通过块内局部坐标获取单元格数据
/// </summary>
public XTableCellData GetCell(int localRow, int localCol)
{
int index = localRow * BlockSizeX + localCol;
if (index < 0 || index >= Cells.Length) return null;
return Cells[index];
}
/// <summary>
/// 通过块内局部坐标设置单元格数据
/// </summary>
public void SetCell(int localRow, int localCol, XTableCellData data)
{
int index = localRow * BlockSizeX + localCol;
if (index < 0 || index >= Cells.Length) return;
Cells[index] = data;
}
/// <summary>
/// 检查是否有合并描述重定向此单元格
/// </summary>
/// <returns>合并描述,若无重定向则返回 null</returns>
public XTableMergeDescriptor GetMergeRedirect(int localRow, int localCol)
{
if (MergeDescriptors == null || MergeDescriptors.Count == 0)
return null;
for (int i = 0; i < MergeDescriptors.Count; i++)
{
if (MergeDescriptors[i].Contains(localRow, localCol))
return MergeDescriptors[i];
}
return null;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa9a14d1a32e7fe4f95c29a0c18deddd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+48
View File
@@ -0,0 +1,48 @@
using System;
using UnityEngine;
namespace XericUI.XTable.Core
{
/// <summary>
/// 单元格数据模型——仅保存数据内容,不保存自身尺寸。
/// 尺寸由渲染阶段的行列标题(行高/列宽)决定。
/// 样式通过 StyleNamespace 字符串引用 StyleManager 中的命名空间。
/// </summary>
[Serializable]
public class XTableCellData
{
/// <summary>文本内容</summary>
public string Text;
/// <summary>图片纹理</summary>
public Texture2D Image;
/// <summary>预制体对象(用于嵌入复杂 UI 元素)</summary>
public GameObject Prefab;
/// <summary>
/// 样式命名空间——对应 StyleManager 中的命名空间。
/// 为空或 "xeric_table_default" 时使用默认表格样式。
/// 渲染时从此命名空间读取 /cell/bg/color、/cell/font/size 等路径。
/// </summary>
public string StyleNamespace;
public XTableCellData() { }
public XTableCellData(string text)
{
Text = text;
}
public XTableCellData(string text, string styleNamespace = null) : this(text)
{
StyleNamespace = styleNamespace;
}
public XTableCellData(string text, Texture2D image, string styleNamespace = null) : this(text, styleNamespace)
{
Image = image;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e783197e3dfc5a5489b99e27504e9cf3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+247
View File
@@ -0,0 +1,247 @@
using System.Collections.Generic;
using XericUI.XTable.Mapping;
namespace XericUI.XTable.Core
{
/// <summary>
/// 表格数据主类——持有块字典,提供单元格级别的读写接口。
/// 块按需创建(SetCell 时自动创建不存在的块),通过 Z 曲线索引映射。
/// </summary>
public class XTableData
{
#region 属性
/// <summary>块内列数(默认 32)</summary>
public int BlockSizeX { get; private set; }
/// <summary>块内行数(默认 32)</summary>
public int BlockSizeY { get; private set; }
/// <summary>
/// 块字典——key = Z 曲线索引(ulong),value = 数据块。
/// 具有哈希表性质,按需创建,键不连续(离散存储)。
/// </summary>
public Dictionary<ulong, XTableBlock> BlockMap { get; private set; }
#endregion
#region 构造函数
/// <summary>
/// 创建表格数据实例
/// </summary>
/// <param name="blockSizeX">块内列数,默认 32</param>
/// <param name="blockSizeY">块内行数,默认 32</param>
public XTableData(int blockSizeX = 32, int blockSizeY = 32)
{
BlockSizeX = blockSizeX > 0 ? blockSizeX : 32;
BlockSizeY = blockSizeY > 0 ? blockSizeY : 32;
BlockMap = new Dictionary<ulong, XTableBlock>();
}
#endregion
#region 核心访问方法
/// <summary>
/// 通过全局行列坐标获取单元格数据。
/// 先计算块坐标和 Z 索引,查找块字典,再检查合并重定向。
/// </summary>
/// <returns>单元格数据,不存在则返回 null</returns>
public XTableCellData GetCell(int row, int col)
{
XTableCoordinateUtility.CellToBlockCoordinate(
row, col, BlockSizeX, BlockSizeY,
out int blockRow, out int blockCol,
out int localRow, out int localCol);
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
if (!BlockMap.TryGetValue(zIndex, out XTableBlock block))
return null;
// 检查合并重定向
XTableMergeDescriptor merge = block.GetMergeRedirect(localRow, localCol);
if (merge != null)
{
merge.GetRedirectTarget(out ulong targetBlockIndex, out int targetRow, out int targetCol);
if (merge.IsCrossBlock)
{
// 跨块引用:跳转到目标块
if (!BlockMap.TryGetValue(targetBlockIndex, out XTableBlock targetBlock))
return null;
return targetBlock.GetCell(targetRow, targetCol);
}
else
{
// 本地引用:同块内重定向
return block.GetCell(targetRow, targetCol);
}
}
return block.GetCell(localRow, localCol);
}
/// <summary>
/// 尝试获取单元格数据
/// </summary>
public bool TryGetCell(int row, int col, out XTableCellData data)
{
data = GetCell(row, col);
return data != null;
}
/// <summary>
/// 通过全局行列坐标设置单元格数据。
/// 如果对应的块不存在,则按需创建。
/// </summary>
public void SetCell(int row, int col, XTableCellData data)
{
XTableCoordinateUtility.CellToBlockCoordinate(
row, col, BlockSizeX, BlockSizeY,
out int blockRow, out int blockCol,
out int localRow, out int localCol);
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
if (!BlockMap.TryGetValue(zIndex, out XTableBlock block))
{
block = new XTableBlock(BlockSizeX, BlockSizeY, blockRow, blockCol);
BlockMap[zIndex] = block;
}
block.SetCell(localRow, localCol, data);
}
#endregion
#region 合并单元格
/// <summary>
/// 合并从 (startRow, startCol) 开始、跨越 rowSpan 行和 colSpan 列的矩形区域。
/// 合并源为左上角单元格 (startRow, startCol)。
/// 如果合并区域跨越多个块,则每个涉及的块都会创建对应的 MergeDescriptor。
/// </summary>
public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan)
{
// 确保合并源单元格存在
XTableCoordinateUtility.CellToBlockCoordinate(
startRow, startCol, BlockSizeX, BlockSizeY,
out int srcBlockRow, out int srcBlockCol,
out int srcLocalRow, out int srcLocalCol);
ulong srcZIndex = XTableBlockMapping.BlockCoordToZIndex(srcBlockRow, srcBlockCol);
// 遍历合并区域内的所有单元格,分块处理
for (int r = startRow; r < startRow + rowSpan; r++)
{
for (int c = startCol; c < startCol + colSpan; c++)
{
// 跳过合并源自身
if (r == startRow && c == startCol) continue;
XTableCoordinateUtility.CellToBlockCoordinate(
r, c, BlockSizeX, BlockSizeY,
out int blockRow, out int blockCol,
out int localRow, out int localCol);
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
// 确保块存在
if (!BlockMap.TryGetValue(zIndex, out XTableBlock block))
{
block = new XTableBlock(BlockSizeX, BlockSizeY, blockRow, blockCol);
BlockMap[zIndex] = block;
}
// 查找或创建此块的合并描述(相同源)
XTableMergeDescriptor descriptor = FindOrCreateMergeDescriptor(block,
zIndex == srcZIndex ? srcLocalRow : -1,
zIndex == srcZIndex ? srcLocalCol : -1,
srcZIndex, srcLocalRow, srcLocalCol,
zIndex != srcZIndex);
descriptor.MergedCellSet.Add((localRow, localCol));
}
}
}
/// <summary>
/// 查找或创建合并描述
/// </summary>
private XTableMergeDescriptor FindOrCreateMergeDescriptor(
XTableBlock block, int checkLocalRow, int checkLocalCol,
ulong srcZIndex, int srcLocalRow, int srcLocalCol, bool isCrossBlock)
{
// 查找已有描述
for (int i = 0; i < block.MergeDescriptors.Count; i++)
{
var desc = block.MergeDescriptors[i];
if (desc.MergeRowSpan > 0 && desc.MergeColSpan > 0)
{
// 简化的同源判断:跨块引用共享 sourceBlockIndex
if (isCrossBlock && desc.IsCrossBlock && desc.SourceBlockIndex == srcZIndex)
return desc;
}
}
// 创建新描述
var newDesc = new XTableMergeDescriptor
{
IsCrossBlock = isCrossBlock,
SourceBlockIndex = srcZIndex,
SourceLocalRow = srcLocalRow,
SourceLocalCol = srcLocalCol
};
block.MergeDescriptors.Add(newDesc);
return newDesc;
}
/// <summary>
/// 取消指定区域的合并
/// </summary>
public void UnmergeCells(int startRow, int startCol)
{
XTableCoordinateUtility.CellToBlockCoordinate(
startRow, startCol, BlockSizeX, BlockSizeY,
out int blockRow, out int blockCol,
out int localRow, out int localCol);
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
if (!BlockMap.TryGetValue(zIndex, out XTableBlock block))
return;
// 移除包含此单元格的所有合并描述
for (int i = block.MergeDescriptors.Count - 1; i >= 0; i--)
{
if (block.MergeDescriptors[i].Contains(localRow, localCol))
{
block.MergeDescriptors.RemoveAt(i);
}
}
}
#endregion
#region 查询方法
/// <summary>
/// 判断指定块是否存在
/// </summary>
public bool HasBlock(int blockRow, int blockCol)
{
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
return BlockMap.ContainsKey(zIndex);
}
/// <summary>
/// 获取块数量
/// </summary>
public int BlockCount => BlockMap.Count;
#endregion
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2e5d95e8d6d3d144d8fff6e41f175d77
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
namespace XericUI.XTable.Core
{
/// <summary>
/// 合并单元格描述——所有被合并单元格的 id 指向合并源(左上角单元格)。
/// 同一类型支持本地块内引用和跨块引用,通过 <see cref="IsCrossBlock"/> 区分。
/// </summary>
[Serializable]
public class XTableMergeDescriptor
{
/// <summary>合并区域在本地块内的起始行</summary>
public int LocalStartRow;
/// <summary>合并区域在本地块内的起始列</summary>
public int LocalStartCol;
/// <summary>合并跨越的行数</summary>
public int MergeRowSpan;
/// <summary>合并跨越的列数</summary>
public int MergeColSpan;
/// <summary>是否为跨块引用(true=合并源在另一个块上)</summary>
public bool IsCrossBlock;
/// <summary>
/// 跨块引用时:源块在字典中的 key(Z 曲线索引)
/// 本地引用时:忽略此字段
/// </summary>
public ulong SourceBlockIndex;
/// <summary>源单元格在块内的行号(跨块时=源块的行、本地时=本块的行)</summary>
public int SourceLocalRow;
/// <summary>源单元格在块内的列号(跨块时=源块的列、本地时=本块的列)</summary>
public int SourceLocalCol;
/// <summary>本块内被合并的 (row, col) 集合,查询时快速判断是否需要重定向</summary>
public HashSet<(int row, int col)> MergedCellSet;
public XTableMergeDescriptor()
{
MergedCellSet = new HashSet<(int, int)>();
}
/// <summary>
/// 判断指定本地坐标是否在此合并区域内
/// </summary>
public bool Contains(int localRow, int localCol)
{
return MergedCellSet.Contains((localRow, localCol));
}
/// <summary>
/// 获取重定向目标——返回 (目标块Z索引, 目标块内行, 目标块内列)
/// 本地引用时目标块索引为 0(由调用方忽略)
/// </summary>
public void GetRedirectTarget(out ulong targetBlockIndex, out int targetRow, out int targetCol)
{
targetBlockIndex = IsCrossBlock ? SourceBlockIndex : 0;
targetRow = SourceLocalRow;
targetCol = SourceLocalCol;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: abe5a0c5395fa9249a66cd7195a49dbf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ac372aa4265804648bcff4f505b39f24
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,38 @@
using System.Runtime.CompilerServices;
using XericLibrary.Runtime.MacroLibrary;
namespace XericUI.XTable.Mapping
{
/// <summary>
/// 块映射工具——将块坐标与 Z 曲线索引进行相互转换。
/// 内部调用 <see cref="MacroCurveMapping"/> 的 Morton Code 编码/解码方法。
/// Z 曲线索引避免纯横向排列产生的空缺问题,提升空间局部性。
/// </summary>
public static class XTableBlockMapping
{
/// <summary>
/// 块坐标 → Z 曲线索引 (Morton Code)
/// </summary>
/// <param name="blockRow">块行号 (非负)</param>
/// <param name="blockCol">块列号 (非负)</param>
/// <returns>64 位 Z 曲线索引,用作块字典的 key</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong BlockCoordToZIndex(int blockRow, int blockCol)
{
return MacroCurveMapping.ZOrderEncode(blockCol, blockRow);
}
/// <summary>
/// Z 曲线索引 → 块坐标
/// </summary>
/// <param name="zIndex">Z 曲线索引</param>
/// <param name="blockRow">解码后的块行号</param>
/// <param name="blockCol">解码后的块列号</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ZIndexToBlockCoord(ulong zIndex, out int blockRow, out int blockCol)
{
MacroCurveMapping.ZOrderDecode(zIndex, out blockCol, out blockRow);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d2cad58624b7724fb43fdeca3aa2b50
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,79 @@
#if ENABLE_BURST
using Unity.Burst;
#endif
using System.Runtime.CompilerServices;
namespace XericUI.XTable.Mapping
{
/// <summary>
/// 表格坐标计算工具——提供单元格坐标 ↔ 块坐标 的转换方法。
/// 所有方法支持 Burst 编译加速。
/// </summary>
#if ENABLE_BURST
[BurstCompile]
#endif
public static class XTableCoordinateUtility
{
/// <summary>
/// 将全局单元格坐标 (row, col) 转换为 块坐标 + 块内局部坐标。
/// blockRow = row / blockSizeY, blockCol = col / blockSizeX
/// localRow = row % blockSizeY, localCol = col % blockSizeX
/// </summary>
#if ENABLE_BURST
[BurstCompile]
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CellToBlockCoordinate(
int row, int col, int blockSizeX, int blockSizeY,
out int blockRow, out int blockCol,
out int localRow, out int localCol)
{
blockRow = row / blockSizeY;
blockCol = col / blockSizeX;
localRow = row % blockSizeY;
localCol = col % blockSizeX;
}
/// <summary>
/// 计算块内的线性索引。
/// index = localRow * blockSizeX + localCol
/// </summary>
#if ENABLE_BURST
[BurstCompile]
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LocalCellIndex(int localRow, int localCol, int blockSizeX)
{
return localRow * blockSizeX + localCol;
}
/// <summary>
/// 从块内线性索引反算局部行列坐标。
/// </summary>
#if ENABLE_BURST
[BurstCompile]
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LocalCellFromIndex(int index, int blockSizeX,
out int localRow, out int localCol)
{
localRow = index / blockSizeX;
localCol = index % blockSizeX;
}
/// <summary>
/// 从全局行列坐标直接计算块内线性索引。
/// </summary>
#if ENABLE_BURST
[BurstCompile]
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GlobalCellToLocalIndex(int row, int col, int blockSizeX, int blockSizeY)
{
int localRow = row % blockSizeY;
int localCol = col % blockSizeX;
return localRow * blockSizeX + localCol;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 45c10335c21cdf342816fd78d61cec09
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3b7ab03cb8f1d894a92f9ef988cb9245
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -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:
+8
View File
@@ -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:
+8
View File
@@ -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:
+107
View File
@@ -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:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 64d88208839a5034db31c330796453aa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,38 @@
// ==========================================
// Xeric UI Action Table - Default Style Sheet
// 表格默认样式定义
// ==========================================
namespace: xeric_table_default
// ---- 单元格背景 ----
/cell/bg/color = (1,1,1,1):UnityEngine.Color @Desc:单元格背景颜色
// ---- 单元格前景 ----
/cell/fg/color = (0.1,0.1,0.1,1):UnityEngine.Color @Desc:单元格前景文字颜色
// ---- 边框 ----
/cell/border/top/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:上边框颜色
/cell/border/top/width = 1:System.Single @Desc:上边框宽度
/cell/border/bottom/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:下边框颜色
/cell/border/bottom/width = 1:System.Single @Desc:下边框宽度
/cell/border/left/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:左边框颜色
/cell/border/left/width = 1:System.Single @Desc:左边框宽度
/cell/border/right/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:右边框颜色
/cell/border/right/width = 1:System.Single @Desc:右边框宽度
// ---- 选中高亮 ----
/cell/selection/bg/color = (0.2,0.5,1,0.3):UnityEngine.Color @Desc:选中单元格高亮颜色
// ---- 表头样式 ----
/header/bg/color = (0.9,0.9,0.9,1):UnityEngine.Color @Desc:表头背景颜色
/header/fg/color = (0,0,0,1):UnityEngine.Color @Desc:表头文字颜色
// ---- 字体 ----
/cell/font/size = 14:System.Int32 @Desc:默认字体大小
/cell/font/alignment = MiddleCenter:UnityEngine.TextAnchor @Desc:默认文本对齐
/cell/font/richText = true:System.Boolean @Desc:是否启用富文本
// ---- 行高 / 列宽(缺省值) ----
/cell/default/height = 40:System.Single @Desc:默认行高
/cell/default/width = 100:System.Single @Desc:默认列宽
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: a2465cbc9f8bf384aa95561b40bd4841
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 1693830282, guid: 65aae7f37e34be6409e0966d54c89b6d, type: 3}