Tanoda
ContentScrollSnapHorizontal.cs
Go to the documentation of this file.
1using System.Collections;
2using System.Collections.Generic;
5using System;
6
8{
9 [ExecuteInEditMode]
10 [RequireComponent(typeof(ScrollRect))]
11 [AddComponentMenu("UI/Extensions/ContentSnapScrollHorizontal")]
12 public class ContentScrollSnapHorizontal : MonoBehaviour, IBeginDragHandler, IEndDragHandler
13 {
14
15 [Serializable]
16 public class StartMovementEvent : UnityEvent { }
17 [Serializable]
18 public class CurrentItemChangeEvent : UnityEvent<int> { }
19 [Serializable]
20 public class FoundItemToSnapToEvent : UnityEvent<int> { }
21 [Serializable]
22 public class SnappedToItemEvent : UnityEvent<int> { }
23
24 public bool ignoreInactiveItems = true;
25 public MoveInfo startInfo = new MoveInfo(MoveInfo.IndexType.positionIndex, 0);
26 public GameObject prevButton;
27 public GameObject nextButton;
28 public GameObject pagination;
29 [Tooltip("The velocity below which the scroll rect will start to snap")]
31
32 [Header("Paging Info")]
33 [Tooltip("Should the pagination & buttons jump or lerp to the items")]
34 public bool jumpToItem = false;
35 [Tooltip("The time it will take for the pagination or buttons to move between items")]
36 public float lerpTime = .3f;
37
38 [Header("Events")]
39 [SerializeField]
40 [Tooltip("Event is triggered whenever the scroll rect starts to move, even when triggered programmatically")]
41 private StartMovementEvent m_StartMovementEvent = new StartMovementEvent();
43 {
44 get
45 {
46 return m_StartMovementEvent;
47 }
48 set
49 {
50 m_StartMovementEvent = value;
51 }
52 }
53
54 [SerializeField]
55 [Tooltip("Event is triggered whenever the closest item to the center of the scrollrect changes")]
56 private CurrentItemChangeEvent m_CurrentItemChangeEvent = new CurrentItemChangeEvent();
58 {
59 get
60 {
61 return m_CurrentItemChangeEvent;
62 }
63 set
64 {
65 m_CurrentItemChangeEvent = value;
66 }
67 }
68
69 [SerializeField]
70 [Tooltip("Event is triggered when the ContentSnapScroll decides which item it is going to snap to. Returns the index of the closest position.")]
71 private FoundItemToSnapToEvent m_FoundItemToSnapToEvent = new FoundItemToSnapToEvent();
73 {
74 get
75 {
76 return m_FoundItemToSnapToEvent;
77 }
78 set
79 {
80 m_FoundItemToSnapToEvent = value;
81 }
82 }
83
84 [SerializeField]
85 [Tooltip("Event is triggered when we finally settle on an element. Returns the index of the item's position.")]
86 private SnappedToItemEvent m_SnappedToItemEvent = new SnappedToItemEvent();
88 {
89 get
90 {
91 return m_SnappedToItemEvent;
92 }
93 set
94 {
95 m_SnappedToItemEvent = value;
96 }
97 }
98
99 private ScrollRect scrollRect = null;
100 private RectTransform scrollRectTransform = null;
101 private RectTransform contentTransform = null;
102 private List<Vector3> contentPositions = null;
103 private Vector3 lerpTarget = Vector3.zero;
104 private float totalScrollableWidth = 0;
105 private DrivenRectTransformTracker tracker ;
106 private float mLerpTime = 0;
107 private int _closestItem = 0;
108 private bool mSliding = false;
109 private bool mLerping = false;
110 private bool ContentIsHorizonalLayoutGroup
111 {
112 get
113 {
114 return contentTransform.GetComponent<HorizontalLayoutGroup>() != null;
115 }
116 }
117
118 #region Public Info
122 public bool Moving
123 {
124 get
125 {
126 return Sliding || Lerping;
127 }
128 }
129
133 public bool Sliding
134 {
135 get
136 {
137 return mSliding;
138 }
139 }
143 public bool Lerping
144 {
145 get
146 {
147 return mLerping;
148 }
149 }
150
156 {
157 get
158 {
159 return contentPositions.IndexOf(FindClosestFrom(contentTransform.localPosition));
160 }
161 }
167 {
168 get
169 {
170 return contentPositions.IndexOf(lerpTarget);
171 }
172 }
173 #endregion
174
175 #region Setup
176 private void Awake()
177 {
178 scrollRect = GetComponent<ScrollRect>();
179 scrollRectTransform = (RectTransform) scrollRect.transform;
180 contentTransform = scrollRect.content;
181
182 if (nextButton)
183 nextButton.GetComponent<Button>().onClick.AddListener(() => { NextItem(); });
184
185 if (prevButton)
186 prevButton.GetComponent<Button>().onClick.AddListener(() => { PreviousItem(); });
187
188 SetupDrivenTransforms();
189 SetupSnapScroll();
190 scrollRect.horizontalNormalizedPosition = 0;
191 _closestItem = 0;
193 }
194
195 private void OnDisable()
196 {
197 tracker.Clear();
198 }
199
200 private void SetupDrivenTransforms()
201 {
202 tracker = new DrivenRectTransformTracker();
203 tracker.Clear();
204
205 //So that we can calculate everything correctly
206 foreach (RectTransform child in contentTransform)
207 {
208 tracker.Add(this, child, DrivenTransformProperties.Anchors);
209
210 child.anchorMax = new Vector2(0, 1);
211 child.anchorMin = new Vector2(0, 1);
212 }
213 }
214
215 private void SetupSnapScroll()
216 {
217 if (ContentIsHorizonalLayoutGroup)
218 {
219 //because you can't get the anchored positions of UI elements
220 //when they are in a layout group (as far as I could tell)
221 SetupWithHorizontalLayoutGroup();
222 }
223 else
224 {
225 SetupWithCalculatedSpacing();
226 }
227 }
228
229 private void SetupWithHorizontalLayoutGroup()
230 {
231 HorizontalLayoutGroup horizLayoutGroup = contentTransform.GetComponent<HorizontalLayoutGroup>();
232 float childTotalWidths = 0;
233 int activeChildren = 0;
234 for (int i = 0; i < contentTransform.childCount; i++)
235 {
236 if (!ignoreInactiveItems || contentTransform.GetChild(i).gameObject.activeInHierarchy)
237 {
238 childTotalWidths += ((RectTransform)contentTransform.GetChild(i)).sizeDelta.x;
239 activeChildren++;
240 }
241 }
242 float spacingTotal = (activeChildren - 1) * horizLayoutGroup.spacing;
243 float totalWidth = childTotalWidths + spacingTotal + horizLayoutGroup.padding.left + horizLayoutGroup.padding.right;
244
245 contentTransform.sizeDelta = new Vector2(totalWidth, contentTransform.sizeDelta.y);
246 float scrollRectWidth = Mathf.Min(((RectTransform)contentTransform.GetChild(0)).sizeDelta.x, ((RectTransform)contentTransform.GetChild(contentTransform.childCount - 1)).sizeDelta.x);
247
248 /*---If the scroll view is set to stretch width this breaks stuff---*/
249 scrollRectTransform.sizeDelta = new Vector2(scrollRectWidth, scrollRectTransform.sizeDelta.y);
250
251 contentPositions = new List<Vector3>();
252 float widthOfScrollRect = scrollRectTransform.sizeDelta.x;
253 totalScrollableWidth = totalWidth - widthOfScrollRect;
254 float checkedChildrenTotalWidths = horizLayoutGroup.padding.left;
255 int activeChildrenBeforeSelf = 0;
256 for (int i = 0; i < contentTransform.childCount; i++)
257 {
258 if (!ignoreInactiveItems || contentTransform.GetChild(i).gameObject.activeInHierarchy)
259 {
260 float widthOfSelf = ((RectTransform)contentTransform.GetChild(i)).sizeDelta.x;
261 float offset = checkedChildrenTotalWidths + (horizLayoutGroup.spacing * activeChildrenBeforeSelf) + ((widthOfSelf - widthOfScrollRect) / 2);
262 scrollRect.horizontalNormalizedPosition = offset / totalScrollableWidth;
263 contentPositions.Add(contentTransform.localPosition);
264
265 checkedChildrenTotalWidths += widthOfSelf;
266 activeChildrenBeforeSelf++;
267 }
268 }
269 }
270
271 private void SetupWithCalculatedSpacing()
272 {
273 //we need them in order from left to right for pagination & buttons & our scrollRectWidth
274 List<RectTransform> childrenFromLeftToRight = new List<RectTransform>();
275 for (int i = 0; i < contentTransform.childCount; i++)
276 {
277 if (!ignoreInactiveItems || contentTransform.GetChild(i).gameObject.activeInHierarchy)
278 {
279 RectTransform childBeingSorted = ((RectTransform)contentTransform.GetChild(i));
280 int insertIndex = childrenFromLeftToRight.Count;
281 for (int j = 0; j < childrenFromLeftToRight.Count; j++)
282 {
283 if (DstFromTopLeftOfTransformToTopLeftOfParent(childBeingSorted).x < DstFromTopLeftOfTransformToTopLeftOfParent(childrenFromLeftToRight[j]).x)
284 {
285 insertIndex = j;
286 break;
287 }
288 }
289 childrenFromLeftToRight.Insert(insertIndex, childBeingSorted);
290 }
291 }
292 RectTransform childFurthestToTheRight = childrenFromLeftToRight[childrenFromLeftToRight.Count - 1];
293 float totalWidth = DstFromTopLeftOfTransformToTopLeftOfParent(childFurthestToTheRight).x + childFurthestToTheRight.sizeDelta.x;
294
295 contentTransform.sizeDelta = new Vector2(totalWidth, contentTransform.sizeDelta.y);
296 float scrollRectWidth = Mathf.Min(childrenFromLeftToRight[0].sizeDelta.x, childrenFromLeftToRight[childrenFromLeftToRight.Count - 1].sizeDelta.x);
297
298 // Note: sizeDelta will not be calculated properly if the scroll view is set to stretch width.
299 scrollRectTransform.sizeDelta = new Vector2(scrollRectWidth, scrollRectTransform.sizeDelta.y);
300
301 contentPositions = new List<Vector3>();
302 float widthOfScrollRect = scrollRectTransform.sizeDelta.x;
303 totalScrollableWidth = totalWidth - widthOfScrollRect;
304 for (int i = 0; i < childrenFromLeftToRight.Count; i++)
305 {
306 float offset = DstFromTopLeftOfTransformToTopLeftOfParent(childrenFromLeftToRight[i]).x + ((childrenFromLeftToRight[i].sizeDelta.x - widthOfScrollRect) / 2);
307 scrollRect.horizontalNormalizedPosition = offset / totalScrollableWidth;
308 contentPositions.Add(contentTransform.localPosition);
309 }
310 }
311 #endregion
312
313 #region Public Movement Functions
319 public void GoTo(MoveInfo info)
320 {
321 if (!Moving && info.index != ClosestItemIndex)
322 {
323 MovementStarted.Invoke();
324 }
325
326 if (info.indexType == MoveInfo.IndexType.childIndex)
327 {
328 mLerpTime = info.duration;
329 GoToChild(info.index, info.jump);
330 }
331 else if (info.indexType == MoveInfo.IndexType.positionIndex)
332 {
333 mLerpTime = info.duration;
334 GoToContentPos(info.index, info.jump);
335 }
336 }
337
338 private void GoToChild(int index, bool jump)
339 {
340 int clampedIndex = Mathf.Clamp(index, 0, contentPositions.Count - 1); //contentPositions amount == the amount of available children
341
342 if (ContentIsHorizonalLayoutGroup) //the contentPositions are in child order
343 {
344 lerpTarget = contentPositions[clampedIndex];
345 if (jump)
346 {
347 contentTransform.localPosition = lerpTarget;
348 }
349 else
350 {
351 StopMovement();
352 StartCoroutine("LerpToContent");
353 }
354 }
355 else //the contentPositions are in order from left -> right;
356 {
357 int availableChildIndex = 0; //an available child is one we can snap to
358 Vector3 previousContentTransformPos = contentTransform.localPosition;
359 for (int i = 0; i < contentTransform.childCount; i++)
360 {
361 if (!ignoreInactiveItems || contentTransform.GetChild(i).gameObject.activeInHierarchy)
362 {
363 if (availableChildIndex == clampedIndex)
364 {
365 RectTransform startChild = (RectTransform) contentTransform.GetChild(i);
366 float offset = DstFromTopLeftOfTransformToTopLeftOfParent(startChild).x + ((startChild.sizeDelta.x - scrollRectTransform.sizeDelta.x) / 2);
367 scrollRect.horizontalNormalizedPosition = offset / totalScrollableWidth;
368 lerpTarget = contentTransform.localPosition;
369 if (!jump)
370 {
371 contentTransform.localPosition = previousContentTransformPos;
372 StopMovement();
373 StartCoroutine("LerpToContent");
374 }
375 return;
376 }
377 availableChildIndex++;
378 }
379 }
380 }
381 }
382
383 private void GoToContentPos(int index, bool jump)
384 {
385 int clampedIndex = Mathf.Clamp(index, 0, contentPositions.Count - 1); //contentPositions amount == the amount of available children
386
387 //the content positions are all in order from left -> right
388 //which is what we want so there's no need to check
389
390 lerpTarget = contentPositions[clampedIndex];
391 if (jump)
392 {
393 contentTransform.localPosition = lerpTarget;
394 }
395 else
396 {
397 StopMovement();
398 StartCoroutine("LerpToContent");
399 }
400 }
401
406 public void NextItem()
407 {
408 int index;
409 if (Sliding)
410 {
411 index = ClosestItemIndex + 1;
412 }
413 else
414 {
415 index = LerpTargetIndex + 1;
416 }
417 MoveInfo info = new MoveInfo(MoveInfo.IndexType.positionIndex, index, jumpToItem, lerpTime);
418 GoTo(info);
419 }
420
425 public void PreviousItem()
426 {
427 int index;
428 if (Sliding)
429 {
430 index = ClosestItemIndex - 1;
431 }
432 else
433 {
434 index = LerpTargetIndex - 1;
435 }
436 MoveInfo info = new MoveInfo(MoveInfo.IndexType.positionIndex, index, jumpToItem, lerpTime);
437 GoTo(info);
438 }
439
443 public void UpdateLayout()
444 {
445 SetupDrivenTransforms();
446 SetupSnapScroll();
447 }
448
454 {
455 SetupDrivenTransforms();
456 SetupSnapScroll();
457 GoTo(info);
458 }
459 #endregion
460
461 #region Behind the Scenes Movement stuff
462 public void OnBeginDrag(PointerEventData ped)
463 {
464 StopMovement();
465 if (!Moving)
466 {
467 MovementStarted.Invoke();
468 }
469 }
470
471 public void OnEndDrag(PointerEventData ped)
472 {
473 StartCoroutine("SlideAndLerp");
474 }
475
476 private void Update()
477 {
478 if (_closestItem != ClosestItemIndex)
479 {
481 ChangePaginationInfo(ClosestItemIndex);
482 _closestItem = ClosestItemIndex;
483 }
484 }
485
486 private IEnumerator SlideAndLerp()
487 {
488 mSliding = true;
489 while (Mathf.Abs(scrollRect.velocity.x) > snappingVelocityThreshold)
490 {
491 yield return null;
492 }
493
494 lerpTarget = FindClosestFrom(contentTransform.localPosition);
496
497 while (Vector3.Distance(contentTransform.localPosition, lerpTarget) > 1)
498 {
499 contentTransform.localPosition = Vector3.Lerp(scrollRect.content.localPosition, lerpTarget, 7.5f * Time.deltaTime);
500 yield return null;
501 }
502 mSliding = false;
503 scrollRect.velocity = Vector2.zero;
504 contentTransform.localPosition = lerpTarget;
506 }
507
508 private IEnumerator LerpToContent()
509 {
511 mLerping = true;
512 Vector3 originalContentPos = contentTransform.localPosition;
513 float elapsedTime = 0;
514 while (elapsedTime < mLerpTime)
515 {
516 elapsedTime += Time.deltaTime;
517 contentTransform.localPosition = Vector3.Lerp(originalContentPos, lerpTarget, (elapsedTime / mLerpTime));
518 yield return null;
519 }
521 mLerping = false;
522 }
523 #endregion
524
525 private void StopMovement()
526 {
527 scrollRect.velocity = Vector2.zero;
528 StopCoroutine("SlideAndLerp");
529 StopCoroutine("LerpToContent");
530 }
531
532 private void ChangePaginationInfo(int targetScreen)
533 {
534 if (pagination)
535 for (int i = 0; i < pagination.transform.childCount; i++)
536 {
537 pagination.transform.GetChild(i).GetComponent<Toggle>().isOn = (targetScreen == i);
538 }
539 }
540
541 private Vector2 DstFromTopLeftOfTransformToTopLeftOfParent(RectTransform rt)
542 {
543 //gets rid of any pivot weirdness
544 return new Vector2(rt.anchoredPosition.x - (rt.sizeDelta.x * rt.pivot.x), rt.anchoredPosition.y + (rt.sizeDelta.y * (1 - rt.pivot.y)));
545 }
546
547 private Vector3 FindClosestFrom(Vector3 start)
548 {
549 Vector3 closest = Vector3.zero;
550 float distance = Mathf.Infinity;
551
552 foreach (Vector3 position in contentPositions)
553 {
554 if (Vector3.Distance(start, position) < distance)
555 {
556 distance = Vector3.Distance(start, position);
557 closest = position;
558 }
559 }
560 return closest;
561 }
562
563 [System.Serializable]
564 public struct MoveInfo
565 {
566 public enum IndexType { childIndex, positionIndex }
567 [Tooltip("Child Index means the Index corresponds to the content item at that index in the hierarchy.\n" +
568 "Position Index means the Index corresponds to the content item in that snap position.\n" +
569 "A higher Position Index in a Horizontal Scroll Snap means it would be further to the right.")]
571 [Tooltip("Zero based")]
572 public int index;
573 [Tooltip("If this is true the snap scroll will jump to the index, otherwise it will lerp there.")]
574 public bool jump;
575 [Tooltip("If jump is false this is the time it will take to lerp to the index")]
576 public float duration;
577
583 public MoveInfo(IndexType _indexType, int _index)
584 {
585 indexType = _indexType;
586 index = _index;
587 jump = true;
588 duration = 0;
589 }
590
598 public MoveInfo(IndexType _indexType, int _index, bool _jump, float _duration)
599 {
600 indexType = _indexType;
601 index = _index;
602 jump = _jump;
603 duration = _duration;
604 }
605 }
606 }
607}
UnityEngine.UI.Button Button
Definition: Pointer.cs:7
bool Lerping
Returns if the SnapScroll is moving programmatically
int ClosestItemIndex
Returns the closest item's index *Note this is zero based, and based on position not child order
void UpdateLayoutAndMoveTo(MoveInfo info)
Recalculates the size of the content & snap positions, and moves to a new item afterwards.
void GoTo(MoveInfo info)
Function for going to a specific screen. *Note the index is based on a zero-starting index.
void UpdateLayout()
Function for recalculating the size of the content & the snap positions, such as when you remove or a...
int LerpTargetIndex
Returns the lerpTarget's index *Note this is zero-based, and based on position not child order
bool Sliding
Returns if the SnapScroll is moving because of a touch
void PreviousItem()
Function for going to the previous item *Note the next item is the item to the left of the current it...
void NextItem()
Function for going to the next item *Note the next item is the item to the right of the current item,...
Credit Erdener Gonenc - @PixelEnvision.
MoveInfo(IndexType _indexType, int _index, bool _jump, float _duration)
Creates a MoveInfo
MoveInfo(IndexType _indexType, int _index)
Creates a MoveInfo that jumps to the index