FancyScrollRect.cs 13 KB

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