| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607 |
- //#define PROFILE
- namespace SRF.UI.Layout
- {
- using System;
- using Internal;
- using UnityEngine;
- using UnityEngine.Events;
- using UnityEngine.EventSystems;
- using UnityEngine.UI;
- public interface IVirtualView
- {
- void SetDataContext(object data);
- }
- /// <summary>
- /// </summary>
- [AddComponentMenu(ComponentMenuPaths.VirtualVerticalLayoutGroup)]
- public class VirtualVerticalLayoutGroup : LayoutGroup, IPointerClickHandler
- {
- private readonly SRList<object> _itemList = new SRList<object>();
- private readonly SRList<int> _visibleItemList = new SRList<int>();
- private bool _isDirty = false;
- private SRList<Row> _rowCache = new SRList<Row>();
- private ScrollRect _scrollRect;
- private int _selectedIndex;
- private object _selectedItem;
- [SerializeField] private SelectedItemChangedEvent _selectedItemChanged;
- private int _visibleItemCount;
- private SRList<Row> _visibleRows = new SRList<Row>();
- public StyleSheet AltRowStyleSheet;
- public bool EnableSelection = true;
- public RectTransform ItemPrefab;
- /// <summary>
- /// Rows to show above and below the visible rect to reduce pop-in
- /// </summary>
- public int RowPadding = 2;
- public StyleSheet RowStyleSheet;
- public StyleSheet SelectedRowStyleSheet;
- /// <summary>
- /// Spacing to add between rows
- /// </summary>
- public float Spacing;
- /// <summary>
- /// If true, the scroll view will stick to the last element when fully scrolled to the bottom and an item is added
- /// </summary>
- public bool StickToBottom = true;
- public SelectedItemChangedEvent SelectedItemChanged
- {
- get { return _selectedItemChanged; }
- set { _selectedItemChanged = value; }
- }
- public object SelectedItem
- {
- get { return _selectedItem; }
- set
- {
- if (_selectedItem == value || !EnableSelection)
- {
- return;
- }
- var newSelectedIndex = value == null ? -1 : _itemList.IndexOf(value);
- // Ensure that the new selected item is present in the item list
- if (value != null && newSelectedIndex < 0)
- {
- throw new InvalidOperationException("Cannot select item not present in layout");
- }
- // Invalidate old selected item row
- if (_selectedItem != null)
- {
- InvalidateItem(_selectedIndex);
- }
- _selectedItem = value;
- _selectedIndex = newSelectedIndex;
- // Invalidate the newly selected item
- if (_selectedItem != null)
- {
- InvalidateItem(_selectedIndex);
- }
- SetDirty();
- if (_selectedItemChanged != null)
- {
- _selectedItemChanged.Invoke(_selectedItem);
- }
- }
- }
- public override float minHeight
- {
- get { return _itemList.Count*ItemHeight + padding.top + padding.bottom + Spacing*_itemList.Count; }
- }
- public void OnPointerClick(PointerEventData eventData)
- {
- if (!EnableSelection)
- {
- return;
- }
- var hitObject = eventData.pointerPressRaycast.gameObject;
- if (hitObject == null)
- {
- return;
- }
- var hitPos = hitObject.transform.position;
- var localPos = rectTransform.InverseTransformPoint(hitPos);
- var row = Mathf.FloorToInt(Mathf.Abs(localPos.y)/ItemHeight);
- if (row >= 0 && row < _itemList.Count)
- {
- SelectedItem = _itemList[row];
- }
- else
- {
- SelectedItem = null;
- }
- }
- protected override void Awake()
- {
- base.Awake();
- ScrollRect.onValueChanged.AddListener(OnScrollRectValueChanged);
- var view = ItemPrefab.GetComponent(typeof (IVirtualView));
- if (view == null)
- {
- Debug.LogWarning(
- "[VirtualVerticalLayoutGroup] ItemPrefab does not have a component inheriting from IVirtualView, so no data binding can occur");
- }
- }
- private void OnScrollRectValueChanged(Vector2 d)
- {
- if (d.y < 0 || d.y > 1)
- {
- _scrollRect.verticalNormalizedPosition = Mathf.Clamp01(d.y);
- }
- //CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
- SetDirty();
- }
- protected override void Start()
- {
- base.Start();
- ScrollUpdate();
- }
- protected override void OnEnable()
- {
- base.OnEnable();
- SetDirty();
- }
- protected void Update()
- {
- if (!AlignBottom && !AlignTop)
- {
- Debug.LogWarning("[VirtualVerticalLayoutGroup] Only Lower or Upper alignment is supported.", this);
- childAlignment = TextAnchor.UpperLeft;
- }
- if (SelectedItem != null && !_itemList.Contains(SelectedItem))
- {
- SelectedItem = null;
- }
- if (_isDirty)
- {
- _isDirty = false;
- ScrollUpdate();
- }
- }
- /// <summary>
- /// Invalidate a single row (before removing, or changing selection status)
- /// </summary>
- /// <param name="itemIndex"></param>
- protected void InvalidateItem(int itemIndex)
- {
- if (!_visibleItemList.Contains(itemIndex))
- {
- return;
- }
- _visibleItemList.Remove(itemIndex);
- for (var i = 0; i < _visibleRows.Count; i++)
- {
- if (_visibleRows[i].Index == itemIndex)
- {
- RecycleRow(_visibleRows[i]);
- _visibleRows.RemoveAt(i);
- break;
- }
- }
- }
- /// <summary>
- /// After removing or inserting a row, ensure that the cached indexes (used for layout) match up
- /// with the item index in the list
- /// </summary>
- protected void RefreshIndexCache()
- {
- for (var i = 0; i < _visibleRows.Count; i++)
- {
- _visibleRows[i].Index = _itemList.IndexOf(_visibleRows[i].Data);
- }
- }
- protected void ScrollUpdate()
- {
- if (!Application.isPlaying)
- {
- return;
- }
- //Debug.Log("[SRConsole] ScrollUpdate {0}".Fmt(Time.frameCount));
- var pos = rectTransform.anchoredPosition;
- var startY = pos.y;
- var viewHeight = ((RectTransform) ScrollRect.transform).rect.height;
- // Determine the range of rows that should be visible
- var rowRangeLower = Mathf.FloorToInt(startY/(ItemHeight + Spacing));
- var rowRangeHigher = Mathf.CeilToInt((startY + viewHeight)/(ItemHeight + Spacing));
- // Apply padding to reduce pop-in
- rowRangeLower -= RowPadding;
- rowRangeHigher += RowPadding;
- rowRangeLower = Mathf.Max(0, rowRangeLower);
- rowRangeHigher = Mathf.Min(_itemList.Count, rowRangeHigher);
- var isDirty = false;
- #if PROFILE
- Profiler.BeginSample("Visible Rows Cull");
- #endif
- for (var i = 0; i < _visibleRows.Count; i++)
- {
- var row = _visibleRows[i];
- // Move on if row is still visible
- if (row.Index >= rowRangeLower && row.Index <= rowRangeHigher)
- {
- continue;
- }
- _visibleItemList.Remove(row.Index);
- _visibleRows.Remove(row);
- RecycleRow(row);
- isDirty = true;
- }
- #if PROFILE
- Profiler.EndSample();
- Profiler.BeginSample("Item Visible Check");
- #endif
- for (var i = rowRangeLower; i < rowRangeHigher; ++i)
- {
- if (i >= _itemList.Count)
- {
- break;
- }
- // Move on if row is already visible
- if (_visibleItemList.Contains(i))
- {
- continue;
- }
- var row = GetRow(i);
- _visibleRows.Add(row);
- _visibleItemList.Add(i);
- isDirty = true;
- }
- #if PROFILE
- Profiler.EndSample();
- #endif
- // If something visible has explicitly been changed, or the visible row count has changed
- if (isDirty || _visibleItemCount != _visibleRows.Count)
- {
- //Debug.Log("[SRConsole] IsDirty {0}".Fmt(Time.frameCount));
- LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
- }
- _visibleItemCount = _visibleRows.Count;
- }
- public override void CalculateLayoutInputVertical()
- {
- SetLayoutInputForAxis(minHeight, minHeight, -1, 1);
- }
- public override void SetLayoutHorizontal()
- {
- var width = rectTransform.rect.width - padding.left - padding.right;
- // Position visible rows at 0 x
- for (var i = 0; i < _visibleRows.Count; i++)
- {
- var item = _visibleRows[i];
- SetChildAlongAxis(item.Rect, 0, padding.left, width);
- }
- // Hide non-active rows to one side. More efficient than enabling/disabling them
- for (var i = 0; i < _rowCache.Count; i++)
- {
- var item = _rowCache[i];
- SetChildAlongAxis(item.Rect, 0, -width - padding.left, width);
- }
- }
- public override void SetLayoutVertical()
- {
- if (!Application.isPlaying)
- {
- return;
- }
- //Debug.Log("[SRConsole] SetLayoutVertical {0}".Fmt(Time.frameCount));
- // Position visible rows by the index of the item they represent
- for (var i = 0; i < _visibleRows.Count; i++)
- {
- var item = _visibleRows[i];
- SetChildAlongAxis(item.Rect, 1, item.Index*ItemHeight + padding.top + Spacing*item.Index, ItemHeight);
- }
- }
- private new void SetDirty()
- {
- base.SetDirty();
- if (!IsActive())
- {
- return;
- }
- _isDirty = true;
- //CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
- }
- [Serializable]
- public class SelectedItemChangedEvent : UnityEvent<object> {}
- [Serializable]
- private class Row
- {
- public object Data;
- public int Index;
- public RectTransform Rect;
- public StyleRoot Root;
- public IVirtualView View;
- }
- #region Public Data Methods
- public void AddItem(object item)
- {
- _itemList.Add(item);
- SetDirty();
- if (StickToBottom && Mathf.Approximately(ScrollRect.verticalNormalizedPosition, 0f))
- {
- ScrollRect.normalizedPosition = new Vector2(0, 0);
- }
- }
- public void RemoveItem(object item)
- {
- if (SelectedItem == item)
- {
- SelectedItem = null;
- }
- var index = _itemList.IndexOf(item);
- InvalidateItem(index);
- _itemList.Remove(item);
- RefreshIndexCache();
- SetDirty();
- }
- public void ClearItems()
- {
- for (var i = _visibleRows.Count - 1; i >= 0; i--)
- {
- InvalidateItem(_visibleRows[i].Index);
- }
- _itemList.Clear();
- SetDirty();
- }
- #endregion
- #region Internal Properties
- private ScrollRect ScrollRect
- {
- get
- {
- if (_scrollRect == null)
- {
- _scrollRect = GetComponentInParent<ScrollRect>();
- }
- return _scrollRect;
- }
- }
- private bool AlignBottom
- {
- get
- {
- return childAlignment == TextAnchor.LowerRight || childAlignment == TextAnchor.LowerCenter ||
- childAlignment == TextAnchor.LowerLeft;
- }
- }
- private bool AlignTop
- {
- get
- {
- return childAlignment == TextAnchor.UpperLeft || childAlignment == TextAnchor.UpperCenter ||
- childAlignment == TextAnchor.UpperRight;
- }
- }
- private float _itemHeight = -1;
- private float ItemHeight
- {
- get
- {
- if (_itemHeight <= 0)
- {
- var layoutElement = ItemPrefab.GetComponent(typeof (ILayoutElement)) as ILayoutElement;
- if (layoutElement != null)
- {
- _itemHeight = layoutElement.preferredHeight;
- }
- else
- {
- _itemHeight = ItemPrefab.rect.height;
- }
- if (_itemHeight.ApproxZero())
- {
- Debug.LogWarning(
- "[VirtualVerticalLayoutGroup] ItemPrefab must have a preferred size greater than 0");
- _itemHeight = 10;
- }
- }
- return _itemHeight;
- }
- }
- #endregion
- #region Row Pooling and Provisioning
- private Row GetRow(int forIndex)
- {
- // If there are no rows available in the cache, create one from scratch
- if (_rowCache.Count == 0)
- {
- var newRow = CreateRow();
- PopulateRow(forIndex, newRow);
- return newRow;
- }
- var data = _itemList[forIndex];
- Row row = null;
- Row altRow = null;
- // Determine if the row we're looking for is an alt row
- var target = forIndex%2;
- // Try and find a row which previously had this data, so we can reuse it
- for (var i = 0; i < _rowCache.Count; i++)
- {
- row = _rowCache[i];
- // If this row previously represented this data, just use that one.
- if (row.Data == data)
- {
- _rowCache.RemoveAt(i);
- PopulateRow(forIndex, row);
- break;
- }
- // Cache a row which is was the same alt state as the row we're looking for, in case
- // we don't find an exact match.
- if (row.Index%2 == target)
- {
- altRow = row;
- }
- // Didn't match, reset to null
- row = null;
- }
- // If an exact match wasn't found, but a row with the same alt-status was found, use that one.
- if (row == null && altRow != null)
- {
- _rowCache.Remove(altRow);
- row = altRow;
- PopulateRow(forIndex, row);
- }
- else if (row == null)
- {
- // No match found, use the last added item in the cache
- row = _rowCache.PopLast();
- PopulateRow(forIndex, row);
- }
- return row;
- }
- private void RecycleRow(Row row)
- {
- _rowCache.Add(row);
- }
- private void PopulateRow(int index, Row row)
- {
- row.Index = index;
- // Set data context on row
- row.Data = _itemList[index];
- row.View.SetDataContext(_itemList[index]);
- // If we're using stylesheets
- if (RowStyleSheet != null || AltRowStyleSheet != null || SelectedRowStyleSheet != null)
- {
- // If there is a selected row stylesheet, and this is the selected row, use that one
- if (SelectedRowStyleSheet != null && SelectedItem == row.Data)
- {
- row.Root.StyleSheet = SelectedRowStyleSheet;
- }
- else
- {
- // Otherwise just use the stylesheet suitable for the row alt-status
- row.Root.StyleSheet = index%2 == 0 ? RowStyleSheet : AltRowStyleSheet;
- }
- }
- }
- private Row CreateRow()
- {
- var item = new Row();
- var row = SRInstantiate.Instantiate(ItemPrefab);
- item.Rect = row;
- item.View = row.GetComponent(typeof (IVirtualView)) as IVirtualView;
- if (RowStyleSheet != null || AltRowStyleSheet != null || SelectedRowStyleSheet != null)
- {
- item.Root = row.gameObject.GetComponentOrAdd<StyleRoot>();
- item.Root.StyleSheet = RowStyleSheet;
- }
- row.SetParent(rectTransform, false);
- return item;
- }
- #endregion
- }
- }
|