Files
XericUIActionVessel/Runtime/XTable/Rendering/Component/XericUIActionTable.cs
T

445 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
using XericLibrary.Runtime.UIGraph;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace XericUI.XTable.Rendering.Component
{
/// <summary>
/// 表格组件——纯数据渲染器。
/// 一张全幅背景 + 按需拼装单元格文本/图片。
/// 无行列标题、无样式差异分割线——所有内容由数据定义。
/// </summary>
[ExecuteAlways]
[AddComponentMenu("Xeric UI Vessel/Table/Xeric UI Action Table", 60)]
public partial class XericUIActionTable : XericUIBehaviour, IPointerClickHandler
{
#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 = "";
[SerializeField]
[Tooltip("表格全局配置资源")]
private XTableConfig m_Config;
[SerializeField] private ScrollRect m_ScrollRect;
[SerializeField]
[Tooltip("冻结行数——顶部固定不滚动的行数")]
private int m_FreezeRowCount;
[SerializeField]
[Tooltip("冻结列数——左侧固定不滚动的列数")]
private int m_FreezeColCount;
[SerializeField]
[Tooltip("在 Hierarchy 中显示动态生成的子对象(仅编辑器,不保存到场景)")]
private bool m_ChildrenVisible;
#endregion
#region 私有字段
[System.NonSerialized] private CellAssembler m_Assembler;
[System.NonSerialized] private XTableDirtyFlag m_DirtyFlag;
/// <summary>Viewport 层渲染器——用于绘制冻结行列单元格</summary>
[System.NonSerialized] private TableRenderer m_ViewportRenderer;
[System.NonSerialized] private XTableSelectionHandle m_SelectionHandle;
[System.NonSerialized] private GameObject m_BackgroundGo;
/// <summary>预制体对象池——按原型分组管理</summary>
[System.NonSerialized] private PrefabPrototypePool m_PrefabPool;
/// <summary>活跃预制体实例映射 (row, col) → instance,用于回调编排</summary>
[System.NonSerialized] private Dictionary<(int, int), GameObject> m_ActivePrefabMap;
/// <summary>Texture2D → Sprite 缓存,避免每帧 Sprite.Create 分配</summary>
[System.NonSerialized] private Dictionary<Texture2D, Sprite> m_SpriteCache;
/// <summary>表格四叉树——按 StyleNamespace 分区,渲染阶段查询可见象限</summary>
[System.NonSerialized] private XTableQuadtree m_Quadtree;
/// <summary>边框线段渲染器——使用 UILineRendererV2 的坐标线段绘制</summary>
[System.NonSerialized] private UILineRendererV2 m_BorderRenderer;
private float m_TotalWidth;
private float m_TotalHeight;
/// <summary>内容偏移量(边框半宽),避免 viewport 裁切边缘边框</summary>
private float m_ContentOffset;
// 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 XTableCellData this[int row, int col] => TableData.GetOrCreateCell(row, col);
public float[] RowHeights => m_RowHeights;
public float[] ColWidths => m_ColWidths;
/// <summary>冻结行数</summary>
public int FreezeRowCount => m_FreezeRowCount;
/// <summary>冻结列数</summary>
public int FreezeColCount => m_FreezeColCount;
public string DefaultStyleNamespace => m_DefaultStyleNamespace;
/// <summary>选中操作句柄——外部可通过此句柄控制选区,或直接操作 StartRow/StartCol 等属性</summary>
public XTableSelectionHandle SelectionHandle
{
get
{
if (m_SelectionHandle == null)
m_SelectionHandle = new XTableSelectionHandle();
return m_SelectionHandle;
}
}
// ── 向后兼容属性(供 Editor 脚本使用)──
public int SelectedRow => SelectionHandle.StartRow;
public int SelectedCol => SelectionHandle.StartCol;
public int SelectStartRow => SelectionHandle.StartRow;
public int SelectStartCol => SelectionHandle.StartCol;
public int SelectEndRow => SelectionHandle.EndRow;
public int SelectEndCol => SelectionHandle.EndCol;
public bool HasMultiSelection => SelectionHandle.HasMultiSelection;
/// <summary>动态子对象在 Hierarchy 中是否可见(仅编辑器有效)</summary>
public bool ChildrenVisible
{
get => m_ChildrenVisible;
set => SetChildrenVisible(value);
}
/// <summary>动态子对象挂载的父节点。
/// 若绑定了 ScrollRect 则使用其 Content,否则回退到自身的 rectTransform。</summary>
public Transform ContentTransform
{
get
{
if (m_ScrollRect != null && m_ScrollRect.content != null)
return m_ScrollRect.content;
return rectTransform;
}
}
/// <summary>表格全局配置资源</summary>
public XTableConfig Config
{
get
{
if (m_Config == null)
m_Config = XTableConfig.LoadOrDefault();
return m_Config;
}
set => m_Config = value;
}
/// <summary>单元格内容水平内边距(单侧)</summary>
private float CellPadding => Config.CellHorizontalPadding;
/// <summary>单元格内容宽度扣除量</summary>
private float CellPadding2 => Config.CellHorizontalPadding * 2f;
/// <summary>判断单元格是否在多选范围内</summary>
public bool IsInSelectionRange(int row, int col)
{
return SelectionHandle.IsInRange(row, col);
}
#endregion
#region 生命周期
private static bool s_StyleSheetsPreloaded;
/// <summary>
/// 预加载 Resources 目录下所有 XericStyleSheet 资产到 StyleManager。
/// 仅首次调用生效,后续调用直接跳过。
/// </summary>
private static void PreloadAllStyleSheets()
{
if (s_StyleSheetsPreloaded) return;
s_StyleSheetsPreloaded = true;
var sheets = Resources.LoadAll<XericStyleSheet>("");
foreach (var sheet in sheets)
{
if (sheet != null)
StyleManager.LoadStyleSheet(sheet);
}
}
protected override void Awake()
{
base.Awake();
// 预加载 Resources 中所有 .xsss 样式表到 StyleManager(仅首次)
PreloadAllStyleSheets();
// 域重载后旧子对象可能残留在场景中(DontSave 不自动销毁),先清空再重建
ClearAllChildren();
if (m_Config == null)
m_Config = XTableConfig.LoadOrDefault();
if (string.IsNullOrEmpty(m_DefaultStyleNamespace))
m_DefaultStyleNamespace = Config.DefaultStyleNamespace;
if (m_TableData == null)
m_TableData = new XTableData();
// 强制 Content 的 anchor/pivot 为 (0,1),确保 anchoredPosition 语义一致。
// 若不固定,ScrollRect 可能改变 anchor 导致坐标计算错误。
if (m_ScrollRect != null && m_ScrollRect.content != null)
{
RectTransform ct = m_ScrollRect.content as RectTransform;
if (ct != null)
{
ct.anchorMin = new Vector2(0, 1);
ct.anchorMax = new Vector2(0, 1);
ct.pivot = new Vector2(0, 1);
}
}
m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace);
m_PrefabPool = new PrefabPrototypePool(ContentTransform);
m_ActivePrefabMap = new Dictionary<(int, int), GameObject>();
RecalculateTotalSize();
EnsureDefaultStyleNamespace();
CreateDefaultSampleData();
CreateBackground();
// 创建 Viewport 层渲染器(用于冻结单元格)
CreateViewportRenderer();
if (m_ScrollRect != null)
m_ScrollRect.onValueChanged.AddListener(OnScrollValueChanged);
if (!Application.isPlaying)
SubscribeEditorUpdate();
else
m_Assembler.OnStyleChangeDetected += OnPoolStyleChanged;
MarkDirty(TableDirtyType.All);
}
protected override void OnEnable()
{
base.OnEnable();
EnsureDefaultStyleNamespace();
MarkDirty(TableDirtyType.All);
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
// 编辑器下属性变更 / 选中组件时即刻刷新样式
MarkDirty(TableDirtyType.All);
}
#endif
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.OnStyleChangeDetected -= OnPoolStyleChanged;
// 销毁所有动态生成的对象
if (m_Assembler != null)
m_Assembler.DestroyAll();
m_PrefabPool?.DestroyAll();
m_ActivePrefabMap?.Clear();
DestroyBorderRenderer();
if (m_BackgroundGo != null)
{
if (Application.isPlaying) Destroy(m_BackgroundGo);
else DestroyImmediate(m_BackgroundGo);
m_BackgroundGo = null;
}
// 销毁 Viewport 冻结渲染器
m_ViewportRenderer?.DestroyAll();
m_ViewportRenderer = null;
base.OnDestroy();
}
private void Update()
{
if (!m_DirtyFlag.IsDirty) return;
var flags = m_DirtyFlag.Flags;
// 仅滚动或选区变更 → 轻刷新(不重建边框/背景/预制体全回收)
if ((flags & ~(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged)) == 0)
RefreshViewLight();
else
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 脏标记与滚动回调
public void MarkDirty(TableDirtyType type)
{
m_DirtyFlag.Mark(type);
// 仅数据或布局变更时重建四叉树;滚动/选区变更无需重建
if ((type & (TableDirtyType.DataChanged | TableDirtyType.LayoutChanged | TableDirtyType.StyleChanged)) != 0)
MaintainQuadtree();
}
/// <summary>运行时样式变更回调:标记脏数据,下一帧重绘</summary>
private void OnPoolStyleChanged() => MarkDirty(TableDirtyType.StyleChanged);
[System.NonSerialized] private bool m_IsRefreshingFromScroll;
private void OnScrollValueChanged(Vector2 _)
{
// 防重入:RefreshViewLight 中操作 content 可能触发 ScrollRect 再次回调
if (m_IsRefreshingFromScroll) return;
m_IsRefreshingFromScroll = true;
try
{
// 滚动时仅做轻刷新(RefreshViewLight 内部会调用 UpdateVisibleRect),
// 不重建背景、边框、不重置 content.sizeDelta、不做孤儿清理。
RefreshViewLight();
}
finally
{
m_IsRefreshingFromScroll = false;
}
}
#endregion
#region IPointerClickHandler
public void OnPointerClick(PointerEventData eventData)
{
// 直接用 ContentTransform 做坐标转换:单元格是 Content 的子对象,
// 其 anchoredPosition 就在 Content 局部空间中,无需手工换算 scroll 偏移。
RectTransform contentRt = ContentTransform as RectTransform;
if (contentRt == null) return;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
contentRt, eventData.position, eventData.pressEventCamera, out Vector2 localPoint))
return;
// 转换为左上角坐标系(Content 的 anchor/pivot 都是 (0,1))
float tableX = localPoint.x - contentRt.rect.xMin - m_ContentOffset;
float tableY = contentRt.rect.yMax - localPoint.y - m_ContentOffset;
int col = -1;
float ax = 0;
for (int c = 0; c < m_ColWidths.Length; c++)
{
if (tableX >= ax && tableX < ax + m_ColWidths[c])
{
col = c;
break;
}
ax += m_ColWidths[c];
}
int row = -1;
float ay = 0;
for (int r = 0; r < m_RowHeights.Length; r++)
{
if (tableY >= ay && tableY < ay + m_RowHeights[r])
{
row = r;
break;
}
ay += m_RowHeights[r];
}
if (row >= 0 && col >= 0)
{
SelectCell(row, col);
OnCellSelected?.Invoke(row, col);
}
}
#endregion
}
}