13 RectTransform viewport =
null;
15 ScrollDirection directionOfRecognize = ScrollDirection.Vertical;
17 MovementType movementType = MovementType.Elastic;
19 float elasticity = 0.1f;
21 float scrollSensitivity = 1f;
24 [SerializeField, Tooltip(
"Only used when inertia is enabled")]
25 float decelerationRate = 0.03f;
26 [SerializeField, Tooltip(
"Only used when inertia is enabled")]
27 Snap snap =
new Snap { Enable =
true, VelocityThreshold = 0.5f, Duration = 0.3f };
31 readonly AutoScrollState autoScrollState =
new AutoScrollState();
33 Action<float> onUpdatePosition;
34 Action<int> onItemSelected;
36 Vector2 pointerStartLocalPosition;
37 float dragStartScrollPosition;
38 float prevScrollPosition;
39 float currentScrollPosition;
53 Elastic = ScrollRect.MovementType.Elastic,
54 Clamped = ScrollRect.MovementType.Clamped
61 public float VelocityThreshold;
62 public float Duration;
69 public float Duration;
70 public float StartTime;
71 public float EndScrollPosition;
79 EndScrollPosition = 0f;
85 this.onUpdatePosition = onUpdatePosition;
90 this.onItemSelected = onItemSelected;
95 this.dataCount = dataCount;
98 public void ScrollTo(
int index,
float duration)
100 autoScrollState.Reset();
101 autoScrollState.Enable =
true;
102 autoScrollState.Duration = duration;
103 autoScrollState.StartTime = Time.unscaledTime;
104 autoScrollState.EndScrollPosition = CalculateDestinationIndex(index);
107 dragStartScrollPosition = currentScrollPosition;
109 ItemSelected(Mathf.RoundToInt(GetCircularPosition(autoScrollState.EndScrollPosition, dataCount)));
114 autoScrollState.Reset();
119 index = CalculateDestinationIndex(index);
122 UpdatePosition(index);
125 void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
127 if (eventData.button != PointerEventData.InputButton.Left)
132 pointerStartLocalPosition = Vector2.zero;
133 RectTransformUtility.ScreenPointToLocalPointInRectangle(
136 eventData.pressEventCamera,
137 out pointerStartLocalPosition);
139 dragStartScrollPosition = currentScrollPosition;
141 autoScrollState.Reset();
144 void IDragHandler.OnDrag(PointerEventData eventData)
146 if (eventData.button != PointerEventData.InputButton.Left)
157 if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
160 eventData.pressEventCamera,
166 var pointerDelta = localCursor - pointerStartLocalPosition;
167 var position = (directionOfRecognize == ScrollDirection.Horizontal ? -pointerDelta.x : pointerDelta.y)
170 + dragStartScrollPosition;
172 var offset = CalculateOffset(position);
175 if (movementType == MovementType.Elastic)
179 position -= RubberDelta(offset, scrollSensitivity);
183 UpdatePosition(position);
186 void IEndDragHandler.OnEndDrag(PointerEventData eventData)
188 if (eventData.button != PointerEventData.InputButton.Left)
196 float GetViewportSize()
198 return directionOfRecognize == ScrollDirection.Horizontal
199 ? viewport.rect.size.x
200 : viewport.rect.size.y;
203 float CalculateOffset(
float position)
205 if (movementType == MovementType.Unrestricted)
215 if (position > dataCount - 1)
217 return dataCount - 1 - position;
223 void UpdatePosition(
float position)
225 currentScrollPosition = position;
227 if (onUpdatePosition !=
null)
229 onUpdatePosition(currentScrollPosition);
233 void ItemSelected(
int index)
235 if (onItemSelected !=
null)
237 onItemSelected(index);
241 float RubberDelta(
float overStretching,
float viewSize)
243 return (1 - (1 / ((Mathf.Abs(overStretching) * 0.55f / viewSize) + 1))) * viewSize * Mathf.Sign(overStretching);
248 var deltaTime = Time.unscaledDeltaTime;
249 var offset = CalculateOffset(currentScrollPosition);
251 if (autoScrollState.Enable)
255 if (autoScrollState.Elastic)
257 var speed = velocity;
258 position = Mathf.SmoothDamp(currentScrollPosition, currentScrollPosition + offset, ref speed, elasticity, Mathf.Infinity, deltaTime);
261 if (Mathf.Abs(velocity) < 0.01f)
263 position = Mathf.Clamp(Mathf.RoundToInt(position), 0, dataCount - 1);
265 autoScrollState.Reset();
270 var alpha = Mathf.Clamp01((Time.unscaledTime - autoScrollState.StartTime) / Mathf.Max(autoScrollState.Duration,
float.Epsilon));
271 position = Mathf.Lerp(dragStartScrollPosition, autoScrollState.EndScrollPosition, EaseInOutCubic(0, 1, alpha));
273 if (Mathf.Approximately(alpha, 1f))
275 autoScrollState.Reset();
279 UpdatePosition(position);
281 else if (!dragging && (!Mathf.Approximately(offset, 0f) || !Mathf.Approximately(velocity, 0f)))
283 var position = currentScrollPosition;
285 if (movementType == MovementType.Elastic && !Mathf.Approximately(offset, 0f))
287 autoScrollState.Reset();
288 autoScrollState.Enable =
true;
289 autoScrollState.Elastic =
true;
291 ItemSelected(Mathf.Clamp(Mathf.RoundToInt(position), 0, dataCount - 1));
295 velocity *= Mathf.Pow(decelerationRate, deltaTime);
297 if (Mathf.Abs(velocity) < 0.001f)
302 position += velocity * deltaTime;
304 if (snap.Enable && Mathf.Abs(velocity) < snap.VelocityThreshold)
306 ScrollTo(Mathf.RoundToInt(currentScrollPosition), snap.Duration);
314 if (!Mathf.Approximately(velocity, 0f))
316 if (movementType == MovementType.Clamped)
318 offset = CalculateOffset(position);
321 if (Mathf.Approximately(position, 0f) || Mathf.Approximately(position, dataCount - 1f))
324 ItemSelected(Mathf.RoundToInt(position));
328 UpdatePosition(position);
332 if (!autoScrollState.Enable && dragging && inertia)
334 var newVelocity = (currentScrollPosition - prevScrollPosition) / deltaTime;
335 velocity = Mathf.Lerp(velocity, newVelocity, deltaTime * 10f);
338 if (currentScrollPosition != prevScrollPosition)
340 prevScrollPosition = currentScrollPosition;
344 int CalculateDestinationIndex(
int index)
346 return movementType == MovementType.Unrestricted
347 ? CalculateClosestIndex(index)
348 : Mathf.Clamp(index, 0, dataCount - 1);
351 int CalculateClosestIndex(
int index)
353 var diff = GetCircularPosition(index, dataCount)
354 - GetCircularPosition(currentScrollPosition, dataCount);
356 if (Mathf.Abs(diff) > dataCount * 0.5f)
358 diff = Mathf.Sign(-diff) * (dataCount - Mathf.Abs(diff));
361 return Mathf.RoundToInt(diff + currentScrollPosition);
364 float GetCircularPosition(
float position,
int length)
366 return position < 0 ? length - 1 + (position + 1) % length : position % length;
369 float EaseInOutCubic(
float start,
float end,
float value)
376 return end * 0.5f * value * value * value + start;
380 return end * 0.5f * (value * value * value + 2f) + start;
Credit Erdener Gonenc - @PixelEnvision.