2
0

ScrollPositionController.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. /// Credit setchi (https://github.com/setchi)
  2. /// Sourced from - https://github.com/setchi/FancyScrollView
  3. using System;
  4. using UnityEngine.Events;
  5. using UnityEngine.EventSystems;
  6. namespace UnityEngine.UI.Extensions
  7. {
  8. public class ScrollPositionController : UIBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler
  9. {
  10. [SerializeField]
  11. RectTransform viewport = null;
  12. [SerializeField]
  13. ScrollDirection directionOfRecognize = ScrollDirection.Vertical;
  14. [SerializeField]
  15. MovementType movementType = MovementType.Elastic;
  16. [SerializeField]
  17. float elasticity = 0.1f;
  18. [SerializeField]
  19. float scrollSensitivity = 1f;
  20. [SerializeField]
  21. bool inertia = true;
  22. [SerializeField, Tooltip("Only used when inertia is enabled")]
  23. float decelerationRate = 0.03f;
  24. [SerializeField, Tooltip("Only used when inertia is enabled")]
  25. Snap snap = new Snap { Enable = true, VelocityThreshold = 0.5f, Duration = 0.3f };
  26. [SerializeField]
  27. int dataCount;
  28. readonly AutoScrollState autoScrollState = new AutoScrollState();
  29. Action<float> onUpdatePosition;
  30. Action<int> onItemSelected;
  31. Vector2 pointerStartLocalPosition;
  32. float dragStartScrollPosition;
  33. float prevScrollPosition;
  34. float currentScrollPosition;
  35. bool dragging;
  36. float velocity;
  37. enum ScrollDirection
  38. {
  39. Vertical,
  40. Horizontal,
  41. }
  42. enum MovementType
  43. {
  44. Unrestricted = ScrollRect.MovementType.Unrestricted,
  45. Elastic = ScrollRect.MovementType.Elastic,
  46. Clamped = ScrollRect.MovementType.Clamped
  47. }
  48. [Serializable]
  49. struct Snap
  50. {
  51. public bool Enable;
  52. public float VelocityThreshold;
  53. public float Duration;
  54. }
  55. class AutoScrollState
  56. {
  57. public bool Enable;
  58. public bool Elastic;
  59. public float Duration;
  60. public float StartTime;
  61. public float EndScrollPosition;
  62. public void Reset()
  63. {
  64. Enable = false;
  65. Elastic = false;
  66. Duration = 0f;
  67. StartTime = 0f;
  68. EndScrollPosition = 0f;
  69. }
  70. }
  71. public void OnUpdatePosition(Action<float> onUpdatePosition)
  72. {
  73. this.onUpdatePosition = onUpdatePosition;
  74. }
  75. public void OnItemSelected(Action<int> onItemSelected)
  76. {
  77. this.onItemSelected = onItemSelected;
  78. }
  79. public void SetDataCount(int dataCount)
  80. {
  81. this.dataCount = dataCount;
  82. }
  83. public void ScrollTo(int index, float duration)
  84. {
  85. autoScrollState.Reset();
  86. autoScrollState.Enable = true;
  87. autoScrollState.Duration = duration;
  88. autoScrollState.StartTime = Time.unscaledTime;
  89. autoScrollState.EndScrollPosition = CalculateDestinationIndex(index);
  90. velocity = 0f;
  91. dragStartScrollPosition = currentScrollPosition;
  92. ItemSelected(Mathf.RoundToInt(GetCircularPosition(autoScrollState.EndScrollPosition, dataCount)));
  93. }
  94. public void JumpTo(int index)
  95. {
  96. autoScrollState.Reset();
  97. velocity = 0f;
  98. dragging = false;
  99. index = CalculateDestinationIndex(index);
  100. ItemSelected(index);
  101. UpdatePosition(index);
  102. }
  103. void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
  104. {
  105. if (eventData.button != PointerEventData.InputButton.Left)
  106. {
  107. return;
  108. }
  109. pointerStartLocalPosition = Vector2.zero;
  110. RectTransformUtility.ScreenPointToLocalPointInRectangle(
  111. viewport,
  112. eventData.position,
  113. eventData.pressEventCamera,
  114. out pointerStartLocalPosition);
  115. dragStartScrollPosition = currentScrollPosition;
  116. dragging = true;
  117. autoScrollState.Reset();
  118. }
  119. void IDragHandler.OnDrag(PointerEventData eventData)
  120. {
  121. if (eventData.button != PointerEventData.InputButton.Left)
  122. {
  123. return;
  124. }
  125. if (!dragging)
  126. {
  127. return;
  128. }
  129. Vector2 localCursor;
  130. if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
  131. viewport,
  132. eventData.position,
  133. eventData.pressEventCamera,
  134. out localCursor))
  135. {
  136. return;
  137. }
  138. var pointerDelta = localCursor - pointerStartLocalPosition;
  139. var position = (directionOfRecognize == ScrollDirection.Horizontal ? -pointerDelta.x : pointerDelta.y)
  140. / GetViewportSize()
  141. * scrollSensitivity
  142. + dragStartScrollPosition;
  143. var offset = CalculateOffset(position);
  144. position += offset;
  145. if (movementType == MovementType.Elastic)
  146. {
  147. if (offset != 0f)
  148. {
  149. position -= RubberDelta(offset, scrollSensitivity);
  150. }
  151. }
  152. UpdatePosition(position);
  153. }
  154. void IEndDragHandler.OnEndDrag(PointerEventData eventData)
  155. {
  156. if (eventData.button != PointerEventData.InputButton.Left)
  157. {
  158. return;
  159. }
  160. dragging = false;
  161. }
  162. float GetViewportSize()
  163. {
  164. return directionOfRecognize == ScrollDirection.Horizontal
  165. ? viewport.rect.size.x
  166. : viewport.rect.size.y;
  167. }
  168. float CalculateOffset(float position)
  169. {
  170. if (movementType == MovementType.Unrestricted)
  171. {
  172. return 0f;
  173. }
  174. if (position < 0f)
  175. {
  176. return -position;
  177. }
  178. if (position > dataCount - 1)
  179. {
  180. return dataCount - 1 - position;
  181. }
  182. return 0f;
  183. }
  184. void UpdatePosition(float position)
  185. {
  186. currentScrollPosition = position;
  187. if (onUpdatePosition != null)
  188. {
  189. onUpdatePosition(currentScrollPosition);
  190. }
  191. }
  192. void ItemSelected(int index)
  193. {
  194. if (onItemSelected != null)
  195. {
  196. onItemSelected(index);
  197. }
  198. }
  199. float RubberDelta(float overStretching, float viewSize)
  200. {
  201. return (1 - (1 / ((Mathf.Abs(overStretching) * 0.55f / viewSize) + 1))) * viewSize * Mathf.Sign(overStretching);
  202. }
  203. void Update()
  204. {
  205. var deltaTime = Time.unscaledDeltaTime;
  206. var offset = CalculateOffset(currentScrollPosition);
  207. if (autoScrollState.Enable)
  208. {
  209. var position = 0f;
  210. if (autoScrollState.Elastic)
  211. {
  212. var speed = velocity;
  213. position = Mathf.SmoothDamp(currentScrollPosition, currentScrollPosition + offset, ref speed, elasticity, Mathf.Infinity, deltaTime);
  214. velocity = speed;
  215. if (Mathf.Abs(velocity) < 0.01f)
  216. {
  217. position = Mathf.Clamp(Mathf.RoundToInt(position), 0, dataCount - 1);
  218. velocity = 0f;
  219. autoScrollState.Reset();
  220. }
  221. }
  222. else
  223. {
  224. var alpha = Mathf.Clamp01((Time.unscaledTime - autoScrollState.StartTime) / Mathf.Max(autoScrollState.Duration, float.Epsilon));
  225. position = Mathf.Lerp(dragStartScrollPosition, autoScrollState.EndScrollPosition, EaseInOutCubic(0, 1, alpha));
  226. if (Mathf.Approximately(alpha, 1f))
  227. {
  228. autoScrollState.Reset();
  229. }
  230. }
  231. UpdatePosition(position);
  232. }
  233. else if (!dragging && (!Mathf.Approximately(offset, 0f) || !Mathf.Approximately(velocity, 0f)))
  234. {
  235. var position = currentScrollPosition;
  236. if (movementType == MovementType.Elastic && !Mathf.Approximately(offset, 0f))
  237. {
  238. autoScrollState.Reset();
  239. autoScrollState.Enable = true;
  240. autoScrollState.Elastic = true;
  241. ItemSelected(Mathf.Clamp(Mathf.RoundToInt(position), 0, dataCount - 1));
  242. }
  243. else if (inertia)
  244. {
  245. velocity *= Mathf.Pow(decelerationRate, deltaTime);
  246. if (Mathf.Abs(velocity) < 0.001f)
  247. {
  248. velocity = 0f;
  249. }
  250. position += velocity * deltaTime;
  251. if (snap.Enable && Mathf.Abs(velocity) < snap.VelocityThreshold)
  252. {
  253. ScrollTo(Mathf.RoundToInt(currentScrollPosition), snap.Duration);
  254. }
  255. }
  256. else
  257. {
  258. velocity = 0f;
  259. }
  260. if (!Mathf.Approximately(velocity, 0f))
  261. {
  262. if (movementType == MovementType.Clamped)
  263. {
  264. offset = CalculateOffset(position);
  265. position += offset;
  266. if (Mathf.Approximately(position, 0f) || Mathf.Approximately(position, dataCount - 1f))
  267. {
  268. velocity = 0f;
  269. ItemSelected(Mathf.RoundToInt(position));
  270. }
  271. }
  272. UpdatePosition(position);
  273. }
  274. }
  275. if (!autoScrollState.Enable && dragging && inertia)
  276. {
  277. var newVelocity = (currentScrollPosition - prevScrollPosition) / deltaTime;
  278. velocity = Mathf.Lerp(velocity, newVelocity, deltaTime * 10f);
  279. }
  280. if (currentScrollPosition != prevScrollPosition)
  281. {
  282. prevScrollPosition = currentScrollPosition;
  283. }
  284. }
  285. int CalculateDestinationIndex(int index)
  286. {
  287. return movementType == MovementType.Unrestricted
  288. ? CalculateClosestIndex(index)
  289. : Mathf.Clamp(index, 0, dataCount - 1);
  290. }
  291. int CalculateClosestIndex(int index)
  292. {
  293. var diff = GetCircularPosition(index, dataCount)
  294. - GetCircularPosition(currentScrollPosition, dataCount);
  295. if (Mathf.Abs(diff) > dataCount * 0.5f)
  296. {
  297. diff = Mathf.Sign(-diff) * (dataCount - Mathf.Abs(diff));
  298. }
  299. return Mathf.RoundToInt(diff + currentScrollPosition);
  300. }
  301. float GetCircularPosition(float position, int length)
  302. {
  303. return position < 0 ? length - 1 + (position + 1) % length : position % length;
  304. }
  305. float EaseInOutCubic(float start, float end, float value)
  306. {
  307. value /= 0.5f;
  308. end -= start;
  309. if (value < 1f)
  310. {
  311. return end * 0.5f * value * value * value + start;
  312. }
  313. value -= 2f;
  314. return end * 0.5f * (value * value * value + 2f) + start;
  315. }
  316. }
  317. }