Finally got the scrollpool working properly with dynamic content height

This commit is contained in:
Sinai 2021-04-19 23:47:25 +10:00
parent 7a253fa0a0
commit 300b35c2d3
8 changed files with 149 additions and 160 deletions

View File

@ -53,19 +53,19 @@ namespace UnityExplorer.UI.Panels
//scrollPool.ReloadData();
}
//scrollPool.Refresh();
scrollPool.RefreshCells(true);
}
public override void SetDefaultPosAndAnchors()
{
mainPanelRect.localPosition = Vector2.zero;
mainPanelRect.anchorMin = new Vector2(1, 0);
mainPanelRect.anchorMax = new Vector2(1, 1);
mainPanelRect.sizeDelta = new Vector2(-300f, mainPanelRect.sizeDelta.y);
mainPanelRect.anchoredPosition = new Vector2(-200, 0);
mainPanelRect.localPosition = Vector2.zero;
mainPanelRect.pivot = new Vector2(0.5f, 0.5f);
mainPanelRect.anchorMin = new Vector2(0.5f, 0);
mainPanelRect.anchorMax = new Vector2(0.5f, 1);
mainPanelRect.offsetMin = new Vector2(mainPanelRect.offsetMin.x, 100); // bottom
mainPanelRect.offsetMax = new Vector2(mainPanelRect.offsetMax.x, -50); // top
mainPanelRect.pivot = new Vector2(0.5f, 0.5f);
mainPanelRect.sizeDelta = new Vector2(700f, mainPanelRect.sizeDelta.y);
mainPanelRect.anchoredPosition = new Vector2(-150, 0);
}
private ScrollPool scrollPool;
@ -83,14 +83,14 @@ namespace UnityExplorer.UI.Panels
var test = new DynamicListTest(scrollPool, this);
test.Init();
var prototype = DynamicCellTest.CreatePrototypeCell(scrollContent);
var prototype = DynamicCell.CreatePrototypeCell(scrollContent);
scrollPool.PrototypeCell = prototype.GetComponent<RectTransform>();
dummyContentHolder = new GameObject("DummyHolder");
dummyContentHolder.SetActive(false);
GameObject.DontDestroyOnLoad(dummyContentHolder);
for (int i = 0; i < 10; i++)
for (int i = 0; i < 100; i++)
{
dummyContents.Add(CreateDummyContent());
}
@ -104,15 +104,25 @@ namespace UnityExplorer.UI.Panels
private GameObject CreateDummyContent()
{
var obj = UIFactory.CreateVerticalGroup(dummyContentHolder, "Content", true, true, true, true, 2, new Vector4(2, 2, 2, 2));
UIFactory.SetLayoutElement(obj, minHeight: 25, flexibleHeight: 9999);
//UIFactory.SetLayoutElement(obj, minHeight: 25, flexibleHeight: 9999);
obj.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var label = UIFactory.CreateLabel(obj, "label", "Dummy " + dummyContents.Count, TextAnchor.MiddleCenter);
UIFactory.SetLayoutElement(label.gameObject, minHeight: 25, flexibleHeight: 0);
var inputObj = UIFactory.CreateInputField(obj, "input", "...", out InputField inputField);
inputField.lineType = InputField.LineType.MultiLineNewline;
//var input = UIFactory.CreateSrollInputField(obj, "input2", "...", out InputFieldScroller inputScroller);
//UIFactory.SetLayoutElement(input, minHeight: 50, flexibleHeight: 9999);
//input.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var inputObj = UIFactory.CreateInputField(obj, "input", "...", out var inputField);
UIFactory.SetLayoutElement(inputObj, minHeight: 25, flexibleHeight: 9999);
//inputObj.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
inputObj.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
inputField.lineType = InputField.LineType.MultiLineNewline;
int numLines = UnityEngine.Random.Range(0, 10);
inputField.text = "This field has " + numLines + " lines";
for (int i = 0; i < numLines; i++)
inputField.text += "\r\n";
return obj;
}
@ -138,7 +148,7 @@ namespace UnityExplorer.UI.Panels
Scroller.Initialize(this);
}
public ICell CreateCell(RectTransform cellTransform) => new DynamicCellTest(cellTransform.gameObject);
public ICell CreateCell(RectTransform cellTransform) => new DynamicCell(cellTransform.gameObject);
public void SetCell(ICell icell, int index)
{
@ -148,58 +158,16 @@ namespace UnityExplorer.UI.Panels
return;
}
var root = (icell as DynamicCellTest).uiRoot;
var root = (icell as DynamicCell).uiRoot;
var content = Inspector.dummyContents[index];
if (content.transform.parent.ReferenceEqual(root.transform))
return;
if (root.transform.Find("Content") is Transform existing)
{
ExplorerCore.Log("removing existing content");
existing.transform.SetParent(Inspector.dummyContentHolder.transform, false);
}
var content = Inspector.dummyContents[index];
content.transform.SetParent(root.transform, false);
}
}
public class DynamicCellTest : ICell
{
public DynamicCellTest(GameObject uiRoot)
{
this.uiRoot = uiRoot;
}
public bool Enabled => m_enabled;
private bool m_enabled;
public GameObject uiRoot;
public InputField input;
public void Disable()
{
m_enabled = false;
uiRoot.SetActive(false);
}
public void Enable()
{
m_enabled = true;
uiRoot.SetActive(true);
}
public static GameObject CreatePrototypeCell(GameObject parent)
{
var prototype = UIFactory.CreateVerticalGroup(parent, "PrototypeCell", true, true, true, true, 2, default,
new Color(0.15f, 0.15f, 0.15f), TextAnchor.MiddleCenter);
var rect = prototype.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(prototype, minWidth: 100, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 9999);
prototype.SetActive(false);
return prototype;
}
}
}

View File

@ -461,7 +461,7 @@ namespace UnityExplorer.UI
var mainObj = CreateScrollView(parent, "InputFieldScrollView", out GameObject scrollContent, out SliderScrollbar scroller, color);
var inputObj = CreateInputField(scrollContent, name, placeHolderText ?? "...", out InputField inputField, fontSize, 0);
CreateInputField(scrollContent, name, placeHolderText ?? "...", out InputField inputField, fontSize, 0);
inputField.lineType = InputField.LineType.MultiLineNewline;
inputField.targetGraphic.color = color;
@ -471,6 +471,31 @@ namespace UnityExplorer.UI
return mainObj;
}
// Little helper class to force rebuild of an input field's layout on value change.
// This is limited to once per frame per input field, so its not too expensive.
private class InputFieldRefresher
{
private float timeOfLastRebuild;
private readonly RectTransform rectTransform;
public InputFieldRefresher(InputField inputField)
{
if (!inputField)
return;
rectTransform = inputField.GetComponent<RectTransform>();
inputField.onValueChanged.AddListener((string val) =>
{
if (Time.time > timeOfLastRebuild)
{
timeOfLastRebuild = Time.time;
LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
}
});
}
}
/// <summary>
/// Create a standard InputField control.
/// </summary>
@ -546,6 +571,8 @@ namespace UnityExplorer.UI
inputField.textComponent = inputText;
new InputFieldRefresher(inputField);
return mainObj;
}
@ -715,14 +742,17 @@ namespace UnityExplorer.UI
var contentRect = content.GetComponent<RectTransform>();
contentRect.anchorMin = Vector2.zero;
contentRect.anchorMax = Vector2.one;
contentRect.pivot = new Vector2(0.0f, 1.0f);
contentRect.pivot = new Vector2(0.5f, 1f);
contentRect.sizeDelta = new Vector2(0f, 0f);
contentRect.offsetMax = new Vector2(0f, 0f);
SetLayoutGroup<VerticalLayoutGroup>(content, true, false, true, true, 0, 2, 2, 2, 2, TextAnchor.UpperCenter);
content.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var scrollRect = mainObj.AddComponent<ScrollRect>();
scrollRect.movementType = ScrollRect.MovementType.Clamped;
scrollRect.inertia = false;
//scrollRect.decelerationRate = 0.135f;
scrollRect.scrollSensitivity = 15;
scrollRect.horizontal = false;
scrollRect.vertical = true;
@ -730,6 +760,8 @@ namespace UnityExplorer.UI
scrollRect.viewport = viewportRect;
scrollRect.content = contentRect;
// Slider
var sliderContainer = CreateVerticalGroup(mainObj, "SliderContainer",
false, false, true, true, 0, default, new Color(0.05f, 0.05f, 0.05f));
SetLayoutElement(sliderContainer, minWidth: 25, flexibleWidth:0, flexibleHeight: 9999);
@ -753,6 +785,8 @@ namespace UnityExplorer.UI
container.pivot = new Vector3(0.5f, 0.5f);
}
// finalize and create ScrollPool
uiRoot = mainObj;
var scrollPool = new ScrollPool(scrollRect)
@ -760,14 +794,8 @@ namespace UnityExplorer.UI
AutoResizeHandleRect = autoResizeSliderHandle
};
SetLayoutGroup<VerticalLayoutGroup>(content, true, false, true, false, 0, 2, 2, 2, 2,
TextAnchor.UpperCenter);
//viewportObj.GetComponent<Mask>().enabled = false;
var rect = content.GetComponent<RectTransform>();
rect.pivot = new Vector2(0.5f, 1f);
return scrollPool;
}

View File

@ -14,13 +14,13 @@ namespace UnityExplorer.UI.Widgets
public Action<ButtonCell<T>> OnClick;
public ButtonListCell<T> list;
public ButtonListSource<T> list;
public GameObject uiRoot;
public Text buttonText;
public Button button;
public ButtonCell(ButtonListCell<T> list, GameObject uiRoot, Button button, Text text)
public ButtonCell(ButtonListSource<T> list, GameObject uiRoot, Button button, Text text)
{
this.list = list;
this.uiRoot = uiRoot;

View File

@ -9,7 +9,7 @@ using UnityExplorer.UI.Widgets;
namespace UnityExplorer.UI.Widgets
{
public class ButtonListCell<T> : IPoolDataSource
public class ButtonListSource<T> : IPoolDataSource
{
internal ScrollPool Scroller;
@ -28,7 +28,7 @@ namespace UnityExplorer.UI.Widgets
}
private string currentFilter;
public ButtonListCell(ScrollPool infiniteScroller, Func<List<T>> getEntriesMethod,
public ButtonListSource(ScrollPool infiniteScroller, Func<List<T>> getEntriesMethod,
Action<ButtonCell<T>, int> setICellMethod, Func<T, string, bool> shouldDisplayMethod,
Action<ButtonCell<T>> onCellClickedMethod)
{

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
namespace UnityExplorer.UI.Widgets
{
public class DynamicCell : ICell
{
public DynamicCell(GameObject uiRoot)
{
this.uiRoot = uiRoot;
}
public bool Enabled => m_enabled;
private bool m_enabled;
public GameObject uiRoot;
public InputField input;
public void Disable()
{
m_enabled = false;
uiRoot.SetActive(false);
}
public void Enable()
{
m_enabled = true;
uiRoot.SetActive(true);
}
public static GameObject CreatePrototypeCell(GameObject parent)
{
var prototype = UIFactory.CreateVerticalGroup(parent, "PrototypeCell", true, true, true, true, 0, new Vector4(1,1,1,1),
new Color(0.15f, 0.15f, 0.15f), TextAnchor.MiddleCenter);
var rect = prototype.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(prototype, minWidth: 100, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 9999);
prototype.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
prototype.SetActive(false);
return prototype;
}
}
}

View File

@ -47,7 +47,7 @@ namespace UnityExplorer.UI.Widgets
if (index >= heightCache.Count)
{
while (index > heightCache.Count)
heightCache.Add(defaultCellHeight);
Add(defaultCellHeight);
Add(value);
return;
}
@ -138,7 +138,7 @@ namespace UnityExplorer.UI.Widgets
if (TotalCellHeight.Equals(value))
return;
m_totalCellHeight = value;
SetContentHeight();
//SetContentHeight();
}
}
private float m_totalCellHeight;
@ -153,28 +153,27 @@ namespace UnityExplorer.UI.Widgets
private Vector2 _prevAnchoredPos;
private Vector2 _prevViewportSize; // TODO track viewport height and rebuild on change
#region internal set tracking and update
#region Internal set tracking and update
//private bool _recycling;
public bool ExternallySetting
// A sanity check so only one thing is setting the value per frame.
public bool WritingLocked
{
get => externallySetting;
get => writingLocked;
internal set
{
if (externallySetting == value)
if (writingLocked == value)
return;
timeOfLastExternalSet = Time.time;
externallySetting = value;
timeofLastWriteLock = Time.time;
writingLocked = value;
}
}
private bool externallySetting;
private float timeOfLastExternalSet;
private bool writingLocked;
private float timeofLastWriteLock;
public override void Update()
{
if (externallySetting && timeOfLastExternalSet < Time.time)
externallySetting = false;
if (writingLocked && timeofLastWriteLock < Time.time)
writingLocked = false;
}
#endregion
@ -214,7 +213,7 @@ namespace UnityExplorer.UI.Widgets
BuildInitialHeightCache();
CreateCellPool();
SetContentHeight();
//SetContentHeight();
UpdateSliderPositionAndSize();
@ -241,14 +240,6 @@ namespace UnityExplorer.UI.Widgets
RecycleViewBounds = new Vector2(Viewport.MinY() + extra, Viewport.MaxY() - extra);
}
private void SetContentHeight()
{
var viewRect = scrollRect.viewport;
scrollRect.content.sizeDelta = new Vector2(
scrollRect.content.sizeDelta.x,
AdjustedTotalCellHeight - viewRect.rect.height);
}
// Refresh methods
private struct CellInfo { public int cellIndex, dataIndex; }
@ -275,8 +266,6 @@ namespace UnityExplorer.UI.Widgets
}
}
// TODO this is not quite right, it can move the content, it shouldnt move it
public void RefreshCells(bool andReloadFromDataSource = false)
{
if (!CellPool.Any()) return;
@ -310,7 +299,6 @@ namespace UnityExplorer.UI.Widgets
}
SetRecycleViewBounds();
SetContentHeight();
if (andReloadFromDataSource)
{
@ -318,7 +306,6 @@ namespace UnityExplorer.UI.Widgets
RecycleTopToBottom();
}
SetContentHeight();
UpdateSliderPositionAndSize();
if (jumpToBottom)
@ -335,49 +322,14 @@ namespace UnityExplorer.UI.Widgets
LayoutRebuilder.ForceRebuildLayoutImmediate(Content);
// ExplorerCore.Log("Set cell, real height is " + cachedCell.Rect.rect.height + ", pref height is " + cachedCell.Rect.GetComponent<LayoutElement>().preferredHeight);
cachedCell.Height = cachedCell.Cell.Enabled ? cachedCell.Rect.rect.height : 0f;
DataHeightCache[dataIndex] = cachedCell.Height;
//ExplorerCore.Log("set to cache as " + cachedCell.Height);
}
//private void UpdateDisplayedHeightCache()
//{
// if (!CellPool.Any()) return;
// var enumerator = GetPoolEnumerator();
// while (enumerator.MoveNext())
// {
// var curr = enumerator.Current;
// var cell = CellPool[curr.cellIndex];
// cell.Height = cell.Rect.rect.height;
// DataHeightCache[curr.dataIndex] = cell.Height;
// }
// //int cellIdx = topPoolCellIndex;
// //int dataIndex = topDataIndex;
// //int iterated = 0;
// //while (iterated < CellPool.Count)
// //{
// // var cell = CellPool[cellIdx];
// // cellIdx++;
// // if (cellIdx >= CellPool.Count)
// // cellIdx = 0;
// //
// // cell.Height = cell.Rect.rect.height;
// // DataHeightCache[dataIndex] = cell.Height;
// //
// // dataIndex++;
// // iterated++;
// //}
//}
// Cell pool
private void CreateCellPool()
{
//Reseting Pool
if (CellPool.Any())
{
foreach (var cell in CellPool)
@ -386,10 +338,7 @@ namespace UnityExplorer.UI.Widgets
}
if (!PrototypeCell)
{
ExplorerCore.Log("no prototype cell, cannot initialize");
return;
}
throw new Exception("No prototype cell set, cannot initialize");
//Set the prototype cell active and set cell anchor as top
PrototypeCell.gameObject.SetActive(true);
@ -398,7 +347,6 @@ namespace UnityExplorer.UI.Widgets
float requiredCoverage = scrollRect.viewport.rect.height * ExtraPoolCoverageRatio;
topPoolCellIndex = 0;
//topDataIndex = 0;
bottomPoolIndex = -1;
// create cells until the Pool area is covered.
@ -414,13 +362,14 @@ namespace UnityExplorer.UI.Widgets
CellPool.Add(new CachedCell(this, rect, cell));
rect.SetParent(scrollRect.content, false);
cell.Disable();
currentPoolCoverage += rect.rect.height + this.contentLayout.spacing;
}
bottomDataIndex = bottomPoolIndex;
// after creating pool, set displayed cells.
//posY = 0f;
for (int i = 0; i < CellPool.Count; i++)
{
var cell = CellPool[i];
@ -436,7 +385,7 @@ namespace UnityExplorer.UI.Widgets
private void OnValueChangedListener(Vector2 val)
{
if (ExternallySetting)
if (WritingLocked)
return;
SetRecycleViewBounds();
@ -468,23 +417,20 @@ namespace UnityExplorer.UI.Widgets
}
private bool ShouldRecycleTop => GetCellExtent(CellPool[topPoolCellIndex]) >= RecycleViewBounds.x
//&& CellPool[topMostCellIndex].Rect.position.y >= Viewport.MinY()
&& CellPool[bottomPoolIndex].Rect.position.y > RecycleViewBounds.y;
private bool ShouldRecycleBottom => GetCellExtent(CellPool[bottomPoolIndex]) < RecycleViewBounds.y
//&& CellPool[bottomMostCellIndex].Rect.position.y < Viewport.MaxY()
&& CellPool[topPoolCellIndex].Rect.position.y < RecycleViewBounds.x;
private float GetCellExtent(CachedCell cell) => cell.Rect.MaxY() - contentLayout.spacing;
private float RecycleTopToBottom()
{
ExternallySetting = true;
WritingLocked = true;
float recycledheight = 0f;
while (ShouldRecycleTop && CurrentDataCount < DataSource.ItemCount)
//while (GetCellExtent(CellPool[topMostCellIndex]) > Viewport.MinY() && CurrentDataCount < DataSource.ItemCount)
{
var cell = CellPool[topPoolCellIndex];
@ -500,7 +446,6 @@ namespace UnityExplorer.UI.Widgets
SetCell(cell, CurrentDataCount);
//set new indices
//topDataIndex++;
bottomDataIndex++;
bottomPoolIndex = topPoolCellIndex;
@ -512,12 +457,10 @@ namespace UnityExplorer.UI.Widgets
private float RecycleBottomToTop()
{
ExternallySetting = true;
WritingLocked = true;
float recycledheight = 0f;
// works, except when moving+resizing a cell at the top, that seems to cause issues, need to fix that.
while (ShouldRecycleBottom && CurrentDataCount > CellPool.Count)
{
var cell = CellPool[bottomPoolIndex];
@ -531,7 +474,6 @@ namespace UnityExplorer.UI.Widgets
recycledheight += prevHeight + contentLayout.spacing;
//set new index
//topDataIndex--;
bottomDataIndex--;
//set Cell
@ -542,7 +484,6 @@ namespace UnityExplorer.UI.Widgets
var diff = newHeight - prevHeight;
if (diff != 0.0f)
{
SetContentHeight();
Content.anchoredPosition += Vector2.up * diff;
recycledheight += diff;
}
@ -601,9 +542,9 @@ namespace UnityExplorer.UI.Widgets
private void OnSliderValueChanged(float val)
{
if (this.ExternallySetting)
if (this.WritingLocked)
return;
this.ExternallySetting = true;
this.WritingLocked = true;
// TODO this cant work until we have a cache of all data heights.
// will need to maintain that as we go and assume default height for indeterminate cells.

View File

@ -272,14 +272,15 @@
<Compile Include="UI\Utility\PanelDragger.cs" />
<Compile Include="UI\Utility\SignatureHighlighter.cs" />
<Compile Include="UI\Utility\ToStringUtility.cs" />
<Compile Include="UI\Widgets\ScrollPool\DynamicCell.cs" />
<Compile Include="UI\Widgets\ScrollPool\ICell.cs" />
<Compile Include="UI\Widgets\ScrollPool\IPoolDataSource.cs" />
<Compile Include="UI\Widgets\ScrollPool\ScrollPoolBak.cs" />
<Compile Include="UI\Widgets\ScrollPool\ScrollPool_bak.cs" />
<Compile Include="UI\Widgets\ScrollPool\ScrollPool.cs" />
<Compile Include="UI\Widgets\ScrollPool\UIExtensions.cs" />
<Compile Include="UI\Widgets\InputFieldScroller.cs" />
<Compile Include="UI\Widgets\ButtonList\ButtonCell.cs" />
<Compile Include="UI\Widgets\ButtonList\ButtonListCell.cs" />
<Compile Include="UI\Widgets\ButtonList\ButtonListSource.cs" />
<Compile Include="UI\Widgets\SliderScrollbar.cs" />
<Compile Include="UI\Widgets\TransformTree\CachedTransform.cs" />
<Compile Include="UI\Widgets\TransformTree\TransformCell.cs" />
@ -290,9 +291,7 @@
<None Include="ILRepack.targets" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Folder Include="UI\Widgets\ScrollPool\Dynamic\" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="packages\ILRepack.Lib.MSBuild.Task.2.0.18.1\build\ILRepack.Lib.MSBuild.Task.targets" Condition="Exists('packages\ILRepack.Lib.MSBuild.Task.2.0.18.1\build\ILRepack.Lib.MSBuild.Task.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">