FancyScrollView.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /*
  2. * FancyScrollView (https://github.com/setchi/FancyScrollView)
  3. * Copyright (c) 2020 setchi
  4. * Licensed under MIT (https://github.com/setchi/FancyScrollView/blob/master/LICENSE)
  5. */
  6. using System.Collections.Generic;
  7. using UnityEngine;
  8. namespace FancyScrollView
  9. {
  10. /// <summary>
  11. /// スクロールビューを実装するための抽象基底クラス.
  12. /// 無限スクロールおよびスナップに対応しています.
  13. /// <see cref="FancyScrollView{TItemData, TContext}.Context"/> が不要な場合は
  14. /// 代わりに <see cref="FancyScrollView{TItemData}"/> を使用します.
  15. /// </summary>
  16. /// <typeparam name="TItemData">アイテムのデータ型.</typeparam>
  17. /// <typeparam name="TContext"><see cref="Context"/> の型.</typeparam>
  18. public abstract class FancyScrollView<TItemData, TContext> : MonoBehaviour where TContext : class, new()
  19. {
  20. /// <summary>
  21. /// セル同士の間隔.
  22. /// </summary>
  23. [SerializeField, Range(1e-2f, 1f)] protected float cellInterval = 0.2f;
  24. /// <summary>
  25. /// スクロール位置の基準.
  26. /// </summary>
  27. /// <remarks>
  28. /// たとえば、 <c>0.5</c> を指定してスクロール位置が <c>0</c> の場合, 中央に最初のセルが配置されます.
  29. /// </remarks>
  30. [SerializeField, Range(0f, 1f)] protected float scrollOffset = 0.5f;
  31. /// <summary>
  32. /// セルを循環して配置させるどうか.
  33. /// </summary>
  34. /// <remarks>
  35. /// <c>true</c> にすると最後のセルの後に最初のセル, 最初のセルの前に最後のセルが並ぶようになります.
  36. /// 無限スクロールを実装する場合は <c>true</c> を指定します.
  37. /// </remarks>
  38. [SerializeField] protected bool loop = false;
  39. /// <summary>
  40. /// セルの親要素となる <c>Transform</c>.
  41. /// </summary>
  42. [SerializeField] protected Transform cellContainer = default;
  43. readonly IList<FancyCell<TItemData, TContext>> pool = new List<FancyCell<TItemData, TContext>>();
  44. /// <summary>
  45. /// 初期化済みかどうか.
  46. /// </summary>
  47. protected bool initialized;
  48. /// <summary>
  49. /// 現在のスクロール位置.
  50. /// </summary>
  51. protected float currentPosition;
  52. /// <summary>
  53. /// セルの Prefab.
  54. /// </summary>
  55. protected abstract GameObject CellPrefab { get; }
  56. /// <summary>
  57. /// アイテム一覧のデータ.
  58. /// </summary>
  59. protected IList<TItemData> ItemsSource { get; set; } = new List<TItemData>();
  60. /// <summary>
  61. /// <typeparamref name="TContext"/> のインスタンス.
  62. /// セルとスクロールビュー間で同じインスタンスが共有されます. 情報の受け渡しや状態の保持に使用します.
  63. /// </summary>
  64. protected TContext Context { get; } = new TContext();
  65. /// <summary>
  66. /// 初期化を行います.
  67. /// </summary>
  68. /// <remarks>
  69. /// 最初にセルが生成される直前に呼び出されます.
  70. /// </remarks>
  71. protected virtual void Initialize() { }
  72. /// <summary>
  73. /// 渡されたアイテム一覧に基づいて表示内容を更新します.
  74. /// </summary>
  75. /// <param name="itemsSource">アイテム一覧.</param>
  76. protected virtual void UpdateContents(IList<TItemData> itemsSource)
  77. {
  78. ItemsSource = itemsSource;
  79. Refresh();
  80. }
  81. /// <summary>
  82. /// セルのレイアウトを強制的に更新します.
  83. /// </summary>
  84. protected virtual void Relayout() => UpdatePosition(currentPosition, false);
  85. /// <summary>
  86. /// セルのレイアウトと表示内容を強制的に更新します.
  87. /// </summary>
  88. protected virtual void Refresh() => UpdatePosition(currentPosition, true);
  89. /// <summary>
  90. /// スクロール位置を更新します.
  91. /// </summary>
  92. /// <param name="position">スクロール位置.</param>
  93. protected virtual void UpdatePosition(float position) => UpdatePosition(position, false);
  94. void UpdatePosition(float position, bool forceRefresh)
  95. {
  96. if (!initialized)
  97. {
  98. Initialize();
  99. initialized = true;
  100. }
  101. currentPosition = position;
  102. var p = position - scrollOffset / cellInterval;
  103. var firstIndex = Mathf.CeilToInt(p);
  104. var firstPosition = (Mathf.Ceil(p) - p) * cellInterval;
  105. if (firstPosition + pool.Count * cellInterval < 1f)
  106. {
  107. ResizePool(firstPosition);
  108. }
  109. UpdateCells(firstPosition, firstIndex, forceRefresh);
  110. }
  111. void ResizePool(float firstPosition)
  112. {
  113. Debug.Assert(CellPrefab != null);
  114. Debug.Assert(cellContainer != null);
  115. var addCount = Mathf.CeilToInt((1f - firstPosition) / cellInterval) - pool.Count;
  116. for (var i = 0; i < addCount; i++)
  117. {
  118. var cell = Instantiate(CellPrefab, cellContainer).GetComponent<FancyCell<TItemData, TContext>>();
  119. if (cell == null)
  120. {
  121. throw new MissingComponentException(string.Format(
  122. "FancyCell<{0}, {1}> component not found in {2}.",
  123. typeof(TItemData).FullName, typeof(TContext).FullName, CellPrefab.name));
  124. }
  125. cell.SetContext(Context);
  126. cell.Initialize();
  127. cell.SetVisible(false);
  128. pool.Add(cell);
  129. }
  130. }
  131. void UpdateCells(float firstPosition, int firstIndex, bool forceRefresh)
  132. {
  133. for (var i = 0; i < pool.Count; i++)
  134. {
  135. var index = firstIndex + i;
  136. var position = firstPosition + i * cellInterval;
  137. var cell = pool[CircularIndex(index, pool.Count)];
  138. if (loop)
  139. {
  140. index = CircularIndex(index, ItemsSource.Count);
  141. }
  142. if (index < 0 || index >= ItemsSource.Count || position > 1f)
  143. {
  144. cell.SetVisible(false);
  145. continue;
  146. }
  147. if (forceRefresh || cell.Index != index || !cell.IsVisible)
  148. {
  149. cell.Index = index;
  150. cell.SetVisible(true);
  151. cell.UpdateContent(ItemsSource[index]);
  152. }
  153. cell.UpdatePosition(position);
  154. }
  155. }
  156. int CircularIndex(int i, int size) => size < 1 ? 0 : i < 0 ? size - 1 + (i + 1) % size : i % size;
  157. #if UNITY_EDITOR
  158. bool cachedLoop;
  159. float cachedCellInterval, cachedScrollOffset;
  160. void LateUpdate()
  161. {
  162. if (cachedLoop != loop ||
  163. cachedCellInterval != cellInterval ||
  164. cachedScrollOffset != scrollOffset)
  165. {
  166. cachedLoop = loop;
  167. cachedCellInterval = cellInterval;
  168. cachedScrollOffset = scrollOffset;
  169. UpdatePosition(currentPosition);
  170. }
  171. }
  172. #endif
  173. }
  174. /// <summary>
  175. /// <see cref="FancyScrollView{TItemData}"/> のコンテキストクラス.
  176. /// </summary>
  177. public sealed class NullContext { }
  178. /// <summary>
  179. /// スクロールビューを実装するための抽象基底クラス.
  180. /// 無限スクロールおよびスナップに対応しています.
  181. /// </summary>
  182. /// <typeparam name="TItemData"></typeparam>
  183. /// <seealso cref="FancyScrollView{TItemData, TContext}"/>
  184. public abstract class FancyScrollView<TItemData> : FancyScrollView<TItemData, NullContext> { }
  185. }