表格设置单元格为预制体的情况下不会刷新这个预制体,因为条件写反了。
This commit is contained in:
@@ -90,22 +90,13 @@ namespace XericUI.XTable.Rendering.Component
|
||||
sc = Mathf.Max(sc, StartCol);
|
||||
er = Mathf.Min(er, EndRow > 0 ? EndRow : rows.Length);
|
||||
ec = Mathf.Min(ec, EndCol > 0 ? EndCol : cols.Length);
|
||||
if (sr >= er || sc >= ec)
|
||||
{
|
||||
if (fullRefresh) ReleaseAll();
|
||||
return;
|
||||
}
|
||||
if (sr >= er || sc >= ec) return;
|
||||
|
||||
var target = new HashSet<(int, int)>();
|
||||
for (int r = sr; r < er; r++)
|
||||
for (int c = sc; c < ec; c++)
|
||||
target.Add((r, c));
|
||||
|
||||
if (fullRefresh)
|
||||
ReleaseAll();
|
||||
else
|
||||
ReleaseExcept(target);
|
||||
|
||||
foreach (var cell in target)
|
||||
if (fullRefresh || !m_Rendered.Contains(cell))
|
||||
RenderCell(cell.Item1, cell.Item2, rows, cols, editorMode);
|
||||
@@ -122,18 +113,6 @@ namespace XericUI.XTable.Rendering.Component
|
||||
m_Rendered.Clear();
|
||||
}
|
||||
|
||||
private void ReleaseExcept(HashSet<(int, int)> target)
|
||||
{
|
||||
var remove = new List<(int, int)>();
|
||||
foreach (var pair in m_Tracked)
|
||||
if (!target.Contains(pair.Key)) remove.Add(pair.Key);
|
||||
foreach (var key in remove)
|
||||
{
|
||||
ReleaseTracked(m_Tracked[key]);
|
||||
m_Tracked.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseTracked(Tracked tracked)
|
||||
{
|
||||
Assembler.ReleaseText(tracked.Text);
|
||||
@@ -191,15 +170,20 @@ namespace XericUI.XTable.Rendering.Component
|
||||
}
|
||||
else if (cell.ContentType == CellContentType.Prefab && cell.Prefab != null && PrefabPool != null)
|
||||
{
|
||||
var prefab = PrefabPool.Acquire(cell.Prefab, cellId, m_Parent);
|
||||
var key = (row, col);
|
||||
GameObject activePrefab = null;
|
||||
bool wasActive = ActivePrefabMap != null
|
||||
&& ActivePrefabMap.TryGetValue(key, out activePrefab);
|
||||
var prefab = wasActive ? activePrefab : PrefabPool.Acquire(cell.Prefab, cellId, m_Parent);
|
||||
if (prefab != null)
|
||||
{
|
||||
var rt = prefab.GetComponent<RectTransform>();
|
||||
rt.anchoredPosition = new Vector2(x, y);
|
||||
rt.sizeDelta = new Vector2(width, height);
|
||||
if (ActivePrefabMap != null)
|
||||
ActivePrefabMap[(row, col)] = prefab;
|
||||
cell.OnPrefabAcquire?.Invoke(prefab);
|
||||
ActivePrefabMap[key] = prefab;
|
||||
if (!wasActive)
|
||||
cell.OnPrefabAcquire?.Invoke(prefab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +452,9 @@ namespace XericUI.XTable.Rendering.Component
|
||||
public void RemoveRow(int row)
|
||||
{
|
||||
if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.RemoveRow, row))) return;
|
||||
var prefabCallbacks = CaptureActivePrefabReleaseCallbacks();
|
||||
RemoveRowImmediate(row);
|
||||
RebuildRenderBindings(prefabCallbacks);
|
||||
}
|
||||
|
||||
private void RemoveRowImmediate(int row)
|
||||
@@ -509,7 +511,9 @@ namespace XericUI.XTable.Rendering.Component
|
||||
public void RemoveColumn(int col)
|
||||
{
|
||||
if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.RemoveColumn, col: col))) return;
|
||||
var prefabCallbacks = CaptureActivePrefabReleaseCallbacks();
|
||||
RemoveColumnImmediate(col);
|
||||
RebuildRenderBindings(prefabCallbacks);
|
||||
}
|
||||
|
||||
private void RemoveColumnImmediate(int col)
|
||||
@@ -762,26 +766,16 @@ namespace XericUI.XTable.Rendering.Component
|
||||
public void ClearData()
|
||||
{
|
||||
ThrowIfCheckingUpdate(nameof(ClearData));
|
||||
var prefabCallbacks = CaptureActivePrefabReleaseCallbacks();
|
||||
if (m_TableData != null)
|
||||
{
|
||||
// 清理预制体:先调用 OnPrefabRelease 再清理映射
|
||||
if (m_ActivePrefabMap != null)
|
||||
{
|
||||
foreach (var kv in m_ActivePrefabMap)
|
||||
{
|
||||
var cellData = m_TableData.GetCell(kv.Key.Item1, kv.Key.Item2);
|
||||
cellData?.OnPrefabRelease?.Invoke(kv.Value);
|
||||
}
|
||||
m_ActivePrefabMap.Clear();
|
||||
}
|
||||
m_PrefabPool?.ReturnAll();
|
||||
|
||||
m_TableData.ClearAllMerges();
|
||||
m_TableData.BlockMap.Clear();
|
||||
m_StyleNamespaceDirty = true;
|
||||
}
|
||||
m_Quadtree?.Clear();
|
||||
ClearSelection();
|
||||
RebuildRenderBindings(prefabCallbacks);
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace XericUI.XTable.Rendering.Component
|
||||
{
|
||||
Assembler = m_Assembler,
|
||||
PrefabPool = m_PrefabPool,
|
||||
ActivePrefabMap = m_ActivePrefabMap,
|
||||
CellPadding = CellPadding,
|
||||
Config = Config,
|
||||
SelectionTable = this,
|
||||
|
||||
@@ -19,11 +19,12 @@ namespace XericUI.XTable.Rendering.Component
|
||||
|
||||
/// <summary>可复用列表,避免每帧分配 GC</summary>
|
||||
[NonSerialized] private List<(int, int)> m_ReusableCellList = new(capacity: 128);
|
||||
|
||||
[NonSerialized] private List<QuadrantResult> m_ReusableQuadrants = new(capacity: 32);
|
||||
|
||||
/// <summary>控制滚动时是否使用四叉树(大表格推荐)</summary>
|
||||
private bool UseQuadtreeForScroll => m_Quadtree != null && m_Quadtree.IsBuilt
|
||||
&& m_RowHeights.Length * m_ColWidths.Length > 2000;
|
||||
&& m_RowHeights.Length * m_ColWidths.Length > 2000;
|
||||
|
||||
private struct CellTrackedObjs
|
||||
{
|
||||
@@ -52,6 +53,7 @@ namespace XericUI.XTable.Rendering.Component
|
||||
m_Assembler?.ReleaseText(objs.CellText);
|
||||
m_Assembler?.ReleaseImage(objs.CellImage);
|
||||
}
|
||||
|
||||
m_CellTracked.Clear();
|
||||
m_LastRenderedCells.Clear();
|
||||
}
|
||||
@@ -212,13 +214,19 @@ namespace XericUI.XTable.Rendering.Component
|
||||
{
|
||||
if (data.Prefab == null) return;
|
||||
|
||||
GameObject instance = m_PrefabPool.Acquire(data.Prefab, cellId);
|
||||
var key = (row, col);
|
||||
GameObject instance = null;
|
||||
bool wasActive = m_ActivePrefabMap != null
|
||||
&& m_ActivePrefabMap.TryGetValue(key, out instance);
|
||||
if (!wasActive)
|
||||
instance = m_PrefabPool.Acquire(data.Prefab, cellId);
|
||||
if (instance == null) return;
|
||||
RectTransform prt = instance.GetComponent<RectTransform>();
|
||||
prt.anchoredPosition = new Vector2(cellX, cellY);
|
||||
prt.sizeDelta = new Vector2(cellW, cellH);
|
||||
m_ActivePrefabMap[(row, col)] = instance;
|
||||
data.OnPrefabAcquire?.Invoke(instance);
|
||||
m_ActivePrefabMap[key] = instance;
|
||||
if (!wasActive)
|
||||
data.OnPrefabAcquire?.Invoke(instance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -227,6 +235,8 @@ namespace XericUI.XTable.Rendering.Component
|
||||
|
||||
private void ReleaseRenderedBindings(Dictionary<GameObject, System.Action<GameObject>> prefabReleaseCallbacks = null)
|
||||
{
|
||||
// 结构重建必须解除所有 cellId 映射;不能只依赖当前可见区域的 tracked 集合。
|
||||
m_Assembler?.ReleaseAllBindings();
|
||||
ReleaseAllTrackedCells();
|
||||
ReleaseFrozenCells();
|
||||
m_LastRenderedCells.Clear();
|
||||
@@ -238,6 +248,12 @@ namespace XericUI.XTable.Rendering.Component
|
||||
m_ActivePrefabMap?.Clear();
|
||||
}
|
||||
|
||||
/// <summary>结构删除后的唯一绑定重建入口;普通刷新和滚动不得调用。</summary>
|
||||
private void RebuildRenderBindings(Dictionary<GameObject, System.Action<GameObject>> prefabReleaseCallbacks)
|
||||
{
|
||||
ReleaseRenderedBindings(prefabReleaseCallbacks);
|
||||
}
|
||||
|
||||
public void RefreshView(bool releaseBindings = true)
|
||||
{
|
||||
TableDirtyType refreshedFlags = m_DirtyFlag.Flags;
|
||||
@@ -271,9 +287,6 @@ namespace XericUI.XTable.Rendering.Component
|
||||
|
||||
UpdateBackground();
|
||||
|
||||
if (releaseBindings)
|
||||
ReleaseRenderedBindings(CaptureActivePrefabReleaseCallbacks());
|
||||
|
||||
// 清理孤立子对象
|
||||
CleanOrphanChildren();
|
||||
|
||||
@@ -334,32 +347,9 @@ namespace XericUI.XTable.Rendering.Component
|
||||
var newVisible = new HashSet<(int, int)>();
|
||||
ComputeVisibleCellsScrollInto(newVisible);
|
||||
|
||||
// 2. 回收不再可见的追踪对象
|
||||
m_ReusableCellList.Clear();
|
||||
foreach (var cell in m_LastRenderedCells)
|
||||
if (!newVisible.Contains(cell))
|
||||
m_ReusableCellList.Add(cell);
|
||||
foreach (var cell in m_ReusableCellList)
|
||||
ReleaseTrackedCell(cell.Item1, cell.Item2);
|
||||
// 2. 离屏单元格保持其绑定,供后续重绘直接复用。
|
||||
|
||||
// 3. 回收不再可见的预制体(仅从映射表移除,预制体保留在计数器槽位中复用)
|
||||
if (m_ActivePrefabMap != null && m_ActivePrefabMap.Count > 0)
|
||||
{
|
||||
m_ReusableCellList.Clear();
|
||||
foreach (var kv in m_ActivePrefabMap)
|
||||
if (!newVisible.Contains(kv.Key))
|
||||
m_ReusableCellList.Add(kv.Key);
|
||||
|
||||
foreach (var key in m_ReusableCellList)
|
||||
{
|
||||
GameObject instance = m_ActivePrefabMap[key];
|
||||
var data = TableData.GetCell(key.Item1, key.Item2);
|
||||
m_PrefabPool?.Release(instance, data?.OnPrefabRelease);
|
||||
m_ActivePrefabMap.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 渲染单元格:选区变更时全量重渲所有可见单元格,否则仅新进入的
|
||||
// 3. 渲染单元格:选区变更时全量重渲所有可见单元格,否则仅新进入的
|
||||
foreach (var cell in newVisible)
|
||||
{
|
||||
if (!m_LastRenderedCells.Contains(cell))
|
||||
@@ -411,7 +401,8 @@ namespace XericUI.XTable.Rendering.Component
|
||||
if (m_BackgroundRenderer != null) return;
|
||||
|
||||
var backgroundGo = new GameObject("_TableBackground",
|
||||
typeof(RectTransform), typeof(CanvasRenderer), typeof(XTableBackgroundRenderer), typeof(XTableSelectionInput));
|
||||
typeof(RectTransform), typeof(CanvasRenderer), typeof(XTableBackgroundRenderer),
|
||||
typeof(XTableSelectionInput));
|
||||
backgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
|
||||
backgroundGo.transform.SetParent(ContentTransform, false);
|
||||
backgroundGo.transform.SetAsFirstSibling();
|
||||
@@ -451,7 +442,8 @@ namespace XericUI.XTable.Rendering.Component
|
||||
{
|
||||
if (string.IsNullOrEmpty(quadrant.Namespace) || quadrant.Namespace == baseNamespace)
|
||||
continue;
|
||||
rects.Add(new XTableBackgroundRenderer.RectData(quadrant.Region, GetCellBgColor(quadrant.Namespace)));
|
||||
rects.Add(new XTableBackgroundRenderer.RectData(quadrant.Region,
|
||||
GetCellBgColor(quadrant.Namespace)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -474,4 +466,4 @@ namespace XericUI.XTable.Rendering.Component
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,17 +72,20 @@ namespace XericUI.XTable.Rendering.Component
|
||||
private sealed class UpdateContext
|
||||
{
|
||||
public TableDirtyType DirtyFlags;
|
||||
public bool RequiresBindingRebuild;
|
||||
public readonly List<UpdateCommand> Commands = new();
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
DirtyFlags = TableDirtyType.None;
|
||||
RequiresBindingRebuild = false;
|
||||
Commands.Clear();
|
||||
}
|
||||
|
||||
public void MergeFrom(UpdateContext child)
|
||||
{
|
||||
DirtyFlags |= child.DirtyFlags;
|
||||
RequiresBindingRebuild |= child.RequiresBindingRebuild;
|
||||
Commands.AddRange(child.Commands);
|
||||
}
|
||||
}
|
||||
@@ -134,7 +137,10 @@ namespace XericUI.XTable.Rendering.Component
|
||||
private bool TryEnqueueUpdateCommand(UpdateCommand command)
|
||||
{
|
||||
if (!IsCheckingUpdate) return false;
|
||||
m_UpdateContextStack.Peek().Commands.Add(command);
|
||||
UpdateContext context = m_UpdateContextStack.Peek();
|
||||
context.Commands.Add(command);
|
||||
context.RequiresBindingRebuild |= command.Type == UpdateCommandType.RemoveRow
|
||||
|| command.Type == UpdateCommandType.RemoveColumn;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -158,12 +164,14 @@ namespace XericUI.XTable.Rendering.Component
|
||||
m_UpdateContextStack.Push(context);
|
||||
try
|
||||
{
|
||||
Dictionary<GameObject, Action<GameObject>> prefabCallbacks = CaptureActivePrefabReleaseCallbacks();
|
||||
Dictionary<GameObject, Action<GameObject>> prefabCallbacks = context.RequiresBindingRebuild
|
||||
? CaptureActivePrefabReleaseCallbacks() : null;
|
||||
ExecuteUpdateCommands(context);
|
||||
if (context.DirtyFlags == TableDirtyType.None) return;
|
||||
|
||||
m_UpdateContextStack.Pop();
|
||||
ReleaseRenderedBindings(prefabCallbacks);
|
||||
if (context.RequiresBindingRebuild)
|
||||
RebuildRenderBindings(prefabCallbacks);
|
||||
ApplyDirty(context.DirtyFlags);
|
||||
RefreshView(false);
|
||||
RefreshSelectionOverlays();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
|
||||
using UnityEngine;
|
||||
@@ -7,7 +8,7 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
{
|
||||
/// <summary>
|
||||
/// 表格单元动态拼装器——通过 SlotCounterPool 管理 Image + TMP_Text 的组合创建与回收。
|
||||
/// 每个单元格通过 cellId 映射到固定索引的计数器槽位,空闲槽位经 FrozenSlots 全局流转复用。
|
||||
/// cellId 在绑定期间稳定复用同一对象;对象显隐仅随所属 Counter 整体借出或归还变更。
|
||||
/// 所有元素统一使用 anchor=(0,1) pivot=(0,1) 左上角坐标系。
|
||||
/// 所有动态生成对象标记 HideFlags.DontSave | HideFlags.HideInHierarchy,防止泄漏到场景。
|
||||
/// </summary>
|
||||
@@ -21,7 +22,6 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
public RectTransform Rect;
|
||||
public Image Image;
|
||||
public StyleBoundElement Style;
|
||||
/// <summary>绑定的单元格 ID,用于 Release 定位槽位</summary>
|
||||
public int CellId;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
public RectTransform Rect;
|
||||
public TMP_Text Text;
|
||||
public StyleBoundElement Style;
|
||||
/// <summary>绑定的单元格 ID,用于 Release 定位槽位</summary>
|
||||
public int CellId;
|
||||
}
|
||||
|
||||
@@ -40,19 +39,10 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
#region 字段
|
||||
|
||||
private Transform m_Parent;
|
||||
|
||||
/// <summary>运行时的默认样式命名空间(编辑器下为 null,不绑定)</summary>
|
||||
private string m_DefaultStyleNamespace;
|
||||
|
||||
/// <summary>所有已绑定的 StyleBoundElement 回调集合(用于样式变更时通知外部)</summary>
|
||||
public event System.Action OnStyleChangeDetected;
|
||||
|
||||
/// <summary>基于计数器的 Image 槽位对象池</summary>
|
||||
private SlotCounterPool<PooledImage> m_ImagePool;
|
||||
/// <summary>基于计数器的 Text 槽位对象池</summary>
|
||||
private SlotCounterPool<PooledText> m_TextPool;
|
||||
|
||||
/// <summary>HideFlags 应用于所有动态创建的对象(不保存、不在 Hierarchy 显示)</summary>
|
||||
private const HideFlags POOL_FLAGS = HideFlags.DontSave | HideFlags.HideInHierarchy;
|
||||
|
||||
#endregion
|
||||
@@ -63,11 +53,13 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
{
|
||||
m_Parent = parent;
|
||||
m_DefaultStyleNamespace = defaultStyleNamespace;
|
||||
m_ImagePool = new SlotCounterPool<PooledImage>(() => CreateImage("cell_img"));
|
||||
m_TextPool = new SlotCounterPool<PooledText>(() => CreateText("cell_text"));
|
||||
m_ImagePool = new SlotCounterPool<PooledImage>(() => CreateImage("cell_img"),
|
||||
SetImageCounterActive, SetImageCounterInactive);
|
||||
m_TextPool = new SlotCounterPool<PooledText>(() => CreateText("cell_text"),
|
||||
SetTextCounterActive, SetTextCounterInactive);
|
||||
}
|
||||
|
||||
/// <summary>彻底销毁所有池对象(包括计数器内活跃槽位和冻结槽位)</summary>
|
||||
/// <summary>彻底销毁所有池对象。</summary>
|
||||
public void DestroyAll()
|
||||
{
|
||||
m_ImagePool.DestroyAll(item => KillObject(item.Root));
|
||||
@@ -95,48 +87,57 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
|
||||
#region 池操作
|
||||
|
||||
/// <summary>为指定 cellId 获取一个池化 Image</summary>
|
||||
/// <summary>为指定 cellId 获取池化 Image;已有绑定原样返回。</summary>
|
||||
public PooledImage GetImage(int cellId, string name = "cell_img", string styleNamespace = null,
|
||||
Transform parent = null)
|
||||
{
|
||||
var item = m_ImagePool.Acquire(cellId);
|
||||
var item = m_ImagePool.Acquire(cellId, out bool isNewBinding);
|
||||
if (item == null) return null;
|
||||
if (!isNewBinding)
|
||||
{
|
||||
if (parent != null && item.Root.transform.parent != parent)
|
||||
item.Root.transform.SetParent(parent, false);
|
||||
BindStyle(item.Style, styleNamespace);
|
||||
return item;
|
||||
}
|
||||
|
||||
if (parent != null && item.Root.transform.parent != parent)
|
||||
item.Root.transform.SetParent(parent, false);
|
||||
item.CellId = cellId;
|
||||
item.Root.SetActive(true);
|
||||
item.Root.name = name;
|
||||
ResetImageDefaults(item);
|
||||
BindStyle(item.Style, styleNamespace);
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>为指定 cellId 获取一个池化 TMP_Text</summary>
|
||||
/// <summary>为指定 cellId 获取池化 TMP_Text;已有绑定原样返回。</summary>
|
||||
public PooledText GetText(int cellId, string name = "cell_text", string styleNamespace = null,
|
||||
Transform parent = null)
|
||||
{
|
||||
var item = m_TextPool.Acquire(cellId);
|
||||
var item = m_TextPool.Acquire(cellId, out bool isNewBinding);
|
||||
if (item == null) return null;
|
||||
if (!isNewBinding)
|
||||
{
|
||||
if (parent != null && item.Root.transform.parent != parent)
|
||||
item.Root.transform.SetParent(parent, false);
|
||||
BindStyle(item.Style, styleNamespace);
|
||||
return item;
|
||||
}
|
||||
|
||||
if (parent != null && item.Root.transform.parent != parent)
|
||||
item.Root.transform.SetParent(parent, false);
|
||||
item.CellId = cellId;
|
||||
item.Root.SetActive(true);
|
||||
item.Root.name = name;
|
||||
ResetTextDefaults(item);
|
||||
BindStyle(item.Style, styleNamespace);
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>运行时绑定样式到池元素的 StyleBoundElement,实现被动更新(push)</summary>
|
||||
private void BindStyle(StyleBoundElement style, string cellNamespace)
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
|
||||
string ns = cellNamespace ?? m_DefaultStyleNamespace;
|
||||
if (string.IsNullOrEmpty(ns)) return;
|
||||
|
||||
if (style != null && style.BoundNamespace != ns)
|
||||
{
|
||||
style.OnStyleChangedCallback -= OnStyleChangedNotify;
|
||||
@@ -150,39 +151,64 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
OnStyleChangeDetected?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>归还所有槽位(全量刷新时调用)。被归还对象会隐藏,直到下次 Acquire。</summary>
|
||||
/// <summary>解除指定 cellId 的 Image 与 Text 槽位绑定。</summary>
|
||||
public void ReleaseCell(int cellId)
|
||||
{
|
||||
m_ImagePool.ReleaseCell(cellId);
|
||||
m_TextPool.ReleaseCell(cellId);
|
||||
}
|
||||
|
||||
/// <summary>解除全部 Image 与 Text 槽位绑定。</summary>
|
||||
public void ReleaseAllBindings()
|
||||
{
|
||||
m_ImagePool.ReleaseAllBindings();
|
||||
m_TextPool.ReleaseAllBindings();
|
||||
}
|
||||
|
||||
/// <summary>兼容旧 API:解除全部槽位绑定。</summary>
|
||||
public void ReturnAll()
|
||||
{
|
||||
m_ImagePool.ReturnAll(HideImage);
|
||||
m_TextPool.ReturnAll(HideText);
|
||||
ReleaseAllBindings();
|
||||
}
|
||||
|
||||
/// <summary>归还单个 Image 池对象并隐藏。</summary>
|
||||
/// <summary>解除指定 Image 的槽位绑定;显隐仅在所属 Counter 完整归还时变更。</summary>
|
||||
public void ReleaseImage(PooledImage item)
|
||||
{
|
||||
if (item == null) return;
|
||||
HideImage(item);
|
||||
m_ImagePool.ReleaseByObject(item);
|
||||
if (item != null) m_ImagePool.ReleaseCell(item.CellId);
|
||||
}
|
||||
|
||||
/// <summary>归还单个 Text 池对象并隐藏。</summary>
|
||||
/// <summary>解除指定 Text 的槽位绑定;显隐仅在所属 Counter 完整归还时变更。</summary>
|
||||
public void ReleaseText(PooledText item)
|
||||
{
|
||||
if (item == null) return;
|
||||
HideText(item);
|
||||
m_TextPool.ReleaseByObject(item);
|
||||
if (item != null) m_TextPool.ReleaseCell(item.CellId);
|
||||
}
|
||||
|
||||
private static void HideImage(PooledImage item)
|
||||
private static void SetImageCounterActive(IReadOnlyList<PooledImage> items)
|
||||
{
|
||||
item.Style?.Unbind();
|
||||
item.Root.SetActive(false);
|
||||
for (int i = 0; i < items.Count; i++) items[i].Root.SetActive(true);
|
||||
}
|
||||
|
||||
private static void HideText(PooledText item)
|
||||
private static void SetImageCounterInactive(IReadOnlyList<PooledImage> items)
|
||||
{
|
||||
item.Style?.Unbind();
|
||||
item.Root.SetActive(false);
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].Style?.Unbind();
|
||||
items[i].Root.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetTextCounterActive(IReadOnlyList<PooledText> items)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++) items[i].Root.SetActive(true);
|
||||
}
|
||||
|
||||
private static void SetTextCounterInactive(IReadOnlyList<PooledText> items)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].Style?.Unbind();
|
||||
items[i].Root.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -228,19 +254,11 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
|
||||
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 }
|
||||
};
|
||||
return new PooledImage { Root = go, Rect = rect, Image = img, Style = new StyleBoundElement { ImageComponent = img } };
|
||||
}
|
||||
|
||||
private PooledText CreateText(string name)
|
||||
@@ -254,19 +272,11 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
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 }
|
||||
};
|
||||
return new PooledText { Root = go, Rect = rect, Text = text, Style = new StyleBoundElement { TextComponent = text } };
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -30,14 +30,14 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
Transform targetParent = parent ?? m_Parent;
|
||||
if (pool.ActiveSlots.TryGetValue(cellId, out GameObject existing))
|
||||
{
|
||||
Prepare(existing, targetParent, cellId);
|
||||
UpdateLayout(existing, targetParent, cellId);
|
||||
return existing;
|
||||
}
|
||||
|
||||
GameObject instance = pool.Standby.Count > 0 ? pool.Standby.Pop() : Create(prefab, targetParent);
|
||||
if (instance == null) return null;
|
||||
pool.ActiveSlots[cellId] = instance;
|
||||
Prepare(instance, targetParent, cellId);
|
||||
PrepareNewBinding(instance, targetParent, cellId);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,13 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static void Prepare(GameObject instance, Transform parent, int cellId)
|
||||
private static void PrepareNewBinding(GameObject instance, Transform parent, int cellId)
|
||||
{
|
||||
UpdateLayout(instance, parent, cellId);
|
||||
instance.SetActive(true);
|
||||
}
|
||||
|
||||
private static void UpdateLayout(GameObject instance, Transform parent, int cellId)
|
||||
{
|
||||
if (parent != null && instance.transform.parent != parent)
|
||||
instance.transform.SetParent(parent, false);
|
||||
@@ -120,7 +126,6 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
rt.anchorMin = AnchorTopLeft;
|
||||
rt.anchorMax = AnchorTopLeft;
|
||||
rt.pivot = PivotTopLeft;
|
||||
instance.SetActive(true);
|
||||
}
|
||||
|
||||
private static void KillObject(GameObject go)
|
||||
|
||||
@@ -7,8 +7,7 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于计数器的槽位对象池(泛型)。
|
||||
/// 每个 cellId 映射到固定索引的 SlotCounter,释放槽位保留在原计数器内复用。
|
||||
/// 避免跨计数器拆分碰撞组导致的对象位置冲突。
|
||||
/// cellId 在绑定期间稳定地映射到同一槽位;计数器仅在全部槽位解除绑定后整体待命复用。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">池对象类型</typeparam>
|
||||
public class SlotCounterPool<T> where T : class
|
||||
@@ -17,32 +16,30 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
|
||||
private class Slot
|
||||
{
|
||||
/// <summary>当前绑定的单元格 ID(-1 = 空闲,可被偷取)</summary>
|
||||
public int CellId = -1;
|
||||
/// <summary>池对象引用</summary>
|
||||
public T PooledObject;
|
||||
}
|
||||
|
||||
private class SlotCounter
|
||||
{
|
||||
public List<Slot> Slots = new List<Slot>(capacity: 4);
|
||||
public readonly List<Slot> Slots = new List<Slot>(capacity: 4);
|
||||
public int BoundSlotCount;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 字段
|
||||
|
||||
/// <summary>计数器数组大小(= 池上限,也是 cellId 取模的模数)</summary>
|
||||
/// <summary>计数器数组大小(= 池上限,也是 cellId 路由键的模数)</summary>
|
||||
public const int POOL_SIZE = 256;
|
||||
|
||||
private SlotCounter[] m_Counters = new SlotCounter[POOL_SIZE];
|
||||
|
||||
/// <summary>已创建对象总数(不含预制体 Instantiate)</summary>
|
||||
private readonly SlotCounter[] m_Counters = new SlotCounter[POOL_SIZE];
|
||||
private readonly Stack<SlotCounter> m_StandbyCounters = new Stack<SlotCounter>();
|
||||
private readonly Func<T> m_Factory;
|
||||
private readonly Action<IReadOnlyList<T>> m_OnCounterBorrowed;
|
||||
private readonly Action<IReadOnlyList<T>> m_OnCounterReturned;
|
||||
private int m_CreatedCount;
|
||||
|
||||
/// <summary>对象工厂(构造函数传入)</summary>
|
||||
private Func<T> m_Factory;
|
||||
|
||||
/// <summary>已创建对象总数(只读)</summary>
|
||||
public int CreatedCount => m_CreatedCount;
|
||||
|
||||
@@ -50,106 +47,116 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
|
||||
#region 构造
|
||||
|
||||
public SlotCounterPool(Func<T> factory)
|
||||
public SlotCounterPool(Func<T> factory, Action<IReadOnlyList<T>> onCounterBorrowed = null,
|
||||
Action<IReadOnlyList<T>> onCounterReturned = null)
|
||||
{
|
||||
m_Factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
m_OnCounterBorrowed = onCounterBorrowed;
|
||||
m_OnCounterReturned = onCounterReturned;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 池操作
|
||||
|
||||
/// <summary>
|
||||
/// 为指定 cellId 获取一个池对象。
|
||||
/// 优先复用本计数器内的空闲槽位,最后创建新对象。
|
||||
/// </summary>
|
||||
public T Acquire(int cellId)
|
||||
/// <summary>为指定 cellId 获取池对象,并指示是否建立了新槽位绑定。</summary>
|
||||
public T Acquire(int cellId, out bool isNewBinding)
|
||||
{
|
||||
int index = cellId % POOL_SIZE;
|
||||
if (index < 0) index += POOL_SIZE; // 处理负 cellId
|
||||
|
||||
int index = GetRouteIndex(cellId);
|
||||
var counter = m_Counters[index];
|
||||
bool borrowedCounter = false;
|
||||
if (counter == null)
|
||||
{
|
||||
counter = new SlotCounter();
|
||||
counter = m_StandbyCounters.Count > 0 ? m_StandbyCounters.Pop() : new SlotCounter();
|
||||
m_Counters[index] = counter;
|
||||
borrowedCounter = true;
|
||||
}
|
||||
|
||||
// 1. 在计数器内查找空闲槽位
|
||||
for (int i = 0; i < counter.Slots.Count; i++)
|
||||
{
|
||||
var slot = counter.Slots[i];
|
||||
if (slot.CellId == cellId)
|
||||
{
|
||||
isNewBinding = false;
|
||||
return slot.PooledObject;
|
||||
}
|
||||
}
|
||||
|
||||
Slot availableSlot = null;
|
||||
for (int i = 0; i < counter.Slots.Count; i++)
|
||||
{
|
||||
if (counter.Slots[i].CellId == -1)
|
||||
{
|
||||
counter.Slots[i].CellId = cellId;
|
||||
return counter.Slots[i].PooledObject;
|
||||
availableSlot = counter.Slots[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 创建新对象。槽位不能跨计数器流转,避免破坏碰撞单元格的稳定关系。
|
||||
if (m_CreatedCount < POOL_SIZE)
|
||||
if (availableSlot == null)
|
||||
{
|
||||
var obj = m_Factory();
|
||||
var slot = new Slot { CellId = cellId, PooledObject = obj };
|
||||
counter.Slots.Add(slot);
|
||||
if (m_CreatedCount >= POOL_SIZE)
|
||||
{
|
||||
if (borrowedCounter)
|
||||
{
|
||||
m_Counters[index] = null;
|
||||
m_StandbyCounters.Push(counter);
|
||||
}
|
||||
Debug.LogWarning($"[SlotCounterPool<{typeof(T).Name}>] 已达上限 {POOL_SIZE},无法分配新对象");
|
||||
isNewBinding = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
availableSlot = new Slot { PooledObject = m_Factory() };
|
||||
counter.Slots.Add(availableSlot);
|
||||
m_CreatedCount++;
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 4. 已达上限
|
||||
Debug.LogWarning($"[SlotCounterPool<{typeof(T).Name}>] 已达上限 {POOL_SIZE},无法分配新对象");
|
||||
return null;
|
||||
availableSlot.CellId = cellId;
|
||||
counter.BoundSlotCount++;
|
||||
isNewBinding = true;
|
||||
if (borrowedCounter)
|
||||
m_OnCounterBorrowed?.Invoke(GetObjects(counter));
|
||||
return availableSlot.PooledObject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放指定 cellId 占用的槽位,并保留在原计数器中供相同碰撞组复用。
|
||||
/// </summary>
|
||||
public void Release(int cellId)
|
||||
/// <summary>为指定 cellId 获取一个池对象。</summary>
|
||||
public T Acquire(int cellId)
|
||||
{
|
||||
int index = cellId % POOL_SIZE;
|
||||
if (index < 0) index += POOL_SIZE;
|
||||
return Acquire(cellId, out _);
|
||||
}
|
||||
|
||||
/// <summary>解除 cellId 的槽位绑定;仅计数器完全空闲时才将其整体归还待命栈。</summary>
|
||||
public bool ReleaseCell(int cellId)
|
||||
{
|
||||
int index = GetRouteIndex(cellId);
|
||||
var counter = m_Counters[index];
|
||||
if (counter == null) return;
|
||||
if (counter == null) return false;
|
||||
|
||||
for (int i = 0; i < counter.Slots.Count; i++)
|
||||
{
|
||||
if (counter.Slots[i].CellId == cellId)
|
||||
{
|
||||
counter.Slots[i].CellId = -1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
var slot = counter.Slots[i];
|
||||
if (slot.CellId != cellId) continue;
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过池对象引用查找并释放对应槽位。
|
||||
/// 用于 Release(PooledObject) 场景,需要遍历查找。
|
||||
/// </summary>
|
||||
public bool ReleaseByObject(T pooledObject)
|
||||
{
|
||||
if (pooledObject == null) return false;
|
||||
|
||||
// 先遍历所有计数器的活跃槽位
|
||||
for (int ci = 0; ci < POOL_SIZE; ci++)
|
||||
{
|
||||
var counter = m_Counters[ci];
|
||||
if (counter == null) continue;
|
||||
|
||||
for (int i = 0; i < counter.Slots.Count; i++)
|
||||
{
|
||||
if (ReferenceEquals(counter.Slots[i].PooledObject, pooledObject))
|
||||
{
|
||||
counter.Slots[i].CellId = -1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
slot.CellId = -1;
|
||||
counter.BoundSlotCount--;
|
||||
if (counter.BoundSlotCount == 0)
|
||||
ReturnCounter(index, counter);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>释放所有活跃槽位;回调在槽位变为空闲前执行。</summary>
|
||||
public void ReturnAll(Action<T> beforeRelease = null)
|
||||
/// <summary>兼容旧 API:解除指定 cellId 的绑定。</summary>
|
||||
public void Release(int cellId)
|
||||
{
|
||||
ReleaseCell(cellId);
|
||||
}
|
||||
|
||||
/// <summary>通过池对象引用解除对应 cellId 的槽位绑定。</summary>
|
||||
public bool ReleaseByObject(T pooledObject)
|
||||
{
|
||||
if (pooledObject == null) return false;
|
||||
|
||||
for (int ci = 0; ci < POOL_SIZE; ci++)
|
||||
{
|
||||
var counter = m_Counters[ci];
|
||||
@@ -157,33 +164,81 @@ namespace XericUI.XTable.Rendering.Elements
|
||||
for (int i = 0; i < counter.Slots.Count; i++)
|
||||
{
|
||||
var slot = counter.Slots[i];
|
||||
if (slot.CellId == -1) continue;
|
||||
beforeRelease?.Invoke(slot.PooledObject);
|
||||
slot.CellId = -1;
|
||||
if (slot.CellId != -1 && ReferenceEquals(slot.PooledObject, pooledObject))
|
||||
return ReleaseCell(slot.CellId);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 彻底销毁所有池对象。
|
||||
/// </summary>
|
||||
public void DestroyAll(Action<T> destroyFunc)
|
||||
/// <summary>解除全部槽位绑定,并将所有计数器整体归还待命栈。</summary>
|
||||
public void ReleaseAllBindings()
|
||||
{
|
||||
if (destroyFunc == null) return;
|
||||
|
||||
for (int ci = 0; ci < POOL_SIZE; ci++)
|
||||
{
|
||||
var counter = m_Counters[ci];
|
||||
if (counter == null) continue;
|
||||
|
||||
foreach (var slot in counter.Slots)
|
||||
destroyFunc(slot.PooledObject);
|
||||
counter.Slots.Clear();
|
||||
for (int i = 0; i < counter.Slots.Count; i++)
|
||||
counter.Slots[i].CellId = -1;
|
||||
counter.BoundSlotCount = 0;
|
||||
ReturnCounter(ci, counter);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>兼容旧 API:解除所有活跃槽位绑定。</summary>
|
||||
public void ReturnAll(Action<T> beforeRelease = null)
|
||||
{
|
||||
if (beforeRelease != null)
|
||||
{
|
||||
for (int ci = 0; ci < POOL_SIZE; ci++)
|
||||
{
|
||||
var counter = m_Counters[ci];
|
||||
if (counter == null) continue;
|
||||
for (int i = 0; i < counter.Slots.Count; i++)
|
||||
if (counter.Slots[i].CellId != -1) beforeRelease(counter.Slots[i].PooledObject);
|
||||
}
|
||||
}
|
||||
ReleaseAllBindings();
|
||||
}
|
||||
|
||||
/// <summary>彻底销毁所有池对象。</summary>
|
||||
public void DestroyAll(Action<T> destroyFunc)
|
||||
{
|
||||
if (destroyFunc == null) return;
|
||||
var destroyedCounters = new HashSet<SlotCounter>();
|
||||
for (int ci = 0; ci < POOL_SIZE; ci++)
|
||||
{
|
||||
var counter = m_Counters[ci];
|
||||
if (counter != null) destroyedCounters.Add(counter);
|
||||
m_Counters[ci] = null;
|
||||
}
|
||||
foreach (var counter in m_StandbyCounters) destroyedCounters.Add(counter);
|
||||
m_StandbyCounters.Clear();
|
||||
foreach (var counter in destroyedCounters)
|
||||
foreach (var slot in counter.Slots) destroyFunc(slot.PooledObject);
|
||||
m_CreatedCount = 0;
|
||||
}
|
||||
|
||||
private static int GetRouteIndex(int cellId)
|
||||
{
|
||||
int index = cellId % POOL_SIZE;
|
||||
return index < 0 ? index + POOL_SIZE : index;
|
||||
}
|
||||
|
||||
private void ReturnCounter(int index, SlotCounter counter)
|
||||
{
|
||||
m_OnCounterReturned?.Invoke(GetObjects(counter));
|
||||
m_Counters[index] = null;
|
||||
m_StandbyCounters.Push(counter);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<T> GetObjects(SlotCounter counter)
|
||||
{
|
||||
var objects = new List<T>(counter.Slots.Count);
|
||||
for (int i = 0; i < counter.Slots.Count; i++) objects.Add(counter.Slots[i].PooledObject);
|
||||
return objects;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user