mirror of
https://github.com/GrahamKracker/UnityExplorer.git
synced 2025-07-15 07:56:41 +08:00
use UniverseLib
This commit is contained in:
@ -4,10 +4,13 @@ using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.Core.Input;
|
||||
using UniverseLib.Input;
|
||||
using UnityExplorer.Core.Runtime;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Panels;
|
||||
using UniverseLib.UI.Widgets;
|
||||
using UniverseLib;
|
||||
using UniverseLib.UI;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets.AutoComplete
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UniverseLib.UI;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets.AutoComplete
|
||||
{
|
||||
|
@ -1,14 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.Core.Input;
|
||||
using UnityExplorer.Core.Runtime;
|
||||
using UnityExplorer.UI.Models;
|
||||
using UnityExplorer.UI.Panels;
|
||||
using UniverseLib;
|
||||
using UniverseLib.UI;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets.AutoComplete
|
||||
{
|
||||
|
@ -1,136 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer;
|
||||
using UnityExplorer.Core;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Models;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets
|
||||
{
|
||||
public class AutoSliderScrollbar : UIBehaviourModel
|
||||
{
|
||||
public override GameObject UIRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Slider)
|
||||
return Slider.gameObject;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//public event Action<float> OnValueChanged;
|
||||
|
||||
internal readonly Scrollbar Scrollbar;
|
||||
internal readonly Slider Slider;
|
||||
internal RectTransform ContentRect;
|
||||
internal RectTransform ViewportRect;
|
||||
|
||||
//internal InputFieldScroller m_parentInputScroller;
|
||||
|
||||
public AutoSliderScrollbar(Scrollbar scrollbar, Slider slider, RectTransform contentRect, RectTransform viewportRect)
|
||||
{
|
||||
this.Scrollbar = scrollbar;
|
||||
this.Slider = slider;
|
||||
this.ContentRect = contentRect;
|
||||
this.ViewportRect = viewportRect;
|
||||
|
||||
this.Scrollbar.onValueChanged.AddListener(this.OnScrollbarValueChanged);
|
||||
this.Slider.onValueChanged.AddListener(this.OnSliderValueChanged);
|
||||
|
||||
//this.RefreshVisibility();
|
||||
this.Slider.Set(0f, false);
|
||||
}
|
||||
|
||||
private float lastAnchorPosition;
|
||||
private float lastContentHeight;
|
||||
private float lastViewportHeight;
|
||||
private bool _refreshWanted;
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
if (!Enabled)
|
||||
return;
|
||||
|
||||
_refreshWanted = false;
|
||||
if (ContentRect.localPosition.y != lastAnchorPosition)
|
||||
{
|
||||
lastAnchorPosition = ContentRect.localPosition.y;
|
||||
_refreshWanted = true;
|
||||
}
|
||||
if (ContentRect.rect.height != lastContentHeight)
|
||||
{
|
||||
lastContentHeight = ContentRect.rect.height;
|
||||
_refreshWanted = true;
|
||||
}
|
||||
if (ViewportRect.rect.height != lastViewportHeight)
|
||||
{
|
||||
lastViewportHeight = ViewportRect.rect.height;
|
||||
_refreshWanted = true;
|
||||
}
|
||||
|
||||
if (_refreshWanted)
|
||||
UpdateSliderHandle();
|
||||
}
|
||||
|
||||
public void UpdateSliderHandle()
|
||||
{
|
||||
// calculate handle size based on viewport / total data height
|
||||
var totalHeight = ContentRect.rect.height;
|
||||
var viewportHeight = ViewportRect.rect.height;
|
||||
|
||||
if (totalHeight <= viewportHeight)
|
||||
{
|
||||
Slider.handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 0f);
|
||||
Slider.value = 0f;
|
||||
Slider.interactable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var handleHeight = viewportHeight * Math.Min(1, viewportHeight / totalHeight);
|
||||
handleHeight = Math.Max(15f, handleHeight);
|
||||
|
||||
// resize the handle container area for the size of the handle (bigger handle = smaller container)
|
||||
var container = Slider.m_HandleContainerRect;
|
||||
container.offsetMax = new Vector2(container.offsetMax.x, -(handleHeight * 0.5f));
|
||||
container.offsetMin = new Vector2(container.offsetMin.x, handleHeight * 0.5f);
|
||||
|
||||
// set handle size
|
||||
Slider.handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, handleHeight);
|
||||
|
||||
// if slider is 100% height then make it not interactable
|
||||
Slider.interactable = !Mathf.Approximately(handleHeight, viewportHeight);
|
||||
|
||||
float val = 0f;
|
||||
if (totalHeight > 0f)
|
||||
val = (float)((decimal)ContentRect.localPosition.y / (decimal)(totalHeight - ViewportRect.rect.height));
|
||||
|
||||
Slider.value = val;
|
||||
}
|
||||
|
||||
public void OnScrollbarValueChanged(float value)
|
||||
{
|
||||
value = 1f - value;
|
||||
if (this.Slider.value != value)
|
||||
this.Slider.Set(value, false);
|
||||
//OnValueChanged?.Invoke(value);
|
||||
}
|
||||
|
||||
public void OnSliderValueChanged(float value)
|
||||
{
|
||||
value = 1f - value;
|
||||
this.Scrollbar.value = value;
|
||||
//OnValueChanged?.Invoke(value);
|
||||
}
|
||||
|
||||
public override void ConstructUI(GameObject parent)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets
|
||||
{
|
||||
public class ButtonCell : ICell
|
||||
{
|
||||
public float DefaultHeight => 25f;
|
||||
|
||||
public Action<int> OnClick;
|
||||
public int CurrentDataIndex;
|
||||
|
||||
public ButtonRef Button;
|
||||
|
||||
#region ICell
|
||||
|
||||
public bool Enabled => m_enabled;
|
||||
private bool m_enabled;
|
||||
|
||||
public GameObject UIRoot { get; set; }
|
||||
public RectTransform Rect { get; set; }
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
m_enabled = false;
|
||||
UIRoot.SetActive(false);
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
m_enabled = true;
|
||||
UIRoot.SetActive(true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public virtual GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
UIRoot = UIFactory.CreateHorizontalGroup(parent, "ButtonCell", true, false, true, true, 2, default,
|
||||
new Color(0.11f, 0.11f, 0.11f), TextAnchor.MiddleCenter);
|
||||
Rect = UIRoot.GetComponent<RectTransform>();
|
||||
Rect.anchorMin = new Vector2(0, 1);
|
||||
Rect.anchorMax = new Vector2(0, 1);
|
||||
Rect.pivot = new Vector2(0.5f, 1);
|
||||
Rect.sizeDelta = new Vector2(25, 25);
|
||||
UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0);
|
||||
|
||||
UIRoot.SetActive(false);
|
||||
|
||||
this.Button = UIFactory.CreateButton(UIRoot, "NameButton", "Name");
|
||||
UIFactory.SetLayoutElement(Button.Component.gameObject, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0);
|
||||
var buttonText = Button.Component.GetComponentInChildren<Text>();
|
||||
buttonText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
buttonText.alignment = TextAnchor.MiddleLeft;
|
||||
|
||||
Color normal = new Color(0.11f, 0.11f, 0.11f);
|
||||
Color highlight = new Color(0.16f, 0.16f, 0.16f);
|
||||
Color pressed = new Color(0.05f, 0.05f, 0.05f);
|
||||
Color disabled = new Color(1, 1, 1, 0);
|
||||
RuntimeProvider.Instance.SetColorBlock(Button.Component, normal, highlight, pressed, disabled);
|
||||
|
||||
Button.OnClick += () => { OnClick?.Invoke(CurrentDataIndex); };
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets
|
||||
{
|
||||
public class ButtonListHandler<TData, TCell> : ICellPoolDataSource<TCell> where TCell : ButtonCell
|
||||
{
|
||||
internal ScrollPool<TCell> ScrollPool;
|
||||
|
||||
public int ItemCount => currentEntries.Count;
|
||||
public readonly List<TData> currentEntries = new List<TData>();
|
||||
|
||||
public Func<List<TData>> GetEntries;
|
||||
public Action<TCell, int> SetICell;
|
||||
public Func<TData, string, bool> ShouldDisplay;
|
||||
public Action<int> OnCellClicked;
|
||||
|
||||
public string CurrentFilter
|
||||
{
|
||||
get => currentFilter;
|
||||
set => currentFilter = value ?? "";
|
||||
}
|
||||
private string currentFilter;
|
||||
|
||||
public ButtonListHandler(ScrollPool<TCell> scrollPool, Func<List<TData>> getEntriesMethod,
|
||||
Action<TCell, int> setICellMethod, Func<TData, string, bool> shouldDisplayMethod,
|
||||
Action<int> onCellClickedMethod)
|
||||
{
|
||||
ScrollPool = scrollPool;
|
||||
|
||||
GetEntries = getEntriesMethod;
|
||||
SetICell = setICellMethod;
|
||||
ShouldDisplay = shouldDisplayMethod;
|
||||
OnCellClicked = onCellClickedMethod;
|
||||
}
|
||||
|
||||
public void RefreshData()
|
||||
{
|
||||
var allEntries = GetEntries();
|
||||
currentEntries.Clear();
|
||||
|
||||
foreach (var entry in allEntries)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(currentFilter))
|
||||
{
|
||||
if (!ShouldDisplay(entry, currentFilter))
|
||||
continue;
|
||||
|
||||
currentEntries.Add(entry);
|
||||
}
|
||||
else
|
||||
currentEntries.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnCellBorrowed(TCell cell)
|
||||
{
|
||||
cell.OnClick += OnCellClicked;
|
||||
}
|
||||
|
||||
public virtual void SetCell(TCell cell, int index)
|
||||
{
|
||||
if (currentEntries == null)
|
||||
RefreshData();
|
||||
|
||||
if (index < 0 || index >= currentEntries.Count)
|
||||
cell.Disable();
|
||||
else
|
||||
{
|
||||
cell.Enable();
|
||||
cell.CurrentDataIndex = index;
|
||||
SetICell(cell, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.UI.Models;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets
|
||||
{
|
||||
// To fix an issue with Input Fields and allow them to go inside a ScrollRect nicely.
|
||||
|
||||
public class InputFieldScroller : UIBehaviourModel
|
||||
{
|
||||
public override GameObject UIRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
if (InputField.UIRoot)
|
||||
return InputField.UIRoot;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Action OnScroll;
|
||||
|
||||
internal AutoSliderScrollbar Slider;
|
||||
internal InputFieldRef InputField;
|
||||
|
||||
internal RectTransform ContentRect;
|
||||
internal RectTransform ViewportRect;
|
||||
|
||||
internal static CanvasScaler RootScaler;
|
||||
|
||||
public InputFieldScroller(AutoSliderScrollbar sliderScroller, InputFieldRef inputField)
|
||||
{
|
||||
this.Slider = sliderScroller;
|
||||
this.InputField = inputField;
|
||||
|
||||
inputField.OnValueChanged += OnTextChanged;
|
||||
|
||||
ContentRect = inputField.UIRoot.GetComponent<RectTransform>();
|
||||
ViewportRect = ContentRect.transform.parent.GetComponent<RectTransform>();
|
||||
|
||||
if (!RootScaler)
|
||||
RootScaler = UIManager.CanvasRoot.GetComponent<CanvasScaler>();
|
||||
}
|
||||
|
||||
internal string m_lastText;
|
||||
internal bool m_updateWanted;
|
||||
internal bool m_wantJumpToBottom;
|
||||
private float m_desiredContentHeight;
|
||||
|
||||
private float lastContentPosition;
|
||||
private float lastViewportHeight;
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
if (this.ContentRect.localPosition.y != lastContentPosition)
|
||||
{
|
||||
lastContentPosition = ContentRect.localPosition.y;
|
||||
OnScroll?.Invoke();
|
||||
}
|
||||
|
||||
if (ViewportRect.rect.height != lastViewportHeight)
|
||||
{
|
||||
lastViewportHeight = ViewportRect.rect.height;
|
||||
m_updateWanted = true;
|
||||
}
|
||||
|
||||
if (m_updateWanted)
|
||||
{
|
||||
m_updateWanted = false;
|
||||
ProcessInputText();
|
||||
|
||||
float desiredHeight = Math.Max(m_desiredContentHeight, ViewportRect.rect.height);
|
||||
|
||||
if (ContentRect.rect.height < desiredHeight)
|
||||
{
|
||||
ContentRect.sizeDelta = new Vector2(ContentRect.sizeDelta.x, desiredHeight);
|
||||
this.Slider.UpdateSliderHandle();
|
||||
}
|
||||
else if (ContentRect.rect.height > desiredHeight)
|
||||
{
|
||||
ContentRect.sizeDelta = new Vector2(ContentRect.sizeDelta.x, desiredHeight);
|
||||
this.Slider.UpdateSliderHandle();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_wantJumpToBottom)
|
||||
{
|
||||
Slider.Slider.value = 1f;
|
||||
m_wantJumpToBottom = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnTextChanged(string text)
|
||||
{
|
||||
m_lastText = text;
|
||||
m_updateWanted = true;
|
||||
}
|
||||
|
||||
internal void ProcessInputText()
|
||||
{
|
||||
var curInputRect = InputField.Component.textComponent.rectTransform.rect;
|
||||
var scaleFactor = RootScaler.scaleFactor;
|
||||
|
||||
// Current text settings
|
||||
var texGenSettings = InputField.Component.textComponent.GetGenerationSettings(curInputRect.size);
|
||||
texGenSettings.generateOutOfBounds = false;
|
||||
texGenSettings.scaleFactor = scaleFactor;
|
||||
|
||||
// Preferred text rect height
|
||||
var textGen = InputField.Component.textComponent.cachedTextGeneratorForLayout;
|
||||
m_desiredContentHeight = textGen.GetPreferredHeight(m_lastText, texGenSettings) + 10;
|
||||
}
|
||||
|
||||
public override void ConstructUI(GameObject parent)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,279 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.UI.Models;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets
|
||||
{
|
||||
public interface ICell : IPooledObject
|
||||
{
|
||||
bool Enabled { get; }
|
||||
|
||||
RectTransform Rect { get; set; }
|
||||
|
||||
void Enable();
|
||||
void Disable();
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets
|
||||
{
|
||||
public interface ICellPoolDataSource<T> where T : ICell
|
||||
{
|
||||
int ItemCount { get; }
|
||||
|
||||
void OnCellBorrowed(T cell);
|
||||
//void ReleaseCell(T cell);
|
||||
|
||||
void SetCell(T cell, int index);
|
||||
//void DisableCell(T cell, int index);
|
||||
}
|
||||
}
|
@ -1,703 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.Core.Input;
|
||||
using UnityExplorer.UI.Models;
|
||||
using UnityExplorer.UI.Panels;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets
|
||||
{
|
||||
public struct CellInfo
|
||||
{
|
||||
public int cellIndex, dataIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An object-pooled ScrollRect, attempts to support content of any size and provide a scrollbar for it.
|
||||
/// </summary>
|
||||
public class ScrollPool<T> : UIBehaviourModel, IEnumerable<CellInfo> where T : ICell
|
||||
{
|
||||
public ScrollPool(ScrollRect scrollRect)
|
||||
{
|
||||
this.ScrollRect = scrollRect;
|
||||
}
|
||||
|
||||
public ICellPoolDataSource<T> DataSource { get; set; }
|
||||
|
||||
public readonly List<T> CellPool = new List<T>();
|
||||
|
||||
internal DataHeightCache<T> HeightCache;
|
||||
|
||||
public float PrototypeHeight => _protoHeight ?? (float)(_protoHeight = Pool<T>.Instance.DefaultHeight);
|
||||
private float? _protoHeight;
|
||||
|
||||
public int ExtraPoolCells => 10;
|
||||
public float RecycleThreshold => PrototypeHeight * ExtraPoolCells;
|
||||
public float HalfThreshold => RecycleThreshold * 0.5f;
|
||||
|
||||
// UI
|
||||
|
||||
public override GameObject UIRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ScrollRect)
|
||||
return ScrollRect.gameObject;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public RectTransform Viewport => ScrollRect.viewport;
|
||||
public RectTransform Content => ScrollRect.content;
|
||||
|
||||
internal Slider slider;
|
||||
internal ScrollRect ScrollRect;
|
||||
internal VerticalLayoutGroup contentLayout;
|
||||
|
||||
// Cache / tracking
|
||||
|
||||
private Vector2 RecycleViewBounds;
|
||||
private Vector2 NormalizedScrollBounds;
|
||||
|
||||
/// <summary>
|
||||
/// The first and last pooled indices relative to the DataSource's list
|
||||
/// </summary>
|
||||
private int bottomDataIndex;
|
||||
private int TopDataIndex => Math.Max(0, bottomDataIndex - CellPool.Count + 1);
|
||||
private int CurrentDataCount => bottomDataIndex + 1;
|
||||
|
||||
private float TotalDataHeight => HeightCache.TotalHeight + contentLayout.padding.top + contentLayout.padding.bottom;
|
||||
|
||||
/// <summary>
|
||||
/// The first and last indices of our CellPool in the transform heirarchy
|
||||
/// </summary>
|
||||
private int topPoolIndex, bottomPoolIndex;
|
||||
|
||||
private Vector2 prevAnchoredPos;
|
||||
private float prevViewportHeight;
|
||||
|
||||
#region Internal set tracking and update
|
||||
|
||||
// A sanity check so only one thing is setting the value per frame.
|
||||
public bool WritingLocked
|
||||
{
|
||||
get => writingLocked || PanelDragger.Resizing;
|
||||
internal set
|
||||
{
|
||||
if (writingLocked == value)
|
||||
return;
|
||||
timeofLastWriteLock = Time.realtimeSinceStartup;
|
||||
writingLocked = value;
|
||||
}
|
||||
}
|
||||
private bool writingLocked;
|
||||
private float timeofLastWriteLock;
|
||||
|
||||
private float prevContentHeight = 1.0f;
|
||||
private event Action OnHeightChanged;
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
if (!ScrollRect || DataSource == null)
|
||||
return;
|
||||
|
||||
if (writingLocked && timeofLastWriteLock.OccuredEarlierThanDefault())
|
||||
writingLocked = false;
|
||||
|
||||
if (!writingLocked)
|
||||
{
|
||||
bool viewChange = CheckRecycleViewBounds(true);
|
||||
|
||||
if (viewChange || Content.rect.height != prevContentHeight)
|
||||
{
|
||||
prevContentHeight = Content.rect.height;
|
||||
OnValueChangedListener(Vector2.zero);
|
||||
|
||||
OnHeightChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endregion
|
||||
|
||||
// Public methods
|
||||
|
||||
public void Refresh(bool setCellData, bool jumpToTop = false)
|
||||
{
|
||||
if (jumpToTop)
|
||||
{
|
||||
bottomDataIndex = CellPool.Count - 1;
|
||||
Content.anchoredPosition = Vector2.zero;
|
||||
}
|
||||
|
||||
RefreshCells(setCellData, true);
|
||||
}
|
||||
|
||||
public void JumpToIndex(int index, Action<T> onJumped)
|
||||
{
|
||||
RefreshCells(true, true);
|
||||
|
||||
// Slide to the normalized position of the index
|
||||
var cache = HeightCache[index];
|
||||
float normalized = (cache.startPosition + (cache.height * 0.5f)) / HeightCache.TotalHeight;
|
||||
|
||||
RuntimeProvider.Instance.StartCoroutine(ForceDelayedJump(index, normalized, onJumped));
|
||||
}
|
||||
|
||||
private IEnumerator ForceDelayedJump(int dataIndex, float normalizedPos, Action<T> onJumped)
|
||||
{
|
||||
// Yielding two frames seems necessary in case the Explorer tab had not been opened before the jump.
|
||||
yield return null;
|
||||
yield return null;
|
||||
slider.value = normalizedPos;
|
||||
|
||||
// Get the cell containing the data index and invoke the onJumped listener for it
|
||||
foreach (var cellInfo in this)
|
||||
{
|
||||
if (cellInfo.dataIndex == dataIndex)
|
||||
{
|
||||
onJumped?.Invoke(CellPool[cellInfo.cellIndex]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IEnumerable
|
||||
|
||||
public IEnumerator<CellInfo> GetEnumerator() => EnumerateCellPool();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => EnumerateCellPool();
|
||||
|
||||
// Initialize
|
||||
|
||||
/// <summary>Should be called only once, when the scroll pool is created.</summary>
|
||||
public void Initialize(ICellPoolDataSource<T> dataSource, Action onHeightChangedListener = null)
|
||||
{
|
||||
this.DataSource = dataSource;
|
||||
HeightCache = new DataHeightCache<T>(this);
|
||||
|
||||
// Ensure the pool for the cell type is initialized.
|
||||
Pool<T>.GetPool();
|
||||
|
||||
this.contentLayout = ScrollRect.content.GetComponent<VerticalLayoutGroup>();
|
||||
this.slider = ScrollRect.GetComponentInChildren<Slider>();
|
||||
slider.onValueChanged.AddListener(OnSliderValueChanged);
|
||||
|
||||
ScrollRect.vertical = true;
|
||||
ScrollRect.horizontal = false;
|
||||
|
||||
RuntimeProvider.Instance.StartCoroutine(InitCoroutine(onHeightChangedListener));
|
||||
}
|
||||
|
||||
private IEnumerator InitCoroutine(Action onHeightChangedListener)
|
||||
{
|
||||
ScrollRect.content.anchoredPosition = Vector2.zero;
|
||||
yield return null;
|
||||
yield return null;
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(Content);
|
||||
|
||||
// set intial bounds
|
||||
prevAnchoredPos = Content.anchoredPosition;
|
||||
CheckRecycleViewBounds(false);
|
||||
|
||||
// create initial cell pool and set cells
|
||||
CreateCellPool();
|
||||
|
||||
foreach (var cell in this)
|
||||
SetCell(CellPool[cell.cellIndex], cell.dataIndex);
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(Content);
|
||||
prevContentHeight = Content.rect.height;
|
||||
// update slider
|
||||
SetScrollBounds();
|
||||
UpdateSliderHandle();
|
||||
|
||||
// add onValueChanged listener after setup
|
||||
ScrollRect.onValueChanged.AddListener(OnValueChangedListener);
|
||||
|
||||
OnHeightChanged += onHeightChangedListener;
|
||||
onHeightChangedListener?.Invoke();
|
||||
}
|
||||
|
||||
private void SetScrollBounds()
|
||||
{
|
||||
NormalizedScrollBounds = new Vector2(Viewport.rect.height * 0.5f, TotalDataHeight - (Viewport.rect.height * 0.5f));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// return value = viewport changed height
|
||||
/// </summary>
|
||||
private bool CheckRecycleViewBounds(bool extendPoolIfGrown)
|
||||
{
|
||||
RecycleViewBounds = new Vector2(Viewport.MinY() + HalfThreshold, Viewport.MaxY() - HalfThreshold);
|
||||
|
||||
if (extendPoolIfGrown && prevViewportHeight < Viewport.rect.height && prevViewportHeight != 0.0f)
|
||||
CheckExtendCellPool();
|
||||
|
||||
bool ret = prevViewportHeight == Viewport.rect.height;
|
||||
prevViewportHeight = Viewport.rect.height;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Cell pool
|
||||
|
||||
private CellInfo _current;
|
||||
|
||||
private IEnumerator<CellInfo> EnumerateCellPool()
|
||||
{
|
||||
int cellIdx = topPoolIndex;
|
||||
int dataIndex = TopDataIndex;
|
||||
int iterated = 0;
|
||||
while (iterated < CellPool.Count)
|
||||
{
|
||||
_current.cellIndex = cellIdx;
|
||||
_current.dataIndex = dataIndex;
|
||||
yield return _current;
|
||||
|
||||
cellIdx++;
|
||||
if (cellIdx >= CellPool.Count)
|
||||
cellIdx = 0;
|
||||
|
||||
dataIndex++;
|
||||
iterated++;
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateCellPool()
|
||||
{
|
||||
//ReleaseCells();
|
||||
|
||||
CheckDataSourceCountChange(out _);
|
||||
|
||||
float currentPoolCoverage = 0f;
|
||||
float requiredCoverage = ScrollRect.viewport.rect.height + RecycleThreshold;
|
||||
|
||||
topPoolIndex = 0;
|
||||
bottomPoolIndex = -1;
|
||||
|
||||
WritingLocked = true;
|
||||
// create cells until the Pool area is covered.
|
||||
// use minimum default height so that maximum pool count is reached.
|
||||
while (currentPoolCoverage <= requiredCoverage)
|
||||
{
|
||||
bottomPoolIndex++;
|
||||
|
||||
var cell = Pool<T>.Borrow();
|
||||
CellPool.Add(cell);
|
||||
DataSource.OnCellBorrowed(cell);
|
||||
cell.Rect.SetParent(ScrollRect.content, false);
|
||||
|
||||
currentPoolCoverage += PrototypeHeight;
|
||||
}
|
||||
|
||||
bottomDataIndex = CellPool.Count - 1;
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(Content);
|
||||
}
|
||||
|
||||
private bool CheckExtendCellPool()
|
||||
{
|
||||
CheckDataSourceCountChange();
|
||||
|
||||
var requiredCoverage = Math.Abs(RecycleViewBounds.y - RecycleViewBounds.x);
|
||||
var currentCoverage = CellPool.Count * PrototypeHeight;
|
||||
int cellsRequired = (int)Math.Floor((decimal)(requiredCoverage - currentCoverage) / (decimal)PrototypeHeight);
|
||||
if (cellsRequired > 0)
|
||||
{
|
||||
WritingLocked = true;
|
||||
|
||||
bottomDataIndex += cellsRequired;
|
||||
|
||||
// TODO sometimes still jumps a litte bit, need to figure out why.
|
||||
float prevAnchor = Content.localPosition.y;
|
||||
float prevHeight = Content.rect.height;
|
||||
|
||||
for (int i = 0; i < cellsRequired; i++)
|
||||
{
|
||||
var cell = Pool<T>.Borrow();
|
||||
DataSource.OnCellBorrowed(cell);
|
||||
cell.Rect.SetParent(ScrollRect.content, false);
|
||||
CellPool.Add(cell);
|
||||
|
||||
if (CellPool.Count > 1)
|
||||
{
|
||||
int index = CellPool.Count - 1 - (topPoolIndex % (CellPool.Count - 1));
|
||||
cell.Rect.SetSiblingIndex(index + 1);
|
||||
|
||||
if (bottomPoolIndex == index - 1)
|
||||
bottomPoolIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
RefreshCells(true, true);
|
||||
|
||||
//ExplorerCore.Log("Anchor: " + Content.localPosition.y + ", prev: " + prevAnchor);
|
||||
//ExplorerCore.Log("Height: " + Content.rect.height + ", prev:" + prevHeight);
|
||||
|
||||
if (Content.localPosition.y != prevAnchor)
|
||||
{
|
||||
var diff = Content.localPosition.y - prevAnchor;
|
||||
Content.localPosition = new Vector3(Content.localPosition.x, Content.localPosition.y - diff);
|
||||
}
|
||||
|
||||
if (Content.rect.height != prevHeight)
|
||||
{
|
||||
var diff = Content.rect.height - prevHeight;
|
||||
//ExplorerCore.Log("Height diff: " + diff);
|
||||
//Content.localPosition = new Vector3(Content.localPosition.x, Content.localPosition.y - diff);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Refresh methods
|
||||
|
||||
private bool CheckDataSourceCountChange() => CheckDataSourceCountChange(out _);
|
||||
|
||||
private bool CheckDataSourceCountChange(out bool shouldJumpToBottom)
|
||||
{
|
||||
shouldJumpToBottom = false;
|
||||
|
||||
int count = DataSource.ItemCount;
|
||||
if (bottomDataIndex > count && bottomDataIndex >= CellPool.Count)
|
||||
{
|
||||
bottomDataIndex = Math.Max(count - 1, CellPool.Count - 1);
|
||||
shouldJumpToBottom = true;
|
||||
}
|
||||
|
||||
if (HeightCache.Count < count)
|
||||
{
|
||||
HeightCache.SetIndex(count - 1, PrototypeHeight);
|
||||
return true;
|
||||
}
|
||||
else if (HeightCache.Count > count)
|
||||
{
|
||||
while (HeightCache.Count > count)
|
||||
HeightCache.RemoveLast();
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RefreshCells(bool andReloadFromDataSource, bool setSlider)
|
||||
{
|
||||
if (!CellPool.Any()) return;
|
||||
|
||||
CheckRecycleViewBounds(true);
|
||||
|
||||
CheckDataSourceCountChange(out bool jumpToBottom);
|
||||
|
||||
// update date height cache, and set cells if 'andReload'
|
||||
foreach (var cellInfo in this)
|
||||
{
|
||||
var cell = CellPool[cellInfo.cellIndex];
|
||||
if (andReloadFromDataSource)
|
||||
SetCell(cell, cellInfo.dataIndex);
|
||||
else
|
||||
HeightCache.SetIndex(cellInfo.dataIndex, cell.Rect.rect.height);
|
||||
}
|
||||
|
||||
// force check recycles
|
||||
if (andReloadFromDataSource)
|
||||
{
|
||||
RecycleBottomToTop();
|
||||
RecycleTopToBottom();
|
||||
}
|
||||
|
||||
if (setSlider)
|
||||
UpdateSliderHandle();
|
||||
|
||||
if (jumpToBottom)
|
||||
{
|
||||
var diff = Viewport.MaxY() - CellPool[bottomPoolIndex].Rect.MaxY();
|
||||
Content.anchoredPosition += Vector2.up * diff;
|
||||
}
|
||||
|
||||
if (andReloadFromDataSource)
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(Content);
|
||||
|
||||
SetScrollBounds();
|
||||
ScrollRect.UpdatePrevData();
|
||||
}
|
||||
|
||||
private void RefreshCellHeightsFast()
|
||||
{
|
||||
foreach (var cellInfo in this)
|
||||
HeightCache.SetIndex(cellInfo.dataIndex, CellPool[cellInfo.cellIndex].Rect.rect.height);
|
||||
}
|
||||
|
||||
private void SetCell(T cachedCell, int dataIndex)
|
||||
{
|
||||
cachedCell.Enable();
|
||||
DataSource.SetCell(cachedCell, dataIndex);
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(cachedCell.Rect);
|
||||
HeightCache.SetIndex(dataIndex, cachedCell.Rect.rect.height);
|
||||
}
|
||||
|
||||
// Value change processor
|
||||
|
||||
private void OnValueChangedListener(Vector2 val)
|
||||
{
|
||||
if (WritingLocked || DataSource == null)
|
||||
return;
|
||||
|
||||
if (InputManager.MouseScrollDelta != Vector2.zero)
|
||||
ScrollRect.StopMovement();
|
||||
|
||||
RefreshCellHeightsFast();
|
||||
|
||||
CheckRecycleViewBounds(true);
|
||||
|
||||
float yChange = ((Vector2)ScrollRect.content.localPosition - prevAnchoredPos).y;
|
||||
float adjust = 0f;
|
||||
|
||||
if (yChange > 0) // Scrolling down
|
||||
{
|
||||
if (ShouldRecycleTop)
|
||||
adjust = RecycleTopToBottom();
|
||||
|
||||
}
|
||||
else if (yChange < 0) // Scrolling up
|
||||
{
|
||||
if (ShouldRecycleBottom)
|
||||
adjust = RecycleBottomToTop();
|
||||
}
|
||||
|
||||
var vector = new Vector2(0, adjust);
|
||||
ScrollRect.m_ContentStartPosition += vector;
|
||||
ScrollRect.m_PrevPosition += vector;
|
||||
|
||||
prevAnchoredPos = ScrollRect.content.anchoredPosition;
|
||||
|
||||
SetScrollBounds();
|
||||
UpdateSliderHandle();
|
||||
}
|
||||
|
||||
private bool ShouldRecycleTop => GetCellExtent(CellPool[topPoolIndex].Rect) > RecycleViewBounds.x
|
||||
&& GetCellExtent(CellPool[bottomPoolIndex].Rect) > RecycleViewBounds.y;
|
||||
|
||||
private bool ShouldRecycleBottom => CellPool[bottomPoolIndex].Rect.position.y < RecycleViewBounds.y
|
||||
&& CellPool[topPoolIndex].Rect.position.y < RecycleViewBounds.x;
|
||||
|
||||
private float GetCellExtent(RectTransform cell) => cell.MaxY() - contentLayout.spacing;
|
||||
|
||||
private float RecycleTopToBottom()
|
||||
{
|
||||
float recycledheight = 0f;
|
||||
|
||||
while (ShouldRecycleTop && CurrentDataCount < DataSource.ItemCount)
|
||||
{
|
||||
WritingLocked = true;
|
||||
var cell = CellPool[topPoolIndex];
|
||||
|
||||
//Move top cell to bottom
|
||||
cell.Rect.SetAsLastSibling();
|
||||
var prevHeight = cell.Rect.rect.height;
|
||||
|
||||
// update content position
|
||||
Content.anchoredPosition -= Vector2.up * prevHeight;
|
||||
recycledheight += prevHeight + contentLayout.spacing;
|
||||
|
||||
//set Cell
|
||||
SetCell(cell, CurrentDataCount);
|
||||
|
||||
//set new indices
|
||||
bottomDataIndex++;
|
||||
|
||||
bottomPoolIndex = topPoolIndex;
|
||||
topPoolIndex = (topPoolIndex + 1) % CellPool.Count;
|
||||
}
|
||||
|
||||
return -recycledheight;
|
||||
}
|
||||
|
||||
private float RecycleBottomToTop()
|
||||
{
|
||||
float recycledheight = 0f;
|
||||
|
||||
while (ShouldRecycleBottom && CurrentDataCount > CellPool.Count)
|
||||
{
|
||||
WritingLocked = true;
|
||||
var cell = CellPool[bottomPoolIndex];
|
||||
|
||||
//Move bottom cell to top
|
||||
cell.Rect.SetAsFirstSibling();
|
||||
var prevHeight = cell.Rect.rect.height;
|
||||
|
||||
// update content position
|
||||
Content.anchoredPosition += Vector2.up * prevHeight;
|
||||
recycledheight += prevHeight + contentLayout.spacing;
|
||||
|
||||
//set new index
|
||||
bottomDataIndex--;
|
||||
|
||||
//set Cell
|
||||
SetCell(cell, TopDataIndex);
|
||||
|
||||
// move content again for new cell size
|
||||
var newHeight = cell.Rect.rect.height;
|
||||
var diff = newHeight - prevHeight;
|
||||
if (diff != 0.0f)
|
||||
{
|
||||
Content.anchoredPosition += Vector2.up * diff;
|
||||
recycledheight += diff;
|
||||
}
|
||||
|
||||
//set new indices
|
||||
topPoolIndex = bottomPoolIndex;
|
||||
bottomPoolIndex = (bottomPoolIndex - 1 + CellPool.Count) % CellPool.Count;
|
||||
}
|
||||
|
||||
return recycledheight;
|
||||
}
|
||||
|
||||
// Slider
|
||||
|
||||
private void OnSliderValueChanged(float val)
|
||||
{
|
||||
// Prevent spam invokes unless value is 0 or 1 (so we dont skip over the start/end)
|
||||
if (DataSource == null || (WritingLocked && val != 0 && val != 1))
|
||||
return;
|
||||
//this.WritingLocked = true;
|
||||
|
||||
ScrollRect.StopMovement();
|
||||
RefreshCellHeightsFast();
|
||||
|
||||
// normalize the scroll position for the scroll bounds.
|
||||
// point at the center of the viewport
|
||||
var desiredPosition = val * (NormalizedScrollBounds.y - NormalizedScrollBounds.x) + NormalizedScrollBounds.x;
|
||||
|
||||
// add offset above it for viewport height
|
||||
var halfView = Viewport.rect.height * 0.5f;
|
||||
var desiredMinY = desiredPosition - halfView;
|
||||
|
||||
// get the data index at the top of the viewport
|
||||
int topViewportIndex = HeightCache.GetFirstDataIndexAtPosition(desiredMinY);
|
||||
topViewportIndex = Math.Max(0, topViewportIndex);
|
||||
topViewportIndex = Math.Min(DataSource.ItemCount - 1, topViewportIndex);
|
||||
|
||||
// get the real top pooled data index to display our content
|
||||
int poolStartIndex = Math.Max(0, topViewportIndex - (int)(ExtraPoolCells * 0.5f));
|
||||
poolStartIndex = Math.Min(Math.Max(0, DataSource.ItemCount - CellPool.Count), poolStartIndex);
|
||||
|
||||
var topStartPos = HeightCache[poolStartIndex].startPosition;
|
||||
|
||||
float desiredAnchor;
|
||||
if (desiredMinY < HalfThreshold)
|
||||
desiredAnchor = desiredMinY;
|
||||
else
|
||||
desiredAnchor = desiredMinY - topStartPos;
|
||||
Content.anchoredPosition = new Vector2(0, desiredAnchor);
|
||||
|
||||
int desiredBottomIndex = poolStartIndex + CellPool.Count - 1;
|
||||
|
||||
// check if our pool indices contain the desired index. If so, rotate and set
|
||||
if (bottomDataIndex == desiredBottomIndex)
|
||||
{
|
||||
// cells will be the same, do nothing
|
||||
}
|
||||
else if (TopDataIndex > poolStartIndex && TopDataIndex < desiredBottomIndex)
|
||||
{
|
||||
// top cell falls within the new range, rotate around that
|
||||
int rotate = TopDataIndex - poolStartIndex;
|
||||
for (int i = 0; i < rotate; i++)
|
||||
{
|
||||
CellPool[bottomPoolIndex].Rect.SetAsFirstSibling();
|
||||
|
||||
//set new indices
|
||||
topPoolIndex = bottomPoolIndex;
|
||||
bottomPoolIndex = (bottomPoolIndex - 1 + CellPool.Count) % CellPool.Count;
|
||||
bottomDataIndex--;
|
||||
|
||||
SetCell(CellPool[topPoolIndex], TopDataIndex);
|
||||
}
|
||||
}
|
||||
else if (bottomDataIndex > poolStartIndex && bottomDataIndex < desiredBottomIndex)
|
||||
{
|
||||
// bottom cells falls within the new range, rotate around that
|
||||
int rotate = desiredBottomIndex - bottomDataIndex;
|
||||
for (int i = 0; i < rotate; i++)
|
||||
{
|
||||
CellPool[topPoolIndex].Rect.SetAsLastSibling();
|
||||
|
||||
//set new indices
|
||||
bottomPoolIndex = topPoolIndex;
|
||||
topPoolIndex = (topPoolIndex + 1) % CellPool.Count;
|
||||
bottomDataIndex++;
|
||||
|
||||
SetCell(CellPool[bottomPoolIndex], bottomDataIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bottomDataIndex = desiredBottomIndex;
|
||||
foreach (var info in this)
|
||||
{
|
||||
var cell = CellPool[info.cellIndex];
|
||||
SetCell(cell, info.dataIndex);
|
||||
}
|
||||
}
|
||||
|
||||
CheckRecycleViewBounds(true);
|
||||
|
||||
SetScrollBounds();
|
||||
ScrollRect.UpdatePrevData();
|
||||
|
||||
UpdateSliderHandle();
|
||||
}
|
||||
|
||||
private void UpdateSliderHandle()// bool forcePositionValue = true)
|
||||
{
|
||||
CheckDataSourceCountChange(out _);
|
||||
|
||||
var dataHeight = TotalDataHeight;
|
||||
|
||||
// calculate handle size based on viewport / total data height
|
||||
var viewportHeight = Viewport.rect.height;
|
||||
var handleHeight = viewportHeight * Math.Min(1, viewportHeight / dataHeight);
|
||||
handleHeight = Math.Max(15f, handleHeight);
|
||||
|
||||
// resize the handle container area for the size of the handle (bigger handle = smaller container)
|
||||
var container = slider.m_HandleContainerRect;
|
||||
container.offsetMax = new Vector2(container.offsetMax.x, -(handleHeight * 0.5f));
|
||||
container.offsetMin = new Vector2(container.offsetMin.x, handleHeight * 0.5f);
|
||||
|
||||
// set handle size
|
||||
slider.handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, handleHeight);
|
||||
|
||||
// if slider is 100% height then make it not interactable
|
||||
slider.interactable = !Mathf.Approximately(handleHeight, viewportHeight);
|
||||
|
||||
float val = 0f;
|
||||
if (TotalDataHeight > 0f)
|
||||
{
|
||||
float topPos = 0f;
|
||||
if (HeightCache.Count > 0)
|
||||
topPos = HeightCache[TopDataIndex].startPosition;
|
||||
|
||||
var scrollPos = topPos + Content.anchoredPosition.y;
|
||||
|
||||
var viewHeight = TotalDataHeight - Viewport.rect.height;
|
||||
if (viewHeight != 0.0f)
|
||||
val = (float)((decimal)scrollPos / (decimal)(viewHeight));
|
||||
else
|
||||
val = 0f;
|
||||
}
|
||||
|
||||
slider.Set(val, false);
|
||||
}
|
||||
|
||||
/// <summary>Use <see cref="UIFactory.CreateScrollPool"/></summary>
|
||||
public override void ConstructUI(GameObject parent) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer.UI
|
||||
{
|
||||
public static class UIExtension
|
||||
{
|
||||
public static void GetCorners(this RectTransform rect, Vector3[] corners)
|
||||
{
|
||||
Vector3 bottomLeft = new Vector3(rect.position.x, rect.position.y - rect.rect.height, 0);
|
||||
|
||||
corners[0] = bottomLeft;
|
||||
corners[1] = bottomLeft + new Vector3(0, rect.rect.height, 0);
|
||||
corners[2] = bottomLeft + new Vector3(rect.rect.width, rect.rect.height, 0);
|
||||
corners[3] = bottomLeft + new Vector3(rect.rect.width, 0, 0);
|
||||
}
|
||||
|
||||
// again, using position and rect instead of
|
||||
|
||||
public static float MaxY(this RectTransform rect) => rect.position.y - rect.rect.height;
|
||||
|
||||
public static float MinY(this RectTransform rect) => rect.position.y;
|
||||
|
||||
public static float MaxX(this RectTransform rect) => rect.position.x + rect.rect.width;
|
||||
|
||||
public static float MinX(this RectTransform rect) => rect.position.x;
|
||||
}
|
||||
}
|
@ -6,6 +6,9 @@ using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.Inspectors;
|
||||
using UnityExplorer.UI.Widgets;
|
||||
using UniverseLib;
|
||||
using UniverseLib.UI;
|
||||
using UniverseLib.UI.Widgets;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets
|
||||
{
|
||||
|
@ -6,6 +6,8 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UniverseLib;
|
||||
using UniverseLib.UI.Widgets;
|
||||
|
||||
namespace UnityExplorer.UI.Widgets
|
||||
{
|
||||
|
Reference in New Issue
Block a user