mirror of
https://github.com/GrahamKracker/UnityExplorer.git
synced 2025-07-04 04:22:53 +08:00
280 lines
10 KiB
C#
280 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
|
|
namespace UnityExplorer.UI.Widgets
|
|
{
|
|
public class DataHeightCache<T> where T : ICell
|
|
{
|
|
private ScrollPool<T> ScrollPool { get; }
|
|
|
|
public DataHeightCache(ScrollPool<T> scrollPool)
|
|
{
|
|
ScrollPool = scrollPool;
|
|
}
|
|
|
|
private readonly List<DataViewInfo> heightCache = new List<DataViewInfo>();
|
|
|
|
public DataViewInfo this[int index]
|
|
{
|
|
get => heightCache[index];
|
|
set => SetIndex(index, value);
|
|
}
|
|
|
|
public int Count => heightCache.Count;
|
|
|
|
public float TotalHeight => totalHeight;
|
|
private float totalHeight;
|
|
|
|
public float DefaultHeight => m_defaultHeight ?? (float)(m_defaultHeight = ScrollPool.PrototypeHeight);
|
|
private float? m_defaultHeight;
|
|
|
|
/// <summary>
|
|
/// Lookup table for "which data index first appears at this position"<br/>
|
|
/// Index: DefaultHeight * index from top of data<br/>
|
|
/// Value: the first data index at this position<br/>
|
|
/// </summary>
|
|
private readonly List<int> rangeCache = new List<int>();
|
|
|
|
/// <summary>Same as GetRangeIndexOfPosition, except this rounds up to the next division if there was remainder from the previous cell.</summary>
|
|
private int GetRangeCeilingOfPosition(float position) => (int)Math.Ceiling((decimal)position / (decimal)DefaultHeight);
|
|
|
|
/// <summary>Get the first range (division of DefaultHeight) which the position appears in.</summary>
|
|
private int GetRangeFloorOfPosition(float position) => (int)Math.Floor((decimal)position / (decimal)DefaultHeight);
|
|
|
|
public int GetFirstDataIndexAtPosition(float desiredHeight)
|
|
{
|
|
if (!heightCache.Any())
|
|
return 0;
|
|
|
|
int rangeIndex = GetRangeFloorOfPosition(desiredHeight);
|
|
|
|
// probably shouldnt happen but just in case
|
|
if (rangeIndex < 0)
|
|
return 0;
|
|
if (rangeIndex >= rangeCache.Count)
|
|
{
|
|
int idx = ScrollPool.DataSource.ItemCount - 1;
|
|
return idx;
|
|
}
|
|
|
|
int dataIndex = rangeCache[rangeIndex];
|
|
var cache = heightCache[dataIndex];
|
|
|
|
// if the DataViewInfo is outdated, need to rebuild
|
|
int expectedMin = GetRangeCeilingOfPosition(cache.startPosition);
|
|
int expectedMax = expectedMin + cache.normalizedSpread - 1;
|
|
if (rangeIndex < expectedMin || rangeIndex > expectedMax)
|
|
{
|
|
RecalculateStartPositions(ScrollPool.DataSource.ItemCount - 1);
|
|
|
|
rangeIndex = GetRangeFloorOfPosition(desiredHeight);
|
|
dataIndex = rangeCache[rangeIndex];
|
|
}
|
|
|
|
return dataIndex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the spread of the height, starting from the start position.<br/><br/>
|
|
/// The "spread" begins at the start of the next interval of the DefaultHeight, then increases for
|
|
/// every interval beyond that.
|
|
/// </summary>
|
|
private int GetRangeSpread(float startPosition, float height)
|
|
{
|
|
// get the remainder of the start position divided by min height
|
|
float rem = startPosition % DefaultHeight;
|
|
|
|
// if there is a remainder, this means the previous cell started in our first cell and
|
|
// they take priority, so reduce our height by (minHeight - remainder) to account for that.
|
|
// We need to fill that gap and reach the next cell before we take priority.
|
|
if (rem != 0.0f)
|
|
height -= (DefaultHeight - rem);
|
|
|
|
return (int)Math.Ceiling((decimal)height / (decimal)DefaultHeight);
|
|
}
|
|
|
|
/// <summary>Append a data index to the cache with the provided height value.</summary>
|
|
public void Add(float value)
|
|
{
|
|
value = Math.Max(DefaultHeight, value);
|
|
|
|
int spread = GetRangeSpread(totalHeight, value);
|
|
|
|
heightCache.Add(new DataViewInfo(heightCache.Count, value, totalHeight, spread));
|
|
|
|
int dataIdx = heightCache.Count - 1;
|
|
for (int i = 0; i < spread; i++)
|
|
rangeCache.Add(dataIdx);
|
|
|
|
totalHeight += value;
|
|
}
|
|
|
|
/// <summary>Remove the last (highest count) index from the height cache.</summary>
|
|
public void RemoveLast()
|
|
{
|
|
if (!heightCache.Any())
|
|
return;
|
|
|
|
totalHeight -= heightCache[heightCache.Count - 1];
|
|
heightCache.RemoveAt(heightCache.Count - 1);
|
|
|
|
int idx = heightCache.Count;
|
|
while (rangeCache.Count > 0 && rangeCache[rangeCache.Count - 1] == idx)
|
|
rangeCache.RemoveAt(rangeCache.Count - 1);
|
|
}
|
|
|
|
/// <summary>Set a given data index with the specified value.</summary>
|
|
public void SetIndex(int dataIndex, float height)
|
|
{
|
|
height = (float)Math.Floor(height);
|
|
height = Math.Max(DefaultHeight, height);
|
|
|
|
// If the index being set is beyond the DataSource item count, prune and return.
|
|
if (dataIndex >= ScrollPool.DataSource.ItemCount)
|
|
{
|
|
while (heightCache.Count > dataIndex)
|
|
RemoveLast();
|
|
return;
|
|
}
|
|
|
|
// If the data index exceeds our cache count, fill the gap.
|
|
// This is done by the ScrollPool when the DataSource sets its initial count, or the count increases.
|
|
if (dataIndex >= heightCache.Count)
|
|
{
|
|
while (dataIndex > heightCache.Count)
|
|
Add(DefaultHeight);
|
|
Add(height);
|
|
return;
|
|
}
|
|
|
|
// We are actually updating an index. First, update the height and the totalHeight.
|
|
var cache = heightCache[dataIndex];
|
|
if (cache.height != height)
|
|
{
|
|
var diff = height - cache.height;
|
|
totalHeight += diff;
|
|
cache.height = height;
|
|
}
|
|
|
|
// update our start position using the previous cell (if it exists)
|
|
if (dataIndex > 0)
|
|
{
|
|
var prev = heightCache[dataIndex - 1];
|
|
cache.startPosition = prev.startPosition + prev.height;
|
|
}
|
|
|
|
// Get the normalized range index (actually ceiling) and spread based on our start position and height
|
|
int rangeIndex = GetRangeCeilingOfPosition(cache.startPosition);
|
|
int spread = GetRangeSpread(cache.startPosition, height);
|
|
|
|
// If the previous item in the range cache is not the previous data index, there is a gap.
|
|
if (rangeCache[rangeIndex] != dataIndex)
|
|
{
|
|
// Recalculate start positions up to this index. The gap could be anywhere before here.
|
|
RecalculateStartPositions(ScrollPool.DataSource.ItemCount - 1);
|
|
// Get the range index and spread again after rebuilding
|
|
rangeIndex = GetRangeCeilingOfPosition(cache.startPosition);
|
|
spread = GetRangeSpread(cache.startPosition, height);
|
|
}
|
|
|
|
if (rangeCache[rangeIndex] != dataIndex)
|
|
throw new IndexOutOfRangeException($"Trying to set dataIndex {dataIndex} at rangeIndex {rangeIndex}, but cache is corrupt or invalid!");
|
|
|
|
if (spread != cache.normalizedSpread)
|
|
{
|
|
int spreadDiff = spread - cache.normalizedSpread;
|
|
cache.normalizedSpread = spread;
|
|
|
|
UpdateSpread(dataIndex, rangeIndex, spreadDiff);
|
|
}
|
|
|
|
// set the struct back to the array
|
|
heightCache[dataIndex] = cache;
|
|
}
|
|
|
|
private void UpdateSpread(int dataIndex, int rangeIndex, int spreadDiff)
|
|
{
|
|
if (spreadDiff > 0)
|
|
{
|
|
while (rangeCache[rangeIndex] == dataIndex && spreadDiff > 0)
|
|
{
|
|
rangeCache.Insert(rangeIndex, dataIndex);
|
|
spreadDiff--;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (rangeCache[rangeIndex] == dataIndex && spreadDiff < 0)
|
|
{
|
|
rangeCache.RemoveAt(rangeIndex);
|
|
spreadDiff++;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RecalculateStartPositions(int toIndex)
|
|
{
|
|
if (heightCache.Count <= 1)
|
|
return;
|
|
|
|
rangeCache.Clear();
|
|
|
|
DataViewInfo cache;
|
|
DataViewInfo prev = DataViewInfo.None;
|
|
for (int idx = 0; idx <= toIndex && idx < heightCache.Count; idx++)
|
|
{
|
|
cache = heightCache[idx];
|
|
|
|
if (!prev.Equals(DataViewInfo.None))
|
|
cache.startPosition = prev.startPosition + prev.height;
|
|
else
|
|
cache.startPosition = 0;
|
|
|
|
cache.normalizedSpread = GetRangeSpread(cache.startPosition, cache.height);
|
|
for (int i = 0; i < cache.normalizedSpread; i++)
|
|
rangeCache.Add(cache.dataIndex);
|
|
|
|
heightCache[idx] = cache;
|
|
|
|
prev = cache;
|
|
}
|
|
}
|
|
|
|
public struct DataViewInfo
|
|
{
|
|
// static
|
|
public static DataViewInfo None => s_default;
|
|
private static DataViewInfo s_default = default;
|
|
|
|
public static implicit operator float(DataViewInfo it) => it.height;
|
|
|
|
public DataViewInfo(int index, float height, float startPos, int spread)
|
|
{
|
|
this.dataIndex = index;
|
|
this.height = height;
|
|
this.startPosition = startPos;
|
|
this.normalizedSpread = spread;
|
|
}
|
|
|
|
// instance
|
|
public int dataIndex, normalizedSpread;
|
|
public float height, startPosition;
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
var other = (DataViewInfo)obj;
|
|
|
|
return this.dataIndex == other.dataIndex
|
|
&& this.height == other.height
|
|
&& this.startPosition == other.startPosition
|
|
&& this.normalizedSpread == other.normalizedSpread;
|
|
}
|
|
|
|
public override int GetHashCode() => base.GetHashCode();
|
|
}
|
|
}
|
|
}
|