diff --git a/src/UI/Panels/InspectorTest.cs b/src/UI/Panels/InspectorTest.cs index 0741bdd..7599b1b 100644 --- a/src/UI/Panels/InspectorTest.cs +++ b/src/UI/Panels/InspectorTest.cs @@ -86,8 +86,8 @@ namespace UnityExplorer.UI.Panels var test = new DynamicListTest(scrollPool, this); test.Init(); - var prototype = DynamicCell.CreatePrototypeCell(scrollContent); - scrollPool.PrototypeCell = prototype.GetComponent(); + //var prototype = DynamicCell.CreatePrototypeCell(scrollContent); + //scrollPool.PrototypeCell = prototype.GetComponent(); dummyContentHolder = new GameObject("DummyHolder"); dummyContentHolder.SetActive(false); @@ -170,9 +170,10 @@ namespace UnityExplorer.UI.Panels public void Init() { + var prototype = DynamicCell.CreatePrototypeCell(Scroller.UIRoot); Scroller.DataSource = this; - Scroller.Initialize(this); + Scroller.Initialize(this, prototype); } public ICell CreateCell(RectTransform cellTransform) => new DynamicCell(cellTransform.gameObject); diff --git a/src/UI/Panels/SceneExplorer.cs b/src/UI/Panels/SceneExplorer.cs index 152728c..b53476b 100644 --- a/src/UI/Panels/SceneExplorer.cs +++ b/src/UI/Panels/SceneExplorer.cs @@ -223,6 +223,8 @@ namespace UnityExplorer.UI.Panels refreshRow.SetActive(false); // Transform Tree + + //var prototype = TransformCell.CreatePrototypeCell(scrollContent); var infiniteScroll = UIFactory.CreateScrollPool(content, "TransformTree", out GameObject scrollObj, out GameObject scrollContent, new Color(0.15f, 0.15f, 0.15f)); @@ -230,13 +232,10 @@ namespace UnityExplorer.UI.Panels UIFactory.SetLayoutElement(scrollContent, flexibleHeight: 9999); // Prototype tree cell - var prototype = TransformCell.CreatePrototypeCell(scrollContent); - infiniteScroll.PrototypeCell = prototype.GetComponent(); + + //infiniteScroll.PrototypeCell = prototype.GetComponent(); - Tree = new TransformTree(infiniteScroll) - { - GetRootEntriesMethod = GetRootEntries - }; + Tree = new TransformTree(infiniteScroll) { GetRootEntriesMethod = GetRootEntries }; Tree.Init(); // some references diff --git a/src/UI/Widgets/ButtonList/ButtonCell.cs b/src/UI/Widgets/ButtonList/ButtonCell.cs index 205f290..66a9e2a 100644 --- a/src/UI/Widgets/ButtonList/ButtonCell.cs +++ b/src/UI/Widgets/ButtonList/ButtonCell.cs @@ -42,7 +42,7 @@ namespace UnityExplorer.UI.Widgets uiRoot.SetActive(true); } - public static GameObject CreatePrototypeCell(GameObject parent) + public static RectTransform CreatePrototypeCell(GameObject parent) { var prototype = UIFactory.CreateHorizontalGroup(parent, "PrototypeCell", true, true, true, true, 2, default, new Color(0.15f, 0.15f, 0.15f), TextAnchor.MiddleCenter); @@ -68,7 +68,7 @@ namespace UnityExplorer.UI.Widgets prototype.SetActive(false); - return prototype; + return rect; } } } diff --git a/src/UI/Widgets/ButtonList/ButtonListSource.cs b/src/UI/Widgets/ButtonList/ButtonListSource.cs index e0dcca1..cd8a39d 100644 --- a/src/UI/Widgets/ButtonList/ButtonListSource.cs +++ b/src/UI/Widgets/ButtonList/ButtonListSource.cs @@ -49,9 +49,11 @@ namespace UnityExplorer.UI.Widgets { yield return null; + var proto = ButtonCell.CreatePrototypeCell(Scroller.UIRoot); + RefreshData(); Scroller.DataSource = this; - Scroller.Initialize(this); + Scroller.Initialize(this, proto); } public void RefreshData() diff --git a/src/UI/Widgets/ScrollPool/DynamicCell.cs b/src/UI/Widgets/ScrollPool/DynamicCell.cs index 54f9f9b..250bd35 100644 --- a/src/UI/Widgets/ScrollPool/DynamicCell.cs +++ b/src/UI/Widgets/ScrollPool/DynamicCell.cs @@ -12,6 +12,7 @@ namespace UnityExplorer.UI.Widgets public DynamicCell(GameObject uiRoot) { this.uiRoot = uiRoot; + m_enabled = uiRoot.activeSelf; } public bool Enabled => m_enabled; @@ -32,7 +33,7 @@ namespace UnityExplorer.UI.Widgets uiRoot.SetActive(true); } - public static GameObject CreatePrototypeCell(GameObject parent) + public static RectTransform CreatePrototypeCell(GameObject parent) { var prototype = UIFactory.CreateVerticalGroup(parent, "PrototypeCell", true, true, true, true, 0, new Vector4(1, 0, 0, 0), new Color(0.15f, 0.15f, 0.15f), TextAnchor.MiddleCenter); @@ -47,7 +48,7 @@ namespace UnityExplorer.UI.Widgets prototype.SetActive(false); - return prototype; + return rect; } } } diff --git a/src/UI/Widgets/ScrollPool/ScrollPool.cs b/src/UI/Widgets/ScrollPool/ScrollPool.cs index 0f11d87..ca1d4bb 100644 --- a/src/UI/Widgets/ScrollPool/ScrollPool.cs +++ b/src/UI/Widgets/ScrollPool/ScrollPool.cs @@ -9,20 +9,19 @@ using UnityExplorer.UI.Models; namespace UnityExplorer.UI.Widgets { + // TODO: there is possibly still a bug causing the content to jump around, sometimes observed when + // the pooled content height is extremely large (compared to viewport). maybe it depends on the + // top/bottom cells or something. + /// - /// An object-pooled ScrollRect, attempts to support content of any size and provide a scrollbar for it.
- ///
- /// IMPORTANT CAVEATS:
- /// - A cell cannot be smaller than the Prototype cell's default height
- /// - (maybe?) A cell must start at the default height and only increase after being displayed for the first time
- ///
+ /// An object-pooled ScrollRect, attempts to support content of any size and provide a scrollbar for it. public class ScrollPool : UIBehaviourModel { // used to track and manage cell views public class CachedCell { public ScrollPool Pool { get; } - public RectTransform Rect { get; } + public RectTransform Rect { get; internal set; } public ICell Cell { get; } public CachedCell(ScrollPool pool, RectTransform rect, ICell cell) @@ -38,13 +37,15 @@ namespace UnityExplorer.UI.Widgets this.scrollRect = scrollRect; } - public int ExtraPoolCells => 10; - public float ExtraPoolThreshold => PrototypeCell.rect.height * ExtraPoolCells; - public float HalfPoolThreshold => ExtraPoolThreshold * 0.5f; - public IPoolDataSource DataSource; public RectTransform PrototypeCell; + private float PrototypeHeight => PrototypeCell.rect.height; + + public int ExtraPoolCells => 10; + public float ExtraPoolThreshold => PrototypeHeight * ExtraPoolCells; + public float HalfPoolThreshold => ExtraPoolThreshold * 0.5f; + // UI public override GameObject UIRoot => scrollRect.gameObject; @@ -81,7 +82,7 @@ namespace UnityExplorer.UI.Widgets private int CurrentDataCount => bottomDataIndex + 1; private Vector2 _prevAnchoredPos; - private Vector2 _prevViewportSize; // TODO track viewport height and add if height increased + private float _prevViewportHeight; // TODO track viewport height and add if height increased #region Internal set tracking and update @@ -111,11 +112,17 @@ namespace UnityExplorer.UI.Widgets public void Rebuild() { - Initialize(DataSource); + Initialize(DataSource, PrototypeCell); } - public void Initialize(IPoolDataSource dataSource) + public void Initialize(IPoolDataSource dataSource, RectTransform prototypeCell) { + if (!prototypeCell) + throw new Exception("No prototype cell set, cannot initialize"); + + this.PrototypeCell = prototypeCell; + PrototypeCell.transform.SetParent(Viewport, false); + HeightCache = new DataHeightManager(this); DataSource = dataSource; @@ -136,10 +143,9 @@ namespace UnityExplorer.UI.Widgets yield return null; - _prevAnchoredPos = scrollRect.content.anchoredPosition; - _prevViewportSize = new Vector2(scrollRect.viewport.rect.width, scrollRect.viewport.rect.height); + _prevAnchoredPos = Content.anchoredPosition; - SetRecycleViewBounds(); + SetRecycleViewBounds(false); float start = Time.realtimeSinceStartup; CreateCellPool(); @@ -151,11 +157,114 @@ namespace UnityExplorer.UI.Widgets scrollRect.onValueChanged.AddListener(OnValueChangedListener); } - private void SetRecycleViewBounds() + // Cell pool + + private void CreateCellPool() + { + if (CellPool.Any()) + { + foreach (var cell in CellPool) + GameObject.Destroy(cell.Rect.gameObject); + CellPool.Clear(); + } + + float currentPoolCoverage = 0f; + float requiredCoverage = scrollRect.viewport.rect.height + ExtraPoolThreshold;// * ExtraPoolCoverageRatio; + + topPoolCellIndex = 0; + bottomPoolIndex = -1; + + // create cells until the Pool area is covered. + // use minimum default height so that maximum pool count is reached. + while (currentPoolCoverage <= requiredCoverage) + { + bottomPoolIndex++; + + //Instantiate and add to Pool + RectTransform rect = GameObject.Instantiate(PrototypeCell.gameObject).GetComponent(); + rect.gameObject.SetActive(true); + rect.name = $"Cell_{CellPool.Count + 1}"; + var cell = DataSource.CreateCell(rect); + CellPool.Add(new CachedCell(this, rect, cell)); + rect.SetParent(scrollRect.content, false); + + currentPoolCoverage += rect.rect.height; + } + + bottomDataIndex = CellPool.Count - 1; + + // after creating pool, set displayed cells. + for (int i = 0; i < CellPool.Count; i++) + { + var cell = CellPool[i]; + SetCell(cell, i); + } + + LayoutRebuilder.ForceRebuildLayoutImmediate(Content); + } + + private void SetRecycleViewBounds(bool checkHeightGrow) { var extra = ExtraPoolThreshold; extra *= 0.5f; RecycleViewBounds = new Vector2(Viewport.MinY() + extra, Viewport.MaxY() - extra); + + if (checkHeightGrow && _prevViewportHeight < Viewport.rect.height && _prevViewportHeight != 0.0f) + RefillCellPool(); + + _prevViewportHeight = Viewport.rect.height; + } + + private void RefillCellPool() + { + // TODO buggy for some reason, not quite right. + + var requiredCoverage = Math.Abs(RecycleViewBounds.y - RecycleViewBounds.x); + var currentCoverage = CellPool.Count * PrototypeHeight; + if (currentCoverage < requiredCoverage) + { + //int cellsRequired = (int)Math.Ceiling((decimal)(requiredCoverage - currentCoverage) / (decimal)PrototypeHeight); + + while (currentCoverage <= requiredCoverage) + { + //bottomPoolIndex++; + ExplorerCore.Log("Adding to end of pool"); + + //Instantiate and add to Pool + RectTransform rect = GameObject.Instantiate(PrototypeCell.gameObject).GetComponent(); + rect.gameObject.SetActive(true); + rect.name = $"Cell_{CellPool.Count + 1}"; + rect.SetParent(scrollRect.content, false); + + currentCoverage += rect.rect.height; + + bottomDataIndex++; + } + + CellPool.Clear(); + + int childCount = Content.childCount; + for (int i = 0; i < childCount; i++) + { + var rect = Content.GetChild(i).GetComponent(); + + var cell = DataSource.CreateCell(rect); + CellPool.Add(new CachedCell(this, rect, cell)); + + ExplorerCore.Log("Assigned cell rect " + i); + } + + // reassign cell references + topPoolCellIndex = 0; + bottomPoolIndex = CellPool.Count - 1; + + // after creating pool, set displayed cells. + for (int i = 0; i < CellPool.Count; i++) + { + var cell = CellPool[i]; + SetCell(cell, i); + } + } } // Refresh methods @@ -190,7 +299,7 @@ namespace UnityExplorer.UI.Widgets // ExplorerCore.Log("RefreshCells | " + Time.time); - SetRecycleViewBounds(); + SetRecycleViewBounds(true); // jump to bottom if the data count went below our bottom data index bool jumpToBottom = false; @@ -205,7 +314,7 @@ namespace UnityExplorer.UI.Widgets } if (HeightCache.Count < count) - HeightCache.SetIndex(count - 1, PrototypeCell.rect.height); + HeightCache.SetIndex(count - 1, PrototypeHeight); else if (HeightCache.Count > count) { while (HeightCache.Count > count) @@ -243,7 +352,7 @@ namespace UnityExplorer.UI.Widgets } LayoutRebuilder.ForceRebuildLayoutImmediate(Content); - SetRecycleViewBounds(); + //SetRecycleViewBounds(false); NormalizedScrollBounds = new Vector2(Viewport.rect.height * 0.5f, TotalDataHeight - (Viewport.rect.height * 0.5f)); scrollRect.UpdatePrevData(); } @@ -259,64 +368,6 @@ namespace UnityExplorer.UI.Widgets HeightCache.SetIndex(dataIndex, cachedCell.Cell.Enabled ? cachedCell.Rect.rect.height : 0f); } - // Cell pool - - private void CreateCellPool() - { - if (CellPool.Any()) - { - foreach (var cell in CellPool) - GameObject.Destroy(cell.Rect.gameObject); - CellPool.Clear(); - } - - if (!PrototypeCell) - throw new Exception("No prototype cell set, cannot initialize"); - - //Set the prototype cell active and set cell anchor as top - //PrototypeCell.gameObject.SetActive(true); - - float currentPoolCoverage = 0f; - float requiredCoverage = scrollRect.viewport.rect.height + ExtraPoolThreshold;// * ExtraPoolCoverageRatio; - - topPoolCellIndex = 0; - bottomPoolIndex = -1; - - // create cells until the Pool area is covered. - // use minimum default height so that maximum pool count is reached. - while (currentPoolCoverage <= requiredCoverage) - { - bottomPoolIndex++; - - //Instantiate and add to Pool - RectTransform rect = GameObject.Instantiate(PrototypeCell.gameObject).GetComponent(); - rect.gameObject.SetActive(true); - rect.name = $"Cell_{CellPool.Count + 1}"; - var cell = DataSource.CreateCell(rect); - CellPool.Add(new CachedCell(this, rect, cell)); - rect.SetParent(scrollRect.content, false); - - //cell.Disable(); - - currentPoolCoverage += rect.rect.height; - } - - bottomDataIndex = CellPool.Count - 1; - - // after creating pool, set displayed cells. - for (int i = 0; i < CellPool.Count; i++) - { - var cell = CellPool[i]; - SetCell(cell, i); - } - - LayoutRebuilder.ForceRebuildLayoutImmediate(Content); - - //Deactivate prototype cell if it is not a prefab(i.e it's present in scene) - if (PrototypeCell.gameObject.scene.IsValid()) - PrototypeCell.gameObject.SetActive(false); - } - // Value change processor private void OnValueChangedListener(Vector2 val) @@ -326,7 +377,7 @@ namespace UnityExplorer.UI.Widgets //ExplorerCore.Log("ScrollRect.OnValueChanged | " + Time.time + ", val: " + val.y.ToString("F5")); - SetRecycleViewBounds(); + SetRecycleViewBounds(true); RefreshCells(); float yChange = (scrollRect.content.anchoredPosition - _prevAnchoredPos).y; @@ -356,11 +407,11 @@ namespace UnityExplorer.UI.Widgets UpdateSliderHandle(); } - private bool ShouldRecycleTop => GetCellExtent(CellPool[topPoolCellIndex]) >= RecycleViewBounds.x - && CellPool[bottomPoolIndex].Rect.position.y < Viewport.MaxY(); + private bool ShouldRecycleTop => GetCellExtent(CellPool[topPoolCellIndex]) >= RecycleViewBounds.x; + //&& CellPool[bottomPoolIndex].Rect.position.y < Viewport.MaxY(); - private bool ShouldRecycleBottom => CellPool[bottomPoolIndex].Rect.position.y < RecycleViewBounds.y - && GetCellExtent(CellPool[topPoolCellIndex]) < Viewport.MinY(); + private bool ShouldRecycleBottom => CellPool[bottomPoolIndex].Rect.position.y < RecycleViewBounds.y; + //&& GetCellExtent(CellPool[topPoolCellIndex]) < Viewport.MinY(); private float GetCellExtent(CachedCell cell) => cell.Rect.MaxY() - contentLayout.spacing; diff --git a/src/UI/Widgets/TransformTree/TransformCell.cs b/src/UI/Widgets/TransformTree/TransformCell.cs index ef37e31..5baf342 100644 --- a/src/UI/Widgets/TransformTree/TransformCell.cs +++ b/src/UI/Widgets/TransformTree/TransformCell.cs @@ -103,7 +103,7 @@ namespace UnityExplorer.UI.Widgets ExplorerCore.LogWarning("The object was destroyed!"); } - public static GameObject CreatePrototypeCell(GameObject parent) + public static RectTransform CreatePrototypeCell(GameObject parent) { var prototype = UIFactory.CreateHorizontalGroup(parent, "PrototypeCell", true, true, true, true, 2, default, new Color(0.15f, 0.15f, 0.15f), TextAnchor.MiddleCenter); @@ -136,7 +136,7 @@ namespace UnityExplorer.UI.Widgets prototype.SetActive(false); - return prototype; + return rect; } } } diff --git a/src/UI/Widgets/TransformTree/TransformTree.cs b/src/UI/Widgets/TransformTree/TransformTree.cs index 74153ce..992105c 100644 --- a/src/UI/Widgets/TransformTree/TransformTree.cs +++ b/src/UI/Widgets/TransformTree/TransformTree.cs @@ -70,8 +70,10 @@ namespace UnityExplorer.UI.Widgets { yield return null; + var prototype = TransformCell.CreatePrototypeCell(Scroller.UIRoot); + RefreshData(); - Scroller.Initialize(this); + Scroller.Initialize(this, prototype); } public ICell CreateCell(RectTransform cellTransform)