608 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C#
		
	
	
	
			
		
		
	
	
			608 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C#
		
	
	
	
| //#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
 | |
|     }
 | |
| }
 |