/// Credit BinaryX /// Sourced from - http://forum.unity3d.com/threads/scripts-useful-4-6-scripts-collection.264161/page-2#post-1945602 /// Updated by ddreaper - removed dependency on a custom ScrollRect script. Now implements drag interfaces and standard Scroll Rect. /// Update by xesenix - rewrote almost the entire code /// - configuration for direction move instead of 2 concurrent class (easier to change direction in editor) /// - supports list layout with horizontal or vertical layout need to match direction with type of layout used /// - dynamic checks if scrolled list size changes and recalculates anchor positions /// and item size based on itemsVisibleAtOnce and size of root container /// if you don't wish to use this auto resize turn of autoLayoutItems /// - fixed current page made it independent from pivot /// - replaced pagination with delegate function using System; using UnityEngine.EventSystems; namespace UnityEngine.UI.Extensions { [ExecuteInEditMode] [RequireComponent(typeof(ScrollRect))] [AddComponentMenu("UI/Extensions/Scroll Snap")] public class ScrollSnap : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler { // needed because of reversed behaviour of axis Y compared to X // (positions of children lower in children list in horizontal directions grows when in vertical it gets smaller) public enum ScrollDirection { Horizontal, Vertical } public delegate void PageSnapChange(int page); public event PageSnapChange onPageChange; public ScrollDirection direction = ScrollDirection.Horizontal; protected ScrollRect scrollRect; protected RectTransform scrollRectTransform; protected Transform listContainerTransform; protected RectTransform rectTransform; int pages; protected int startingPage = 0; // anchor points to lerp to to see child on certain indexes protected Vector3[] pageAnchorPositions; protected Vector3 lerpTarget; protected bool lerp; // item list related protected float listContainerMinPosition; protected float listContainerMaxPosition; protected float listContainerSize; protected RectTransform listContainerRectTransform; protected Vector2 listContainerCachedSize; protected float itemSize; protected int itemsCount = 0; [Tooltip("Button to go to the next page. (optional)")] public Button nextButton; [Tooltip("Button to go to the previous page. (optional)")] public Button prevButton; [Tooltip("Number of items visible in one page of scroll frame.")] [RangeAttribute(1, 100)] public int itemsVisibleAtOnce = 1; [Tooltip("Sets minimum width of list items to 1/itemsVisibleAtOnce.")] public bool autoLayoutItems = true; [Tooltip("If you wish to update scrollbar numberOfSteps to number of active children on list.")] public bool linkScrolbarSteps = false; [Tooltip("If you wish to update scrollrect sensitivity to size of list element.")] public bool linkScrolrectScrollSensitivity = false; public Boolean useFastSwipe = true; public int fastSwipeThreshold = 100; // drag related protected bool startDrag = true; protected Vector3 positionOnDragStart = new Vector3(); protected int pageOnDragStart; protected bool fastSwipeTimer = false; protected int fastSwipeCounter = 0; protected int fastSwipeTarget = 10; // Use this for initialization void Awake() { lerp = false; scrollRect = gameObject.GetComponent<ScrollRect>(); scrollRectTransform = gameObject.GetComponent<RectTransform>(); listContainerTransform = scrollRect.content; listContainerRectTransform = listContainerTransform.GetComponent<RectTransform>(); rectTransform = listContainerTransform.gameObject.GetComponent<RectTransform>(); UpdateListItemsSize(); UpdateListItemPositions(); PageChanged(CurrentPage()); if (nextButton) { nextButton.GetComponent<Button>().onClick.AddListener(() => { NextScreen(); }); } if (prevButton) { prevButton.GetComponent<Button>().onClick.AddListener(() => { PreviousScreen(); }); } } void Start() { Awake(); } public void UpdateListItemsSize() { float size = 0; float currentSize = 0; if (direction == ScrollSnap.ScrollDirection.Horizontal) { size = scrollRectTransform.rect.width / itemsVisibleAtOnce; currentSize = listContainerRectTransform.rect.width / itemsCount; } else { size = scrollRectTransform.rect.height / itemsVisibleAtOnce; currentSize = listContainerRectTransform.rect.height / itemsCount; } itemSize = size; if (linkScrolrectScrollSensitivity) { scrollRect.scrollSensitivity = itemSize; } if (autoLayoutItems && currentSize != size && itemsCount > 0) { if (direction == ScrollSnap.ScrollDirection.Horizontal) { foreach (var tr in listContainerTransform) { GameObject child = ((Transform)tr).gameObject; if (child.activeInHierarchy) { var childLayout = child.GetComponent<LayoutElement>(); if (childLayout == null) { childLayout = child.AddComponent<LayoutElement>(); } childLayout.minWidth = itemSize; } } } else { foreach (var tr in listContainerTransform) { GameObject child = ((Transform)tr).gameObject; if (child.activeInHierarchy) { var childLayout = child.GetComponent<LayoutElement>(); if (childLayout == null) { childLayout = child.AddComponent<LayoutElement>(); } childLayout.minHeight = itemSize; } } } } } public void UpdateListItemPositions() { if (!listContainerRectTransform.rect.size.Equals(listContainerCachedSize)) { // checking how many children of list are active int activeCount = 0; foreach (var tr in listContainerTransform) { if (((Transform)tr).gameObject.activeInHierarchy) { activeCount++; } } // if anything changed since last check reinitialize anchors list itemsCount = 0; Array.Resize(ref pageAnchorPositions, activeCount); if (activeCount > 0) { pages = Mathf.Max(activeCount - itemsVisibleAtOnce + 1, 1); if (direction == ScrollDirection.Horizontal) { // looking for list spanning range min/max scrollRect.horizontalNormalizedPosition = 0; listContainerMaxPosition = listContainerTransform.localPosition.x; scrollRect.horizontalNormalizedPosition = 1; listContainerMinPosition = listContainerTransform.localPosition.x; listContainerSize = listContainerMaxPosition - listContainerMinPosition; for (var i = 0; i < pages; i++) { pageAnchorPositions[i] = new Vector3( listContainerMaxPosition - itemSize * i, listContainerTransform.localPosition.y, listContainerTransform.localPosition.z ); } } else { //Debug.Log ("-------------looking for list spanning range----------------"); // looking for list spanning range scrollRect.verticalNormalizedPosition = 1; listContainerMinPosition = listContainerTransform.localPosition.y; scrollRect.verticalNormalizedPosition = 0; listContainerMaxPosition = listContainerTransform.localPosition.y; listContainerSize = listContainerMaxPosition - listContainerMinPosition; for (var i = 0; i < pages; i++) { pageAnchorPositions[i] = new Vector3( listContainerTransform.localPosition.x, listContainerMinPosition + itemSize * i, listContainerTransform.localPosition.z ); } } UpdateScrollbar(linkScrolbarSteps); startingPage = Mathf.Min(startingPage, pages); ResetPage(); } if (itemsCount != activeCount) { PageChanged(CurrentPage()); } itemsCount = activeCount; listContainerCachedSize.Set(listContainerRectTransform.rect.size.x, listContainerRectTransform.rect.size.y); } } public void ResetPage() { if (direction == ScrollDirection.Horizontal) { scrollRect.horizontalNormalizedPosition = pages > 1 ? (float)startingPage / (float)(pages - 1) : 0; } else { scrollRect.verticalNormalizedPosition = pages > 1 ? (float)(pages - startingPage - 1) / (float)(pages - 1) : 0; } } protected void UpdateScrollbar(bool linkSteps) { if (linkSteps) { if (direction == ScrollDirection.Horizontal) { if (scrollRect.horizontalScrollbar != null) { scrollRect.horizontalScrollbar.numberOfSteps = pages; } } else { if (scrollRect.verticalScrollbar != null) { scrollRect.verticalScrollbar.numberOfSteps = pages; } } } else { if (direction == ScrollDirection.Horizontal) { if (scrollRect.horizontalScrollbar != null) { scrollRect.horizontalScrollbar.numberOfSteps = 0; } } else { if (scrollRect.verticalScrollbar != null) { scrollRect.verticalScrollbar.numberOfSteps = 0; } } } } void LateUpdate() { UpdateListItemsSize(); UpdateListItemPositions(); if (lerp) { UpdateScrollbar(false); listContainerTransform.localPosition = Vector3.Lerp(listContainerTransform.localPosition, lerpTarget, 7.5f * Time.deltaTime); if (Vector3.Distance(listContainerTransform.localPosition, lerpTarget) < 0.001f) { listContainerTransform.localPosition = lerpTarget; lerp = false; UpdateScrollbar(linkScrolbarSteps); } //change the info bullets at the bottom of the screen. Just for visual effect if (Vector3.Distance(listContainerTransform.localPosition, lerpTarget) < 10f) { PageChanged(CurrentPage()); } } if (fastSwipeTimer) { fastSwipeCounter++; } } private bool fastSwipe = false; //to determine if a fast swipe was performed //Function for switching screens with buttons public void NextScreen() { UpdateListItemPositions(); if (CurrentPage() < pages - 1) { lerp = true; lerpTarget = pageAnchorPositions[CurrentPage() + 1]; PageChanged(CurrentPage() + 1); } } //Function for switching screens with buttons public void PreviousScreen() { UpdateListItemPositions(); if (CurrentPage() > 0) { lerp = true; lerpTarget = pageAnchorPositions[CurrentPage() - 1]; PageChanged(CurrentPage() - 1); } } //Because the CurrentScreen function is not so reliable, these are the functions used for swipes private void NextScreenCommand() { if (pageOnDragStart < pages - 1) { int targetPage = Mathf.Min(pages - 1, pageOnDragStart + itemsVisibleAtOnce); lerp = true; lerpTarget = pageAnchorPositions[targetPage]; PageChanged(targetPage); } } //Because the CurrentScreen function is not so reliable, these are the functions used for swipes private void PrevScreenCommand() { if (pageOnDragStart > 0) { int targetPage = Mathf.Max(0, pageOnDragStart - itemsVisibleAtOnce); lerp = true; lerpTarget = pageAnchorPositions[targetPage]; PageChanged(targetPage); } } //returns the current screen that the is seeing public int CurrentPage() { float pos; if (direction == ScrollDirection.Horizontal) { pos = listContainerMaxPosition - listContainerTransform.localPosition.x; pos = Mathf.Clamp(pos, 0, listContainerSize); } else { pos = listContainerTransform.localPosition.y - listContainerMinPosition; pos = Mathf.Clamp(pos, 0, listContainerSize); } float page = pos / itemSize; return Mathf.Clamp(Mathf.RoundToInt(page), 0, pages); } public void ChangePage(int page) { if (0 <= page && page < pages) { lerp = true; lerpTarget = pageAnchorPositions[page]; PageChanged(page); } } //changes the bullets on the bottom of the page - pagination private void PageChanged(int currentPage) { startingPage = currentPage; if (nextButton) { nextButton.interactable = currentPage < pages - 1; } if (prevButton) { prevButton.interactable = currentPage > 0; } if (onPageChange != null) { onPageChange(currentPage); } } #region Interfaces public void OnBeginDrag(PointerEventData eventData) { UpdateScrollbar(false); fastSwipeCounter = 0; fastSwipeTimer = true; positionOnDragStart = eventData.position; pageOnDragStart = CurrentPage(); } public void OnEndDrag(PointerEventData eventData) { startDrag = true; float change = 0; if (direction == ScrollDirection.Horizontal) { change = positionOnDragStart.x - eventData.position.x; } else { change = -positionOnDragStart.y + eventData.position.y; } if (useFastSwipe) { fastSwipe = false; fastSwipeTimer = false; if (fastSwipeCounter <= fastSwipeTarget) { if (Math.Abs(change) > fastSwipeThreshold) { fastSwipe = true; } } if (fastSwipe) { if (change > 0) { NextScreenCommand(); } else { PrevScreenCommand(); } } else { lerp = true; lerpTarget = pageAnchorPositions[CurrentPage()]; } } else { lerp = true; lerpTarget = pageAnchorPositions[CurrentPage()]; } } public void OnDrag(PointerEventData eventData) { lerp = false; if (startDrag) { OnBeginDrag(eventData); startDrag = false; } } #endregion } }