FancyScrollRect.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /// Credit setchi (https://github.com/setchi)
  2. /// Sourced from - https://github.com/setchi/FancyScrollView
  3. using System;
  4. using System.Collections.Generic;
  5. using UnityEngine.UI.Extensions.EasingCore;
  6. namespace UnityEngine.UI.Extensions
  7. {
  8. /// <summary>
  9. /// ScrollRect スタイルのスクロールビューを実装するための抽象基底クラス.
  10. /// 無限スクロールおよびスナップには対応していません.
  11. /// <see cref="FancyScrollView{TItemData, TContext}.Context"/> が不要な場合は
  12. /// 代わりに <see cref="FancyScrollRect{TItemData}"/> を使用します.
  13. /// </summary>
  14. /// <typeparam name="TItemData">アイテムのデータ型.</typeparam>
  15. /// <typeparam name="TContext"><see cref="FancyScrollView{TItemData, TContext}.Context"/> の型.</typeparam>
  16. [RequireComponent(typeof(Scroller))]
  17. public abstract class FancyScrollRect<TItemData, TContext> : FancyScrollView<TItemData, TContext>
  18. where TContext : class, IFancyScrollRectContext, new()
  19. {
  20. /// <summary>
  21. /// スクロール中にセルが再利用されるまでの余白のセル数.
  22. /// </summary>
  23. /// <remarks>
  24. /// <c>0</c> を指定するとセルが完全に隠れた直後に再利用されます.
  25. /// <c>1</c> 以上を指定すると, そのセル数だけ余分にスクロールしてから再利用されます.
  26. /// </remarks>
  27. [SerializeField] protected float reuseCellMarginCount = 0f;
  28. /// <summary>
  29. /// コンテンツ先頭の余白.
  30. /// </summary>
  31. [SerializeField] protected float paddingHead = 0f;
  32. /// <summary>
  33. /// コンテンツ末尾の余白.
  34. /// </summary>
  35. [SerializeField] protected float paddingTail = 0f;
  36. /// <summary>
  37. /// スクロール軸方向のセル同士の余白.
  38. /// </summary>
  39. [SerializeField] protected float spacing = 0f;
  40. /// <summary>
  41. /// セルのサイズ.
  42. /// </summary>
  43. protected abstract float CellSize { get; }
  44. /// <summary>
  45. /// スクロール可能かどうか.
  46. /// </summary>
  47. /// <remarks>
  48. /// アイテム数が十分少なくビューポート内に全てのセルが収まっている場合は <c>false</c>, それ以外は <c>true</c> になります.
  49. /// </remarks>
  50. protected virtual bool Scrollable => MaxScrollPosition > 0f;
  51. Scroller cachedScroller;
  52. /// <summary>
  53. /// スクロール位置を制御する <see cref="FancyScrollView.Scroller"/> のインスタンス.
  54. /// </summary>
  55. /// <remarks>
  56. /// <see cref="Scroller"/> のスクロール位置を変更する際は必ず <see cref="ToScrollerPosition(float)"/> を使用して変換した位置を使用してください.
  57. /// </remarks>
  58. protected Scroller Scroller => cachedScroller ?? (cachedScroller = GetComponent<Scroller>());
  59. float ScrollLength => 1f / Mathf.Max(cellInterval, 1e-2f) - 1f;
  60. float ViewportLength => ScrollLength - reuseCellMarginCount * 2f;
  61. float PaddingHeadLength => (paddingHead - spacing * 0.5f) / (CellSize + spacing);
  62. float MaxScrollPosition => ItemsSource.Count
  63. - ScrollLength
  64. + reuseCellMarginCount * 2f
  65. + (paddingHead + paddingTail - spacing) / (CellSize + spacing);
  66. /// <inheritdoc/>
  67. protected override void Initialize()
  68. {
  69. base.Initialize();
  70. Context.ScrollDirection = Scroller.ScrollDirection;
  71. Context.CalculateScrollSize = () =>
  72. {
  73. var interval = CellSize + spacing;
  74. var reuseMargin = interval * reuseCellMarginCount;
  75. var scrollSize = Scroller.ViewportSize + interval + reuseMargin * 2f;
  76. return (scrollSize, reuseMargin);
  77. };
  78. AdjustCellIntervalAndScrollOffset();
  79. Scroller.OnValueChanged(OnScrollerValueChanged);
  80. }
  81. /// <summary>
  82. /// <see cref="Scroller"/> のスクロール位置が変更された際の処理.
  83. /// </summary>
  84. /// <param name="p"><see cref="Scroller"/> のスクロール位置.</param>
  85. void OnScrollerValueChanged(float p)
  86. {
  87. base.UpdatePosition(Scrollable ? ToFancyScrollViewPosition(p) : 0f);
  88. if (Scroller.Scrollbar)
  89. {
  90. if (p > ItemsSource.Count - 1)
  91. {
  92. ShrinkScrollbar(p - (ItemsSource.Count - 1));
  93. }
  94. else if (p < 0f)
  95. {
  96. ShrinkScrollbar(-p);
  97. }
  98. }
  99. }
  100. /// <summary>
  101. /// スクロール範囲を超えてスクロールされた量に基づいて, スクロールバーのサイズを縮小します.
  102. /// </summary>
  103. /// <param name="offset">スクロール範囲を超えてスクロールされた量.</param>
  104. void ShrinkScrollbar(float offset)
  105. {
  106. var scale = 1f - ToFancyScrollViewPosition(offset) / (ViewportLength - PaddingHeadLength);
  107. UpdateScrollbarSize((ViewportLength - PaddingHeadLength) * scale);
  108. }
  109. /// <inheritdoc/>
  110. protected override void Refresh()
  111. {
  112. AdjustCellIntervalAndScrollOffset();
  113. RefreshScroller();
  114. base.Refresh();
  115. }
  116. /// <inheritdoc/>
  117. protected override void Relayout()
  118. {
  119. AdjustCellIntervalAndScrollOffset();
  120. RefreshScroller();
  121. base.Relayout();
  122. }
  123. /// <summary>
  124. /// <see cref="Scroller"/> の各種状態を更新します.
  125. /// </summary>
  126. protected void RefreshScroller()
  127. {
  128. Scroller.Draggable = Scrollable;
  129. Scroller.ScrollSensitivity = ToScrollerPosition(ViewportLength - PaddingHeadLength);
  130. Scroller.Position = ToScrollerPosition(currentPosition);
  131. if (Scroller.Scrollbar)
  132. {
  133. Scroller.Scrollbar.gameObject.SetActive(Scrollable);
  134. UpdateScrollbarSize(ViewportLength);
  135. }
  136. }
  137. /// <inheritdoc/>
  138. protected override void UpdateContents(IList<TItemData> items)
  139. {
  140. Debug.Assert(Context.CalculateScrollSize != null);
  141. AdjustCellIntervalAndScrollOffset();
  142. base.UpdateContents(items);
  143. Scroller.SetTotalCount(items.Count);
  144. RefreshScroller();
  145. }
  146. /// <summary>
  147. /// スクロール位置を更新します.
  148. /// </summary>
  149. /// <param name="position">スクロール位置.</param>
  150. protected new void UpdatePosition(float position)
  151. {
  152. Scroller.Position = ToScrollerPosition(position, 0.5f);
  153. }
  154. /// <summary>
  155. /// 指定したアイテムの位置までジャンプします.
  156. /// </summary>
  157. /// <param name="itemIndex">アイテムのインデックス.</param>
  158. /// <param name="alignment">ビューポート内におけるセル位置の基準. 0f(先頭) ~ 1f(末尾).</param>
  159. protected virtual void JumpTo(int itemIndex, float alignment = 0.5f)
  160. {
  161. Scroller.Position = ToScrollerPosition(itemIndex, alignment);
  162. }
  163. /// <summary>
  164. /// 指定したアイテムの位置まで移動します.
  165. /// </summary>
  166. /// <param name="index">アイテムのインデックス.</param>
  167. /// <param name="duration">移動にかける秒数.</param>
  168. /// <param name="alignment">ビューポート内におけるセル位置の基準. 0f(先頭) ~ 1f(末尾).</param>
  169. /// <param name="onComplete">移動が完了した際に呼び出されるコールバック.</param>
  170. protected virtual void ScrollTo(int index, float duration, float alignment = 0.5f, Action onComplete = null)
  171. {
  172. Scroller.ScrollTo(ToScrollerPosition(index, alignment), duration, onComplete);
  173. }
  174. /// <summary>
  175. /// 指定したアイテムの位置まで移動します.
  176. /// </summary>
  177. /// <param name="index">アイテムのインデックス.</param>
  178. /// <param name="duration">移動にかける秒数.</param>
  179. /// <param name="easing">移動に使用するイージング.</param>
  180. /// <param name="alignment">ビューポート内におけるセル位置の基準. 0f(先頭) ~ 1f(末尾).</param>
  181. /// <param name="onComplete">移動が完了した際に呼び出されるコールバック.</param>
  182. protected virtual void ScrollTo(int index, float duration, Ease easing, float alignment = 0.5f, Action onComplete = null)
  183. {
  184. Scroller.ScrollTo(ToScrollerPosition(index, alignment), duration, easing, onComplete);
  185. }
  186. /// <summary>
  187. /// ビューポートとコンテンツの長さに基づいてスクロールバーのサイズを更新します.
  188. /// </summary>
  189. /// <param name="viewportLength">ビューポートのサイズ.</param>
  190. protected void UpdateScrollbarSize(float viewportLength)
  191. {
  192. var contentLength = Mathf.Max(ItemsSource.Count + (paddingHead + paddingTail - spacing) / (CellSize + spacing), 1);
  193. Scroller.Scrollbar.size = Scrollable ? Mathf.Clamp01(viewportLength / contentLength) : 1f;
  194. }
  195. /// <summary>
  196. /// <see cref="Scroller"/> が扱うスクロール位置を <see cref="FancyScrollRect{TItemData, TContext}"/> が扱うスクロール位置に変換します.
  197. /// </summary>
  198. /// <param name="position"><see cref="Scroller"/> が扱うスクロール位置.</param>
  199. /// <returns><see cref="FancyScrollRect{TItemData, TContext}"/> が扱うスクロール位置.</returns>
  200. protected float ToFancyScrollViewPosition(float position)
  201. {
  202. return position / Mathf.Max(ItemsSource.Count - 1, 1) * MaxScrollPosition - PaddingHeadLength;
  203. }
  204. /// <summary>
  205. /// <see cref="FancyScrollRect{TItemData, TContext}"/> が扱うスクロール位置を <see cref="Scroller"/> が扱うスクロール位置に変換します.
  206. /// </summary>
  207. /// <param name="position"><see cref="FancyScrollRect{TItemData, TContext}"/> が扱うスクロール位置.</param>
  208. /// <returns><see cref="Scroller"/> が扱うスクロール位置.</returns>
  209. protected float ToScrollerPosition(float position)
  210. {
  211. return (position + PaddingHeadLength) / MaxScrollPosition * Mathf.Max(ItemsSource.Count - 1, 1);
  212. }
  213. /// <summary>
  214. /// <see cref="FancyScrollRect{TItemData, TContext}"/> が扱うスクロール位置を <see cref="Scroller"/> が扱うスクロール位置に変換します.
  215. /// </summary>
  216. /// <param name="position"><see cref="FancyScrollRect{TItemData, TContext}"/> が扱うスクロール位置.</param>
  217. /// <param name="alignment">ビューポート内におけるセル位置の基準. 0f(先頭) ~ 1f(末尾).</param>
  218. /// <returns><see cref="Scroller"/> が扱うスクロール位置.</returns>
  219. protected float ToScrollerPosition(float position, float alignment = 0.5f)
  220. {
  221. var offset = alignment * (ScrollLength - (1f + reuseCellMarginCount * 2f))
  222. + (1f - alignment - 0.5f) * spacing / (CellSize + spacing);
  223. return ToScrollerPosition(Mathf.Clamp(position - offset, 0f, MaxScrollPosition));
  224. }
  225. /// <summary>
  226. /// 指定された設定を実現するための
  227. /// <see cref="FancyScrollView{TItemData,TContext}.cellInterval"/> と
  228. /// <see cref="FancyScrollView{TItemData,TContext}.scrollOffset"/> を計算して適用します.
  229. /// </summary>
  230. protected void AdjustCellIntervalAndScrollOffset()
  231. {
  232. var totalSize = Scroller.ViewportSize + (CellSize + spacing) * (1f + reuseCellMarginCount * 2f);
  233. cellInterval = (CellSize + spacing) / totalSize;
  234. scrollOffset = cellInterval * (1f + reuseCellMarginCount);
  235. }
  236. protected virtual void OnValidate()
  237. {
  238. AdjustCellIntervalAndScrollOffset();
  239. if (loop)
  240. {
  241. loop = false;
  242. Debug.LogError("Loop is currently not supported in FancyScrollRect.");
  243. }
  244. if (Scroller.SnapEnabled)
  245. {
  246. Scroller.SnapEnabled = false;
  247. Debug.LogError("Snap is currently not supported in FancyScrollRect.");
  248. }
  249. if (Scroller.MovementType == MovementType.Unrestricted)
  250. {
  251. Scroller.MovementType = MovementType.Elastic;
  252. Debug.LogError("MovementType.Unrestricted is currently not supported in FancyScrollRect.");
  253. }
  254. }
  255. }
  256. /// <summary>
  257. /// ScrollRect スタイルのスクロールビューを実装するための抽象基底クラス.
  258. /// 無限スクロールおよびスナップには対応していません.
  259. /// </summary>
  260. /// <typeparam name="TItemData">アイテムのデータ型.</typeparam>
  261. /// <seealso cref="FancyScrollRect{TItemData, TContext}"/>
  262. public abstract class FancyScrollRect<TItemData> : FancyScrollRect<TItemData, FancyScrollRectContext> { }
  263. }