495 lines
18 KiB
C#
495 lines
18 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;
|
|
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
|
|
{
|
|
#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;
|
|
|
|
[SerializeField] private bool m_EnableSelectionAutoScroll = true;
|
|
[SerializeField, Min(1f), Tooltip("框选自动滚动的 Content 边缘阈值(像素)")]
|
|
private float m_SelectionAutoScrollEdgeThreshold = 32f;
|
|
[SerializeField, Min(1f), Tooltip("框选自动滚动的最大速度(像素/秒)")]
|
|
private float m_SelectionAutoScrollMaxSpeed = 600f;
|
|
[SerializeField, Min(1f), Tooltip("框选自动滚动的边缘加速度指数")]
|
|
private float m_SelectionAutoScrollAcceleration = 1.5f;
|
|
|
|
#endregion
|
|
|
|
#region 私有字段
|
|
|
|
[System.NonSerialized] private CellAssembler m_Assembler;
|
|
[System.NonSerialized] private XTableDirtyFlag m_DirtyFlag;
|
|
|
|
/// <summary>Viewport 下的冻结列、冻结行与左上交叉区渲染器。</summary>
|
|
[System.NonSerialized] private TableRenderer m_FrozenColumnRenderer;
|
|
[System.NonSerialized] private TableRenderer m_FrozenRowRenderer;
|
|
[System.NonSerialized] private TableRenderer m_FrozenCornerRenderer;
|
|
[System.NonSerialized] private RectTransform m_FrozenColumnsContainer;
|
|
[System.NonSerialized] private RectTransform m_FrozenRowsContainer;
|
|
[System.NonSerialized] private RectTransform m_FrozenCornerContainer;
|
|
|
|
[System.NonSerialized] private XTableSelectionHandle m_SelectionHandle;
|
|
[System.NonSerialized] private XTableSelectionOverlay m_NormalSelectionOverlay;
|
|
[System.NonSerialized] private XTableSelectionOverlay m_FrozenColumnSelectionOverlay;
|
|
[System.NonSerialized] private XTableSelectionOverlay m_FrozenRowSelectionOverlay;
|
|
[System.NonSerialized] private XTableSelectionOverlay m_FrozenCornerSelectionOverlay;
|
|
[System.NonSerialized] private bool m_SelectionDragActive;
|
|
[System.NonSerialized] private Vector2 m_LastSelectionPointer;
|
|
[System.NonSerialized] private Camera m_SelectionPointerCamera;
|
|
|
|
[System.NonSerialized] private XTableBackgroundRenderer m_BackgroundRenderer;
|
|
|
|
/// <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>表格四叉树——仅多样式背景分区时按需构建</summary>
|
|
[System.NonSerialized] private XTableQuadtree m_Quadtree;
|
|
[System.NonSerialized] private bool m_QuadtreeDirty = true;
|
|
[System.NonSerialized] private readonly HashSet<string> m_ActiveStyleNamespaces = new();
|
|
[System.NonSerialized] private bool m_StyleNamespaceDirty = true;
|
|
|
|
/// <summary>各渲染区域的边框线段渲染器。</summary>
|
|
[System.NonSerialized] private UILineRendererV2 m_BorderRenderer;
|
|
[System.NonSerialized] private UILineRendererV2 m_FrozenColumnBorderRenderer;
|
|
[System.NonSerialized] private UILineRendererV2 m_FrozenRowBorderRenderer;
|
|
[System.NonSerialized] private UILineRendererV2 m_FrozenCornerBorderRenderer;
|
|
|
|
[NonSerialized] private float[] m_ResolvedRowHeights;
|
|
[NonSerialized] private float[] m_ResolvedColWidths;
|
|
[NonSerialized] private Vector2 m_LastLayoutBaseSize;
|
|
|
|
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;
|
|
public float[] ResolvedRowHeights => m_ResolvedRowHeights;
|
|
public float[] ResolvedColWidths => m_ResolvedColWidths;
|
|
|
|
/// <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();
|
|
m_SelectionHandle.OnSelectionChanged += OnHandleSelectionChanged;
|
|
}
|
|
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;
|
|
|
|
private void CreatePools()
|
|
{
|
|
m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace,
|
|
Config.TextPoolMaxCapacity, Config.ImagePoolMaxCapacity);
|
|
m_PrefabPool = new PrefabPrototypePool(ContentTransform, Config.PrefabPoolMaxCapacity);
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
|
|
CreatePools();
|
|
m_ActivePrefabMap = new Dictionary<(int, int), GameObject>();
|
|
UpdateResolvedCellSizes();
|
|
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();
|
|
DestroyBackgroundRenderer();
|
|
|
|
DestroyFrozenRenderers();
|
|
DestroySelectionOverlay(ref m_NormalSelectionOverlay);
|
|
|
|
base.OnDestroy();
|
|
}
|
|
|
|
/// <summary>是否正在同步重建中,此时 Update / EditorUpdate 应跳过以避免重入</summary>
|
|
[System.NonSerialized] private bool m_IsRebuilding;
|
|
|
|
private void Update()
|
|
{
|
|
UpdateSelectionAutoScroll();
|
|
if (m_IsRebuilding || IsCheckingUpdate || !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)
|
|
{
|
|
if (IsCheckingUpdate)
|
|
{
|
|
m_UpdateContextStack.Peek().DirtyFlags |= type;
|
|
return;
|
|
}
|
|
ApplyDirty(type);
|
|
}
|
|
|
|
private void ApplyDirty(TableDirtyType type)
|
|
{
|
|
m_DirtyFlag.Mark(type);
|
|
|
|
// 数据、布局或样式改变只使四叉树失效;仅多样式背景需要时再构建。
|
|
if ((type & (TableDirtyType.DataChanged | TableDirtyType.LayoutChanged | TableDirtyType.StyleChanged)) != 0)
|
|
m_QuadtreeDirty = true;
|
|
if ((type & (TableDirtyType.LayoutChanged | TableDirtyType.StyleChanged)) != 0)
|
|
m_StyleNamespaceDirty = true;
|
|
}
|
|
|
|
/// <summary>运行时样式变更回调:标记脏数据,下一帧重绘</summary>
|
|
private void OnPoolStyleChanged() => MarkDirty(TableDirtyType.StyleChanged);
|
|
|
|
[System.NonSerialized] private bool m_IsRefreshingFromScroll;
|
|
|
|
private void OnRectTransformDimensionsChange()
|
|
{
|
|
Vector2 layoutBaseSize = GetLayoutBaseSize();
|
|
if (layoutBaseSize != m_LastLayoutBaseSize)
|
|
{
|
|
m_LastLayoutBaseSize = layoutBaseSize;
|
|
MarkDirty(TableDirtyType.LayoutChanged);
|
|
}
|
|
}
|
|
|
|
private Vector2 GetLayoutBaseSize()
|
|
{
|
|
RectTransform baseTransform = m_ScrollRect != null && m_ScrollRect.viewport != null
|
|
? m_ScrollRect.viewport
|
|
: transform.parent as RectTransform;
|
|
if (baseTransform == null) baseTransform = rectTransform;
|
|
|
|
Vector2 size = baseTransform != null ? baseTransform.rect.size : Vector2.zero;
|
|
Vector2 selfSize = rectTransform != null ? rectTransform.rect.size : Vector2.zero;
|
|
return new Vector2(size.x > 0f ? size.x : (m_LastLayoutBaseSize.x > 0f ? m_LastLayoutBaseSize.x : selfSize.x),
|
|
size.y > 0f ? size.y : (m_LastLayoutBaseSize.y > 0f ? m_LastLayoutBaseSize.y : selfSize.y));
|
|
}
|
|
|
|
private void UpdateResolvedCellSizes()
|
|
{
|
|
Vector2 baseSize = GetLayoutBaseSize();
|
|
m_LastLayoutBaseSize = baseSize;
|
|
if (m_RowHeights == null || m_ColWidths == null)
|
|
{
|
|
m_ResolvedRowHeights = null;
|
|
m_ResolvedColWidths = null;
|
|
return;
|
|
}
|
|
|
|
if (m_ResolvedRowHeights == null || m_ResolvedRowHeights.Length != m_RowHeights.Length)
|
|
m_ResolvedRowHeights = new float[m_RowHeights.Length];
|
|
if (m_ResolvedColWidths == null || m_ResolvedColWidths.Length != m_ColWidths.Length)
|
|
m_ResolvedColWidths = new float[m_ColWidths.Length];
|
|
|
|
for (int i = 0; i < m_RowHeights.Length; i++)
|
|
m_ResolvedRowHeights[i] = XTableCoordinateUtility.ResolveSize(m_RowHeights[i], baseSize.y);
|
|
for (int i = 0; i < m_ColWidths.Length; i++)
|
|
m_ResolvedColWidths[i] = XTableCoordinateUtility.ResolveSize(m_ColWidths[i], baseSize.x);
|
|
}
|
|
|
|
private void OnScrollValueChanged(Vector2 _)
|
|
{
|
|
if (IsCheckingUpdate) return;
|
|
// 防重入:RefreshViewLight 中操作 content 可能触发 ScrollRect 再次回调
|
|
if (m_IsRefreshingFromScroll) return;
|
|
m_IsRefreshingFromScroll = true;
|
|
|
|
try
|
|
{
|
|
// 滚动时仅做轻刷新(RefreshViewLight 内部会调用 UpdateVisibleRect),
|
|
// 不重建背景、边框、不重置 content.sizeDelta、不做孤儿清理。
|
|
RefreshViewLight();
|
|
}
|
|
finally
|
|
{
|
|
m_IsRefreshingFromScroll = false;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
}
|