VirtualVerticalLayoutGroup.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. //#define PROFILE
  2. namespace SRF.UI.Layout
  3. {
  4. using System;
  5. using Internal;
  6. using UnityEngine;
  7. using UnityEngine.Events;
  8. using UnityEngine.EventSystems;
  9. using UnityEngine.UI;
  10. public interface IVirtualView
  11. {
  12. void SetDataContext(object data);
  13. }
  14. /// <summary>
  15. /// </summary>
  16. [AddComponentMenu(ComponentMenuPaths.VirtualVerticalLayoutGroup)]
  17. public class VirtualVerticalLayoutGroup : LayoutGroup, IPointerClickHandler
  18. {
  19. private readonly SRList<object> _itemList = new SRList<object>();
  20. private readonly SRList<int> _visibleItemList = new SRList<int>();
  21. private bool _isDirty = false;
  22. private SRList<Row> _rowCache = new SRList<Row>();
  23. private ScrollRect _scrollRect;
  24. private int _selectedIndex;
  25. private object _selectedItem;
  26. [SerializeField] private SelectedItemChangedEvent _selectedItemChanged;
  27. private int _visibleItemCount;
  28. private SRList<Row> _visibleRows = new SRList<Row>();
  29. public StyleSheet AltRowStyleSheet;
  30. public bool EnableSelection = true;
  31. public RectTransform ItemPrefab;
  32. /// <summary>
  33. /// Rows to show above and below the visible rect to reduce pop-in
  34. /// </summary>
  35. public int RowPadding = 2;
  36. public StyleSheet RowStyleSheet;
  37. public StyleSheet SelectedRowStyleSheet;
  38. /// <summary>
  39. /// Spacing to add between rows
  40. /// </summary>
  41. public float Spacing;
  42. /// <summary>
  43. /// If true, the scroll view will stick to the last element when fully scrolled to the bottom and an item is added
  44. /// </summary>
  45. public bool StickToBottom = true;
  46. public SelectedItemChangedEvent SelectedItemChanged
  47. {
  48. get { return _selectedItemChanged; }
  49. set { _selectedItemChanged = value; }
  50. }
  51. public object SelectedItem
  52. {
  53. get { return _selectedItem; }
  54. set
  55. {
  56. if (_selectedItem == value || !EnableSelection)
  57. {
  58. return;
  59. }
  60. var newSelectedIndex = value == null ? -1 : _itemList.IndexOf(value);
  61. // Ensure that the new selected item is present in the item list
  62. if (value != null && newSelectedIndex < 0)
  63. {
  64. throw new InvalidOperationException("Cannot select item not present in layout");
  65. }
  66. // Invalidate old selected item row
  67. if (_selectedItem != null)
  68. {
  69. InvalidateItem(_selectedIndex);
  70. }
  71. _selectedItem = value;
  72. _selectedIndex = newSelectedIndex;
  73. // Invalidate the newly selected item
  74. if (_selectedItem != null)
  75. {
  76. InvalidateItem(_selectedIndex);
  77. }
  78. SetDirty();
  79. if (_selectedItemChanged != null)
  80. {
  81. _selectedItemChanged.Invoke(_selectedItem);
  82. }
  83. }
  84. }
  85. public override float minHeight
  86. {
  87. get { return _itemList.Count*ItemHeight + padding.top + padding.bottom + Spacing*_itemList.Count; }
  88. }
  89. public void OnPointerClick(PointerEventData eventData)
  90. {
  91. if (!EnableSelection)
  92. {
  93. return;
  94. }
  95. var hitObject = eventData.pointerPressRaycast.gameObject;
  96. if (hitObject == null)
  97. {
  98. return;
  99. }
  100. var hitPos = hitObject.transform.position;
  101. var localPos = rectTransform.InverseTransformPoint(hitPos);
  102. var row = Mathf.FloorToInt(Mathf.Abs(localPos.y)/ItemHeight);
  103. if (row >= 0 && row < _itemList.Count)
  104. {
  105. SelectedItem = _itemList[row];
  106. }
  107. else
  108. {
  109. SelectedItem = null;
  110. }
  111. }
  112. protected override void Awake()
  113. {
  114. base.Awake();
  115. ScrollRect.onValueChanged.AddListener(OnScrollRectValueChanged);
  116. var view = ItemPrefab.GetComponent(typeof (IVirtualView));
  117. if (view == null)
  118. {
  119. Debug.LogWarning(
  120. "[VirtualVerticalLayoutGroup] ItemPrefab does not have a component inheriting from IVirtualView, so no data binding can occur");
  121. }
  122. }
  123. private void OnScrollRectValueChanged(Vector2 d)
  124. {
  125. if (d.y < 0 || d.y > 1)
  126. {
  127. _scrollRect.verticalNormalizedPosition = Mathf.Clamp01(d.y);
  128. }
  129. //CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
  130. SetDirty();
  131. }
  132. protected override void Start()
  133. {
  134. base.Start();
  135. ScrollUpdate();
  136. }
  137. protected override void OnEnable()
  138. {
  139. base.OnEnable();
  140. SetDirty();
  141. }
  142. protected void Update()
  143. {
  144. if (!AlignBottom && !AlignTop)
  145. {
  146. Debug.LogWarning("[VirtualVerticalLayoutGroup] Only Lower or Upper alignment is supported.", this);
  147. childAlignment = TextAnchor.UpperLeft;
  148. }
  149. if (SelectedItem != null && !_itemList.Contains(SelectedItem))
  150. {
  151. SelectedItem = null;
  152. }
  153. if (_isDirty)
  154. {
  155. _isDirty = false;
  156. ScrollUpdate();
  157. }
  158. }
  159. /// <summary>
  160. /// Invalidate a single row (before removing, or changing selection status)
  161. /// </summary>
  162. /// <param name="itemIndex"></param>
  163. protected void InvalidateItem(int itemIndex)
  164. {
  165. if (!_visibleItemList.Contains(itemIndex))
  166. {
  167. return;
  168. }
  169. _visibleItemList.Remove(itemIndex);
  170. for (var i = 0; i < _visibleRows.Count; i++)
  171. {
  172. if (_visibleRows[i].Index == itemIndex)
  173. {
  174. RecycleRow(_visibleRows[i]);
  175. _visibleRows.RemoveAt(i);
  176. break;
  177. }
  178. }
  179. }
  180. /// <summary>
  181. /// After removing or inserting a row, ensure that the cached indexes (used for layout) match up
  182. /// with the item index in the list
  183. /// </summary>
  184. protected void RefreshIndexCache()
  185. {
  186. for (var i = 0; i < _visibleRows.Count; i++)
  187. {
  188. _visibleRows[i].Index = _itemList.IndexOf(_visibleRows[i].Data);
  189. }
  190. }
  191. protected void ScrollUpdate()
  192. {
  193. if (!Application.isPlaying)
  194. {
  195. return;
  196. }
  197. //Debug.Log("[SRConsole] ScrollUpdate {0}".Fmt(Time.frameCount));
  198. var pos = rectTransform.anchoredPosition;
  199. var startY = pos.y;
  200. var viewHeight = ((RectTransform) ScrollRect.transform).rect.height;
  201. // Determine the range of rows that should be visible
  202. var rowRangeLower = Mathf.FloorToInt(startY/(ItemHeight + Spacing));
  203. var rowRangeHigher = Mathf.CeilToInt((startY + viewHeight)/(ItemHeight + Spacing));
  204. // Apply padding to reduce pop-in
  205. rowRangeLower -= RowPadding;
  206. rowRangeHigher += RowPadding;
  207. rowRangeLower = Mathf.Max(0, rowRangeLower);
  208. rowRangeHigher = Mathf.Min(_itemList.Count, rowRangeHigher);
  209. var isDirty = false;
  210. #if PROFILE
  211. Profiler.BeginSample("Visible Rows Cull");
  212. #endif
  213. for (var i = 0; i < _visibleRows.Count; i++)
  214. {
  215. var row = _visibleRows[i];
  216. // Move on if row is still visible
  217. if (row.Index >= rowRangeLower && row.Index <= rowRangeHigher)
  218. {
  219. continue;
  220. }
  221. _visibleItemList.Remove(row.Index);
  222. _visibleRows.Remove(row);
  223. RecycleRow(row);
  224. isDirty = true;
  225. }
  226. #if PROFILE
  227. Profiler.EndSample();
  228. Profiler.BeginSample("Item Visible Check");
  229. #endif
  230. for (var i = rowRangeLower; i < rowRangeHigher; ++i)
  231. {
  232. if (i >= _itemList.Count)
  233. {
  234. break;
  235. }
  236. // Move on if row is already visible
  237. if (_visibleItemList.Contains(i))
  238. {
  239. continue;
  240. }
  241. var row = GetRow(i);
  242. _visibleRows.Add(row);
  243. _visibleItemList.Add(i);
  244. isDirty = true;
  245. }
  246. #if PROFILE
  247. Profiler.EndSample();
  248. #endif
  249. // If something visible has explicitly been changed, or the visible row count has changed
  250. if (isDirty || _visibleItemCount != _visibleRows.Count)
  251. {
  252. //Debug.Log("[SRConsole] IsDirty {0}".Fmt(Time.frameCount));
  253. LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
  254. }
  255. _visibleItemCount = _visibleRows.Count;
  256. }
  257. public override void CalculateLayoutInputVertical()
  258. {
  259. SetLayoutInputForAxis(minHeight, minHeight, -1, 1);
  260. }
  261. public override void SetLayoutHorizontal()
  262. {
  263. var width = rectTransform.rect.width - padding.left - padding.right;
  264. // Position visible rows at 0 x
  265. for (var i = 0; i < _visibleRows.Count; i++)
  266. {
  267. var item = _visibleRows[i];
  268. SetChildAlongAxis(item.Rect, 0, padding.left, width);
  269. }
  270. // Hide non-active rows to one side. More efficient than enabling/disabling them
  271. for (var i = 0; i < _rowCache.Count; i++)
  272. {
  273. var item = _rowCache[i];
  274. SetChildAlongAxis(item.Rect, 0, -width - padding.left, width);
  275. }
  276. }
  277. public override void SetLayoutVertical()
  278. {
  279. if (!Application.isPlaying)
  280. {
  281. return;
  282. }
  283. //Debug.Log("[SRConsole] SetLayoutVertical {0}".Fmt(Time.frameCount));
  284. // Position visible rows by the index of the item they represent
  285. for (var i = 0; i < _visibleRows.Count; i++)
  286. {
  287. var item = _visibleRows[i];
  288. SetChildAlongAxis(item.Rect, 1, item.Index*ItemHeight + padding.top + Spacing*item.Index, ItemHeight);
  289. }
  290. }
  291. private new void SetDirty()
  292. {
  293. base.SetDirty();
  294. if (!IsActive())
  295. {
  296. return;
  297. }
  298. _isDirty = true;
  299. //CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
  300. }
  301. [Serializable]
  302. public class SelectedItemChangedEvent : UnityEvent<object> {}
  303. [Serializable]
  304. private class Row
  305. {
  306. public object Data;
  307. public int Index;
  308. public RectTransform Rect;
  309. public StyleRoot Root;
  310. public IVirtualView View;
  311. }
  312. #region Public Data Methods
  313. public void AddItem(object item)
  314. {
  315. _itemList.Add(item);
  316. SetDirty();
  317. if (StickToBottom && Mathf.Approximately(ScrollRect.verticalNormalizedPosition, 0f))
  318. {
  319. ScrollRect.normalizedPosition = new Vector2(0, 0);
  320. }
  321. }
  322. public void RemoveItem(object item)
  323. {
  324. if (SelectedItem == item)
  325. {
  326. SelectedItem = null;
  327. }
  328. var index = _itemList.IndexOf(item);
  329. InvalidateItem(index);
  330. _itemList.Remove(item);
  331. RefreshIndexCache();
  332. SetDirty();
  333. }
  334. public void ClearItems()
  335. {
  336. for (var i = _visibleRows.Count - 1; i >= 0; i--)
  337. {
  338. InvalidateItem(_visibleRows[i].Index);
  339. }
  340. _itemList.Clear();
  341. SetDirty();
  342. }
  343. #endregion
  344. #region Internal Properties
  345. private ScrollRect ScrollRect
  346. {
  347. get
  348. {
  349. if (_scrollRect == null)
  350. {
  351. _scrollRect = GetComponentInParent<ScrollRect>();
  352. }
  353. return _scrollRect;
  354. }
  355. }
  356. private bool AlignBottom
  357. {
  358. get
  359. {
  360. return childAlignment == TextAnchor.LowerRight || childAlignment == TextAnchor.LowerCenter ||
  361. childAlignment == TextAnchor.LowerLeft;
  362. }
  363. }
  364. private bool AlignTop
  365. {
  366. get
  367. {
  368. return childAlignment == TextAnchor.UpperLeft || childAlignment == TextAnchor.UpperCenter ||
  369. childAlignment == TextAnchor.UpperRight;
  370. }
  371. }
  372. private float _itemHeight = -1;
  373. private float ItemHeight
  374. {
  375. get
  376. {
  377. if (_itemHeight <= 0)
  378. {
  379. var layoutElement = ItemPrefab.GetComponent(typeof (ILayoutElement)) as ILayoutElement;
  380. if (layoutElement != null)
  381. {
  382. _itemHeight = layoutElement.preferredHeight;
  383. }
  384. else
  385. {
  386. _itemHeight = ItemPrefab.rect.height;
  387. }
  388. if (_itemHeight.ApproxZero())
  389. {
  390. Debug.LogWarning(
  391. "[VirtualVerticalLayoutGroup] ItemPrefab must have a preferred size greater than 0");
  392. _itemHeight = 10;
  393. }
  394. }
  395. return _itemHeight;
  396. }
  397. }
  398. #endregion
  399. #region Row Pooling and Provisioning
  400. private Row GetRow(int forIndex)
  401. {
  402. // If there are no rows available in the cache, create one from scratch
  403. if (_rowCache.Count == 0)
  404. {
  405. var newRow = CreateRow();
  406. PopulateRow(forIndex, newRow);
  407. return newRow;
  408. }
  409. var data = _itemList[forIndex];
  410. Row row = null;
  411. Row altRow = null;
  412. // Determine if the row we're looking for is an alt row
  413. var target = forIndex%2;
  414. // Try and find a row which previously had this data, so we can reuse it
  415. for (var i = 0; i < _rowCache.Count; i++)
  416. {
  417. row = _rowCache[i];
  418. // If this row previously represented this data, just use that one.
  419. if (row.Data == data)
  420. {
  421. _rowCache.RemoveAt(i);
  422. PopulateRow(forIndex, row);
  423. break;
  424. }
  425. // Cache a row which is was the same alt state as the row we're looking for, in case
  426. // we don't find an exact match.
  427. if (row.Index%2 == target)
  428. {
  429. altRow = row;
  430. }
  431. // Didn't match, reset to null
  432. row = null;
  433. }
  434. // If an exact match wasn't found, but a row with the same alt-status was found, use that one.
  435. if (row == null && altRow != null)
  436. {
  437. _rowCache.Remove(altRow);
  438. row = altRow;
  439. PopulateRow(forIndex, row);
  440. }
  441. else if (row == null)
  442. {
  443. // No match found, use the last added item in the cache
  444. row = _rowCache.PopLast();
  445. PopulateRow(forIndex, row);
  446. }
  447. return row;
  448. }
  449. private void RecycleRow(Row row)
  450. {
  451. _rowCache.Add(row);
  452. }
  453. private void PopulateRow(int index, Row row)
  454. {
  455. row.Index = index;
  456. // Set data context on row
  457. row.Data = _itemList[index];
  458. row.View.SetDataContext(_itemList[index]);
  459. // If we're using stylesheets
  460. if (RowStyleSheet != null || AltRowStyleSheet != null || SelectedRowStyleSheet != null)
  461. {
  462. // If there is a selected row stylesheet, and this is the selected row, use that one
  463. if (SelectedRowStyleSheet != null && SelectedItem == row.Data)
  464. {
  465. row.Root.StyleSheet = SelectedRowStyleSheet;
  466. }
  467. else
  468. {
  469. // Otherwise just use the stylesheet suitable for the row alt-status
  470. row.Root.StyleSheet = index%2 == 0 ? RowStyleSheet : AltRowStyleSheet;
  471. }
  472. }
  473. }
  474. private Row CreateRow()
  475. {
  476. var item = new Row();
  477. var row = SRInstantiate.Instantiate(ItemPrefab);
  478. item.Rect = row;
  479. item.View = row.GetComponent(typeof (IVirtualView)) as IVirtualView;
  480. if (RowStyleSheet != null || AltRowStyleSheet != null || SelectedRowStyleSheet != null)
  481. {
  482. item.Root = row.gameObject.GetComponentOrAdd<StyleRoot>();
  483. item.Root.StyleSheet = RowStyleSheet;
  484. }
  485. row.SetParent(rectTransform, false);
  486. return item;
  487. }
  488. #endregion
  489. }
  490. }