855 lines
23 KiB
C#
855 lines
23 KiB
C#
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.Mapping;
|
||
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;
|
||
|
||
// 多选范围
|
||
private int m_SelectStartRow = -1;
|
||
private int m_SelectStartCol = -1;
|
||
private int m_SelectEndRow = -1;
|
||
private int m_SelectEndCol = -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
|
||
{
|
||
if (m_TableData == null)
|
||
m_TableData = new XTableData();
|
||
return 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;
|
||
|
||
public int SelectStartRow => m_SelectStartRow;
|
||
public int SelectStartCol => m_SelectStartCol;
|
||
public int SelectEndRow => m_SelectEndRow;
|
||
public int SelectEndCol => m_SelectEndCol;
|
||
public bool HasMultiSelection => m_SelectStartRow >= 0 && m_SelectEndRow >= 0;
|
||
|
||
/// <summary>判断单元格是否在多选范围内</summary>
|
||
public bool IsInSelectionRange(int row, int col)
|
||
{
|
||
if (!HasMultiSelection) return false;
|
||
int minR = Mathf.Min(m_SelectStartRow, m_SelectEndRow);
|
||
int maxR = Mathf.Max(m_SelectStartRow, m_SelectEndRow);
|
||
int minC = Mathf.Min(m_SelectStartCol, m_SelectEndCol);
|
||
int maxC = Mathf.Max(m_SelectStartCol, m_SelectEndCol);
|
||
return row >= minR && row <= maxR && col >= minC && col <= maxC;
|
||
}
|
||
|
||
#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 void CreateDefaultSampleData()
|
||
{
|
||
// 仅当数据完全为空时才创建示例数据(首次使用或数据损坏后恢复)
|
||
if (m_TableData.BlockCount > 0) return;
|
||
|
||
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++)
|
||
{
|
||
var data = m_TableData.GetOrCreateCell(r, c);
|
||
data.Text = sample[r, c];
|
||
}
|
||
}
|
||
|
||
#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)
|
||
{
|
||
TableData.SetCell(row, col, data);
|
||
MarkDirty(TableDirtyType.DataChanged);
|
||
}
|
||
|
||
public void SetCellText(int row, int col, string text)
|
||
{
|
||
var data = TableData.GetOrCreateCell(row, col);
|
||
data.Text = text;
|
||
MarkDirty(TableDirtyType.DataChanged);
|
||
}
|
||
|
||
public XTableCellData GetCellData(int row, int col)
|
||
=> TableData.GetOrCreateCell(row, col);
|
||
|
||
public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan)
|
||
{
|
||
TableData.MergeCells(startRow, startCol, rowSpan, colSpan);
|
||
MarkDirty(TableDirtyType.DataChanged);
|
||
}
|
||
|
||
public void UnmergeCells(int startRow, int startCol)
|
||
{
|
||
TableData.UnmergeCells(startRow, startCol);
|
||
MarkDirty(TableDirtyType.DataChanged);
|
||
}
|
||
|
||
/// <summary>在末尾追加一行,默认行高 40</summary>
|
||
public void AddRow(float height = 40f)
|
||
{
|
||
#if UNITY_EDITOR
|
||
UnityEditor.Undo.RecordObject(this, "Add Table Row");
|
||
#endif
|
||
int newLen = m_RowHeights.Length + 1;
|
||
var newArr = new float[newLen];
|
||
System.Array.Copy(m_RowHeights, newArr, m_RowHeights.Length);
|
||
newArr[newLen - 1] = height;
|
||
m_RowHeights = newArr;
|
||
MarkDirty(TableDirtyType.LayoutChanged);
|
||
#if UNITY_EDITOR
|
||
UnityEditor.EditorUtility.SetDirty(this);
|
||
#endif
|
||
}
|
||
|
||
/// <summary>在末尾追加一列,默认列宽 100</summary>
|
||
public void AddColumn(float width = 100f)
|
||
{
|
||
#if UNITY_EDITOR
|
||
UnityEditor.Undo.RecordObject(this, "Add Table Column");
|
||
#endif
|
||
int newLen = m_ColWidths.Length + 1;
|
||
var newArr = new float[newLen];
|
||
System.Array.Copy(m_ColWidths, newArr, m_ColWidths.Length);
|
||
newArr[newLen - 1] = width;
|
||
m_ColWidths = newArr;
|
||
MarkDirty(TableDirtyType.LayoutChanged);
|
||
#if UNITY_EDITOR
|
||
UnityEditor.EditorUtility.SetDirty(this);
|
||
#endif
|
||
}
|
||
|
||
/// <summary>设置指定行的高度</summary>
|
||
public void SetRowHeight(int row, float height)
|
||
{
|
||
if (row < 0 || row >= m_RowHeights.Length) return;
|
||
if (height <= 0) return;
|
||
#if UNITY_EDITOR
|
||
UnityEditor.Undo.RecordObject(this, "Set Row Height");
|
||
#endif
|
||
m_RowHeights[row] = height;
|
||
MarkDirty(TableDirtyType.LayoutChanged);
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(this);
|
||
#endif
|
||
}
|
||
|
||
/// <summary>设置指定列的宽度</summary>
|
||
public void SetColWidth(int col, float width)
|
||
{
|
||
if (col < 0 || col >= m_ColWidths.Length) return;
|
||
if (width <= 0) return;
|
||
#if UNITY_EDITOR
|
||
UnityEditor.Undo.RecordObject(this, "Set Column Width");
|
||
#endif
|
||
m_ColWidths[col] = width;
|
||
MarkDirty(TableDirtyType.LayoutChanged);
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(this);
|
||
#endif
|
||
}
|
||
|
||
/// <summary>删除指定行,单元格数据上移</summary>
|
||
public void RemoveRow(int row)
|
||
{
|
||
if (row < 0 || row >= m_RowHeights.Length) return;
|
||
#if UNITY_EDITOR
|
||
UnityEditor.Undo.RecordObject(this, "Remove Table Row");
|
||
#endif
|
||
int colCount = m_ColWidths.Length;
|
||
|
||
// 先解除涉及该行的合并
|
||
for (int c = 0; c < colCount; c++)
|
||
TableData.UnmergeCells(row, c);
|
||
|
||
// 将下方行数据上移
|
||
for (int r = row + 1; r < m_RowHeights.Length; r++)
|
||
for (int c = 0; c < colCount; c++)
|
||
{
|
||
var data = TableData.GetCell(r, c);
|
||
if (data != null)
|
||
TableData.SetCell(r - 1, c, data);
|
||
else
|
||
TableData.SetCell(r - 1, c, null); // 显式清空原位置
|
||
}
|
||
|
||
// 清空最后一行数据
|
||
int lastRow = m_RowHeights.Length - 1;
|
||
for (int c = 0; c < colCount; c++)
|
||
TableData.SetCell(lastRow, c, null);
|
||
|
||
// 缩小行高数组
|
||
int newLen = m_RowHeights.Length - 1;
|
||
var newArr = new float[newLen];
|
||
for (int i = 0; i < row; i++)
|
||
newArr[i] = m_RowHeights[i];
|
||
for (int i = row; i < newLen; i++)
|
||
newArr[i] = m_RowHeights[i + 1];
|
||
m_RowHeights = newArr;
|
||
|
||
// 更新选中状态
|
||
if (m_SelectedRow >= m_RowHeights.Length)
|
||
m_SelectedRow = Mathf.Max(-1, m_RowHeights.Length - 1);
|
||
if (m_SelectStartRow >= m_RowHeights.Length)
|
||
m_SelectStartRow = Mathf.Max(-1, m_RowHeights.Length - 1);
|
||
if (m_SelectEndRow >= m_RowHeights.Length)
|
||
m_SelectEndRow = Mathf.Max(-1, m_RowHeights.Length - 1);
|
||
|
||
MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged);
|
||
#if UNITY_EDITOR
|
||
UnityEditor.EditorUtility.SetDirty(this);
|
||
#endif
|
||
}
|
||
|
||
/// <summary>删除指定列,单元格数据左移</summary>
|
||
public void RemoveColumn(int col)
|
||
{
|
||
if (col < 0 || col >= m_ColWidths.Length) return;
|
||
#if UNITY_EDITOR
|
||
UnityEditor.Undo.RecordObject(this, "Remove Table Column");
|
||
#endif
|
||
int rowCount = m_RowHeights.Length;
|
||
|
||
// 先解除涉及该列的合并
|
||
for (int r = 0; r < rowCount; r++)
|
||
TableData.UnmergeCells(r, col);
|
||
|
||
// 将右侧列数据左移
|
||
for (int r = 0; r < rowCount; r++)
|
||
for (int c = col + 1; c < m_ColWidths.Length; c++)
|
||
{
|
||
var data = TableData.GetCell(r, c);
|
||
if (data != null)
|
||
TableData.SetCell(r, c - 1, data);
|
||
else
|
||
TableData.SetCell(r, c - 1, null);
|
||
}
|
||
|
||
// 清空最后一列数据
|
||
int lastCol = m_ColWidths.Length - 1;
|
||
for (int r = 0; r < rowCount; r++)
|
||
TableData.SetCell(r, lastCol, null);
|
||
|
||
// 缩小列宽数组
|
||
int newLen = m_ColWidths.Length - 1;
|
||
var newArr = new float[newLen];
|
||
for (int i = 0; i < col; i++)
|
||
newArr[i] = m_ColWidths[i];
|
||
for (int i = col; i < newLen; i++)
|
||
newArr[i] = m_ColWidths[i + 1];
|
||
m_ColWidths = newArr;
|
||
|
||
// 更新选中状态
|
||
if (m_SelectedCol >= m_ColWidths.Length)
|
||
m_SelectedCol = Mathf.Max(-1, m_ColWidths.Length - 1);
|
||
if (m_SelectStartCol >= m_ColWidths.Length)
|
||
m_SelectStartCol = Mathf.Max(-1, m_ColWidths.Length - 1);
|
||
if (m_SelectEndCol >= m_ColWidths.Length)
|
||
m_SelectEndCol = Mathf.Max(-1, m_ColWidths.Length - 1);
|
||
|
||
MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged);
|
||
#if UNITY_EDITOR
|
||
UnityEditor.EditorUtility.SetDirty(this);
|
||
#endif
|
||
}
|
||
|
||
public void SelectCell(int row, int col)
|
||
{
|
||
ClearMultiSelection();
|
||
m_SelectedRow = row;
|
||
m_SelectedCol = col;
|
||
MarkDirty(TableDirtyType.SelectionChanged);
|
||
}
|
||
|
||
public void ClearSelection()
|
||
{
|
||
m_SelectedRow = -1;
|
||
m_SelectedCol = -1;
|
||
ClearMultiSelection();
|
||
MarkDirty(TableDirtyType.SelectionChanged);
|
||
}
|
||
|
||
/// <summary>开始多选(以当前格为锚点)</summary>
|
||
public void StartMultiSelect(int row, int col)
|
||
{
|
||
m_SelectStartRow = row;
|
||
m_SelectStartCol = col;
|
||
m_SelectEndRow = row;
|
||
m_SelectEndCol = col;
|
||
m_SelectedRow = row;
|
||
m_SelectedCol = col;
|
||
MarkDirty(TableDirtyType.SelectionChanged);
|
||
}
|
||
|
||
/// <summary>扩展多选范围到目标格</summary>
|
||
public void ExtendMultiSelect(int row, int col)
|
||
{
|
||
m_SelectEndRow = row;
|
||
m_SelectEndCol = col;
|
||
m_SelectedRow = row;
|
||
m_SelectedCol = col;
|
||
MarkDirty(TableDirtyType.SelectionChanged);
|
||
}
|
||
|
||
/// <summary>清除多选范围</summary>
|
||
public void ClearMultiSelection()
|
||
{
|
||
m_SelectStartRow = -1;
|
||
m_SelectStartCol = -1;
|
||
m_SelectEndRow = -1;
|
||
m_SelectEndCol = -1;
|
||
}
|
||
|
||
/// <summary>通过文本选择一个单元格(如 "A0")</summary>
|
||
public bool SelectCellByText(string text)
|
||
{
|
||
if (!XTableCoordinateFormat.TryParseCell(text, out int row, out int col))
|
||
return false;
|
||
SelectCell(row, col);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>通过文本选择一个范围(如 "A0:B1", "A:A", "0:0")</summary>
|
||
public bool SelectRangeByText(string text)
|
||
{
|
||
if (!XTableCoordinateFormat.TryParseRange(text,
|
||
out int sr, out int sc, out int er, out int ec))
|
||
return false;
|
||
|
||
ClearSelection();
|
||
m_SelectStartRow = sr;
|
||
m_SelectStartCol = sc;
|
||
m_SelectEndRow = er;
|
||
m_SelectEndCol = ec;
|
||
m_SelectedRow = sr;
|
||
m_SelectedCol = sc;
|
||
MarkDirty(TableDirtyType.SelectionChanged);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>获取当前选区的文本描述(单格 "A0",多格 "A0:B1")</summary>
|
||
public string GetSelectionText()
|
||
{
|
||
if (m_SelectedRow < 0 || m_SelectedCol < 0) return string.Empty;
|
||
if (HasMultiSelection &&
|
||
(m_SelectStartRow != m_SelectEndRow || m_SelectStartCol != m_SelectEndCol))
|
||
{
|
||
return XTableCoordinateFormat.RangeToText(
|
||
m_SelectStartRow, m_SelectStartCol,
|
||
m_SelectEndRow, m_SelectEndCol);
|
||
}
|
||
return XTableCoordinateFormat.CellToText(m_SelectedRow, m_SelectedCol);
|
||
}
|
||
|
||
public void RefreshView()
|
||
{
|
||
// 编辑模式下 Awake 可能尚未执行,按需回退初始化
|
||
if (m_Assembler == null)
|
||
{
|
||
if (rectTransform != null)
|
||
m_Assembler = new CellAssembler(rectTransform);
|
||
else return;
|
||
}
|
||
|
||
if (m_RowHeights == null || m_ColWidths == null) return;
|
||
if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return;
|
||
|
||
CreateBackground();
|
||
|
||
#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();
|
||
|
||
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);
|
||
Color multiSelectColor = new Color(0.18f, 0.42f, 0.82f, 0.12f);
|
||
Color focusColor = new Color(0.22f, 0.55f, 0.95f, 0.35f);
|
||
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 = TableData.GetCell(r, c);
|
||
|
||
// 多选范围高亮(覆盖被选中范围内的所有格)
|
||
if (IsInSelectionRange(r, c))
|
||
{
|
||
var selImg = m_Assembler.GetImage("cell_multiselect");
|
||
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
|
||
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
|
||
selImg.Image.color = (r == m_SelectedRow && c == m_SelectedCol)
|
||
? focusColor : multiSelectColor;
|
||
}
|
||
// 单选高亮
|
||
else 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
|
||
}
|
||
}
|