| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676 |
- /// Credit Beka Westberg
- /// Sourced from - https://bitbucket.org/UnityUIExtensions/unity-ui-extensions/pull-requests/28
- /// Updated by SimonDarksideJ - Added some exception management and a SetNewItems to replace the content programmatically
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine.EventSystems;
- using UnityEngine.Events;
- using System;
- namespace UnityEngine.UI.Extensions
- {
- [ExecuteInEditMode]
- [RequireComponent(typeof(ScrollRect))]
- [AddComponentMenu("UI/Extensions/ContentSnapScrollHorizontal")]
- public class ContentScrollSnapHorizontal : MonoBehaviour, IBeginDragHandler, IEndDragHandler
- {
- [Serializable]
- public class StartMovementEvent : UnityEvent { }
- [Serializable]
- public class CurrentItemChangeEvent : UnityEvent<int> { }
- [Serializable]
- public class FoundItemToSnapToEvent : UnityEvent<int> { }
- [Serializable]
- public class SnappedToItemEvent : UnityEvent<int> { }
- public bool ignoreInactiveItems = true;
- public MoveInfo startInfo = new MoveInfo(MoveInfo.IndexType.positionIndex, 0);
- public GameObject prevButton;
- public GameObject nextButton;
- public GameObject pagination;
- [Tooltip("The velocity below which the scroll rect will start to snap")]
- public int snappingVelocityThreshold = 50;
- [Header("Paging Info")]
- [Tooltip("Should the pagination & buttons jump or lerp to the items")]
- public bool jumpToItem = false;
- [Tooltip("The time it will take for the pagination or buttons to move between items")]
- public float lerpTime = .3f;
- [Header("Events")]
- [SerializeField]
- [Tooltip("Event is triggered whenever the scroll rect starts to move, even when triggered programmatically")]
- private StartMovementEvent m_StartMovementEvent = new StartMovementEvent();
- public StartMovementEvent MovementStarted
- {
- get
- {
- return m_StartMovementEvent;
- }
- set
- {
- m_StartMovementEvent = value;
- }
- }
- [SerializeField]
- [Tooltip("Event is triggered whenever the closest item to the center of the scrollrect changes")]
- private CurrentItemChangeEvent m_CurrentItemChangeEvent = new CurrentItemChangeEvent();
- public CurrentItemChangeEvent CurrentItemChanged
- {
- get
- {
- return m_CurrentItemChangeEvent;
- }
- set
- {
- m_CurrentItemChangeEvent = value;
- }
- }
- [SerializeField]
- [Tooltip("Event is triggered when the ContentSnapScroll decides which item it is going to snap to. Returns the index of the closest position.")]
- private FoundItemToSnapToEvent m_FoundItemToSnapToEvent = new FoundItemToSnapToEvent();
- public FoundItemToSnapToEvent ItemFoundToSnap
- {
- get
- {
- return m_FoundItemToSnapToEvent;
- }
- set
- {
- m_FoundItemToSnapToEvent = value;
- }
- }
- [SerializeField]
- [Tooltip("Event is triggered when we finally settle on an element. Returns the index of the item's position.")]
- private SnappedToItemEvent m_SnappedToItemEvent = new SnappedToItemEvent();
- public SnappedToItemEvent ItemSnappedTo
- {
- get
- {
- return m_SnappedToItemEvent;
- }
- set
- {
- m_SnappedToItemEvent = value;
- }
- }
- private ScrollRect scrollRect = null;
- private RectTransform scrollRectTransform = null;
- private RectTransform contentTransform = null;
- private List<Vector3> contentPositions = new List<Vector3>();
- private Vector3 lerpTarget = Vector3.zero;
- private float totalScrollableWidth = 0;
- private DrivenRectTransformTracker tracker ;
- private float mLerpTime = 0;
- private int _closestItem = 0;
- private bool mSliding = false;
- private bool mLerping = false;
- private bool ContentIsHorizonalLayoutGroup
- {
- get
- {
- return contentTransform.GetComponent<HorizontalLayoutGroup>() != null;
- }
- }
- #region Public Info
- /// <summary>
- /// Returns if the SnapScroll is moving
- /// </summary>
- public bool Moving
- {
- get
- {
- return Sliding || Lerping;
- }
- }
- /// <summary>
- /// Returns if the SnapScroll is moving because of a touch
- /// </summary>
- public bool Sliding
- {
- get
- {
- return mSliding;
- }
- }
- /// <summary>
- /// Returns if the SnapScroll is moving programmatically
- /// </summary>
- public bool Lerping
- {
- get
- {
- return mLerping;
- }
- }
- /// <summary>
- /// Returns the closest item's index
- /// *Note this is zero based, and based on position not child order
- /// </summary>
- public int ClosestItemIndex
- {
- get
- {
- return contentPositions.IndexOf(FindClosestFrom(contentTransform.localPosition));
- }
- }
- /// <summary>
- /// Returns the lerpTarget's index
- /// *Note this is zero-based, and based on position not child order
- /// </summary>
- public int LerpTargetIndex
- {
- get
- {
- return contentPositions.IndexOf(lerpTarget);
- }
- }
- #endregion
- #region Setup
- private void Awake()
- {
- scrollRect = GetComponent<ScrollRect>();
- scrollRectTransform = (RectTransform) scrollRect.transform;
- contentTransform = scrollRect.content;
- if (nextButton)
- nextButton.GetComponent<Button>().onClick.AddListener(() => { NextItem(); });
- if (prevButton)
- prevButton.GetComponent<Button>().onClick.AddListener(() => { PreviousItem(); });
- if (IsScrollRectAvailable)
- {
- SetupDrivenTransforms();
- SetupSnapScroll();
- scrollRect.horizontalNormalizedPosition = 0;
- _closestItem = 0;
- GoTo(startInfo);
- }
- }
- public void SetNewItems(ref List<Transform> newItems)
- {
- if (scrollRect && contentTransform)
- {
- for (int i = scrollRect.content.childCount - 1; i >= 0; i--)
- {
- Transform child = contentTransform.GetChild(i);
- child.SetParent(null);
- GameObject.DestroyImmediate(child.gameObject);
- }
- foreach (Transform item in newItems)
- {
- GameObject newItem = item.gameObject;
- if (newItem.IsPrefab())
- {
- newItem = Instantiate(item.gameObject, contentTransform);
- }
- else
- {
- newItem.transform.SetParent(contentTransform);
- }
- }
- SetupDrivenTransforms();
- SetupSnapScroll();
- scrollRect.horizontalNormalizedPosition = 0;
- _closestItem = 0;
- GoTo(startInfo);
- }
- }
- private bool IsScrollRectAvailable
- {
- get
- {
- if (scrollRect &&
- contentTransform &&
- contentTransform.childCount > 0)
- {
- return true;
- }
- return false;
- }
- }
- private void OnDisable()
- {
- tracker.Clear();
- }
- private void SetupDrivenTransforms()
- {
- tracker = new DrivenRectTransformTracker();
- tracker.Clear();
- //So that we can calculate everything correctly
- foreach (RectTransform child in contentTransform)
- {
- tracker.Add(this, child, DrivenTransformProperties.Anchors);
- child.anchorMax = new Vector2(0, 1);
- child.anchorMin = new Vector2(0, 1);
- }
- }
- private void SetupSnapScroll()
- {
- if (ContentIsHorizonalLayoutGroup)
- {
- //because you can't get the anchored positions of UI elements
- //when they are in a layout group (as far as I could tell)
- SetupWithHorizontalLayoutGroup();
- }
- else
- {
- SetupWithCalculatedSpacing();
- }
- }
- private void SetupWithHorizontalLayoutGroup()
- {
- HorizontalLayoutGroup horizLayoutGroup = contentTransform.GetComponent<HorizontalLayoutGroup>();
- float childTotalWidths = 0;
- int activeChildren = 0;
- for (int i = 0; i < contentTransform.childCount; i++)
- {
- if (!ignoreInactiveItems || contentTransform.GetChild(i).gameObject.activeInHierarchy)
- {
- childTotalWidths += ((RectTransform)contentTransform.GetChild(i)).sizeDelta.x;
- activeChildren++;
- }
- }
- float spacingTotal = (activeChildren - 1) * horizLayoutGroup.spacing;
- float totalWidth = childTotalWidths + spacingTotal + horizLayoutGroup.padding.left + horizLayoutGroup.padding.right;
- contentTransform.sizeDelta = new Vector2(totalWidth, contentTransform.sizeDelta.y);
- float scrollRectWidth = Mathf.Min(((RectTransform)contentTransform.GetChild(0)).sizeDelta.x, ((RectTransform)contentTransform.GetChild(contentTransform.childCount - 1)).sizeDelta.x);
- /*---If the scroll view is set to stretch width this breaks stuff---*/
- scrollRectTransform.sizeDelta = new Vector2(scrollRectWidth, scrollRectTransform.sizeDelta.y);
- contentPositions = new List<Vector3>();
- float widthOfScrollRect = scrollRectTransform.sizeDelta.x;
- totalScrollableWidth = totalWidth - widthOfScrollRect;
- float checkedChildrenTotalWidths = horizLayoutGroup.padding.left;
- int activeChildrenBeforeSelf = 0;
- for (int i = 0; i < contentTransform.childCount; i++)
- {
- if (!ignoreInactiveItems || contentTransform.GetChild(i).gameObject.activeInHierarchy)
- {
- float widthOfSelf = ((RectTransform)contentTransform.GetChild(i)).sizeDelta.x;
- float offset = checkedChildrenTotalWidths + (horizLayoutGroup.spacing * activeChildrenBeforeSelf) + ((widthOfSelf - widthOfScrollRect) / 2);
- scrollRect.horizontalNormalizedPosition = offset / totalScrollableWidth;
- contentPositions.Add(contentTransform.localPosition);
- checkedChildrenTotalWidths += widthOfSelf;
- activeChildrenBeforeSelf++;
- }
- }
- }
- private void SetupWithCalculatedSpacing()
- {
- //we need them in order from left to right for pagination & buttons & our scrollRectWidth
- List<RectTransform> childrenFromLeftToRight = new List<RectTransform>();
- for (int i = 0; i < contentTransform.childCount; i++)
- {
- if (!ignoreInactiveItems || contentTransform.GetChild(i).gameObject.activeInHierarchy)
- {
- RectTransform childBeingSorted = ((RectTransform)contentTransform.GetChild(i));
- int insertIndex = childrenFromLeftToRight.Count;
- for (int j = 0; j < childrenFromLeftToRight.Count; j++)
- {
- if (DstFromTopLeftOfTransformToTopLeftOfParent(childBeingSorted).x < DstFromTopLeftOfTransformToTopLeftOfParent(childrenFromLeftToRight[j]).x)
- {
- insertIndex = j;
- break;
- }
- }
- childrenFromLeftToRight.Insert(insertIndex, childBeingSorted);
- }
- }
- RectTransform childFurthestToTheRight = childrenFromLeftToRight[childrenFromLeftToRight.Count - 1];
- float totalWidth = DstFromTopLeftOfTransformToTopLeftOfParent(childFurthestToTheRight).x + childFurthestToTheRight.sizeDelta.x;
- contentTransform.sizeDelta = new Vector2(totalWidth, contentTransform.sizeDelta.y);
- float scrollRectWidth = Mathf.Min(childrenFromLeftToRight[0].sizeDelta.x, childrenFromLeftToRight[childrenFromLeftToRight.Count - 1].sizeDelta.x);
- // Note: sizeDelta will not be calculated properly if the scroll view is set to stretch width.
- scrollRectTransform.sizeDelta = new Vector2(scrollRectWidth, scrollRectTransform.sizeDelta.y);
- contentPositions = new List<Vector3>();
- float widthOfScrollRect = scrollRectTransform.sizeDelta.x;
- totalScrollableWidth = totalWidth - widthOfScrollRect;
- for (int i = 0; i < childrenFromLeftToRight.Count; i++)
- {
- float offset = DstFromTopLeftOfTransformToTopLeftOfParent(childrenFromLeftToRight[i]).x + ((childrenFromLeftToRight[i].sizeDelta.x - widthOfScrollRect) / 2);
- scrollRect.horizontalNormalizedPosition = offset / totalScrollableWidth;
- contentPositions.Add(contentTransform.localPosition);
- }
- }
- #endregion
- #region Public Movement Functions
- /// <summary>
- /// Function for going to a specific screen.
- /// *Note the index is based on a zero-starting index.
- /// </summary>
- /// <param name="info">All of the info about how you want it to move</param>
- public void GoTo(MoveInfo info)
- {
- if (!Moving && info.index != ClosestItemIndex)
- {
- MovementStarted.Invoke();
- }
- if (info.indexType == MoveInfo.IndexType.childIndex)
- {
- mLerpTime = info.duration;
- GoToChild(info.index, info.jump);
- }
- else if (info.indexType == MoveInfo.IndexType.positionIndex)
- {
- mLerpTime = info.duration;
- GoToContentPos(info.index, info.jump);
- }
- }
- private void GoToChild(int index, bool jump)
- {
- int clampedIndex = Mathf.Clamp(index, 0, contentPositions.Count - 1); //contentPositions amount == the amount of available children
- if (ContentIsHorizonalLayoutGroup) //the contentPositions are in child order
- {
- lerpTarget = contentPositions[clampedIndex];
- if (jump)
- {
- contentTransform.localPosition = lerpTarget;
- }
- else
- {
- StopMovement();
- StartCoroutine("LerpToContent");
- }
- }
- else //the contentPositions are in order from left -> right;
- {
- int availableChildIndex = 0; //an available child is one we can snap to
- Vector3 previousContentTransformPos = contentTransform.localPosition;
- for (int i = 0; i < contentTransform.childCount; i++)
- {
- if (!ignoreInactiveItems || contentTransform.GetChild(i).gameObject.activeInHierarchy)
- {
- if (availableChildIndex == clampedIndex)
- {
- RectTransform startChild = (RectTransform) contentTransform.GetChild(i);
- float offset = DstFromTopLeftOfTransformToTopLeftOfParent(startChild).x + ((startChild.sizeDelta.x - scrollRectTransform.sizeDelta.x) / 2);
- scrollRect.horizontalNormalizedPosition = offset / totalScrollableWidth;
- lerpTarget = contentTransform.localPosition;
- if (!jump)
- {
- contentTransform.localPosition = previousContentTransformPos;
- StopMovement();
- StartCoroutine("LerpToContent");
- }
- return;
- }
- availableChildIndex++;
- }
- }
- }
- }
- private void GoToContentPos(int index, bool jump)
- {
- int clampedIndex = Mathf.Clamp(index, 0, contentPositions.Count - 1); //contentPositions amount == the amount of available children
- //the content positions are all in order from left -> right
- //which is what we want so there's no need to check
- lerpTarget = contentPositions[clampedIndex];
- if (jump)
- {
- contentTransform.localPosition = lerpTarget;
- }
- else
- {
- StopMovement();
- StartCoroutine("LerpToContent");
- }
- }
- /// <summary>
- /// Function for going to the next item
- /// *Note the next item is the item to the right of the current item, this is not based on child order
- /// </summary>
- public void NextItem()
- {
- int index;
- if (Sliding)
- {
- index = ClosestItemIndex + 1;
- }
- else
- {
- index = LerpTargetIndex + 1;
- }
- MoveInfo info = new MoveInfo(MoveInfo.IndexType.positionIndex, index, jumpToItem, lerpTime);
- GoTo(info);
- }
- /// <summary>
- /// Function for going to the previous item
- /// *Note the next item is the item to the left of the current item, this is not based on child order
- /// </summary>
- public void PreviousItem()
- {
- int index;
- if (Sliding)
- {
- index = ClosestItemIndex - 1;
- }
- else
- {
- index = LerpTargetIndex - 1;
- }
- MoveInfo info = new MoveInfo(MoveInfo.IndexType.positionIndex, index, jumpToItem, lerpTime);
- GoTo(info);
- }
- /// <summary>
- /// Function for recalculating the size of the content & the snap positions, such as when you remove or add a child
- /// </summary>
- public void UpdateLayout()
- {
- SetupDrivenTransforms();
- SetupSnapScroll();
- }
- /// <summary>
- /// Recalculates the size of the content & snap positions, and moves to a new item afterwards.
- /// </summary>
- /// <param name="info">All of the info about how you want it to move</param>
- public void UpdateLayoutAndMoveTo(MoveInfo info)
- {
- SetupDrivenTransforms();
- SetupSnapScroll();
- GoTo(info);
- }
- #endregion
- #region Behind the Scenes Movement stuff
- public void OnBeginDrag(PointerEventData ped)
- {
- if (contentPositions.Count < 2)
- {
- return;
- }
- StopMovement();
- if (!Moving)
- {
- MovementStarted.Invoke();
- }
- }
- public void OnEndDrag(PointerEventData ped)
- {
- if (contentPositions.Count <= 1)
- {
- return;
- }
- if (IsScrollRectAvailable)
- {
- StartCoroutine("SlideAndLerp");
- }
- }
- private void Update()
- {
- if (IsScrollRectAvailable)
- {
- if (_closestItem != ClosestItemIndex)
- {
- CurrentItemChanged.Invoke(ClosestItemIndex);
- ChangePaginationInfo(ClosestItemIndex);
- _closestItem = ClosestItemIndex;
- }
- }
- }
- private IEnumerator SlideAndLerp()
- {
- mSliding = true;
- while (Mathf.Abs(scrollRect.velocity.x) > snappingVelocityThreshold)
- {
- yield return null;
- }
- lerpTarget = FindClosestFrom(contentTransform.localPosition);
- ItemFoundToSnap.Invoke(LerpTargetIndex);
- while (Vector3.Distance(contentTransform.localPosition, lerpTarget) > 1)
- {
- contentTransform.localPosition = Vector3.Lerp(scrollRect.content.localPosition, lerpTarget, 7.5f * Time.deltaTime);
- yield return null;
- }
- mSliding = false;
- scrollRect.velocity = Vector2.zero;
- contentTransform.localPosition = lerpTarget;
- ItemSnappedTo.Invoke(LerpTargetIndex);
- }
- private IEnumerator LerpToContent()
- {
- ItemFoundToSnap.Invoke(LerpTargetIndex);
- mLerping = true;
- Vector3 originalContentPos = contentTransform.localPosition;
- float elapsedTime = 0;
- while (elapsedTime < mLerpTime)
- {
- elapsedTime += Time.deltaTime;
- contentTransform.localPosition = Vector3.Lerp(originalContentPos, lerpTarget, (elapsedTime / mLerpTime));
- yield return null;
- }
- ItemSnappedTo.Invoke(LerpTargetIndex);
- mLerping = false;
- }
- #endregion
- private void StopMovement()
- {
- scrollRect.velocity = Vector2.zero;
- StopCoroutine("SlideAndLerp");
- StopCoroutine("LerpToContent");
- }
- private void ChangePaginationInfo(int targetScreen)
- {
- if (pagination)
- for (int i = 0; i < pagination.transform.childCount; i++)
- {
- pagination.transform.GetChild(i).GetComponent<Toggle>().isOn = (targetScreen == i);
- }
- }
- private Vector2 DstFromTopLeftOfTransformToTopLeftOfParent(RectTransform rt)
- {
- //gets rid of any pivot weirdness
- return new Vector2(rt.anchoredPosition.x - (rt.sizeDelta.x * rt.pivot.x), rt.anchoredPosition.y + (rt.sizeDelta.y * (1 - rt.pivot.y)));
- }
- private Vector3 FindClosestFrom(Vector3 start)
- {
- Vector3 closest = Vector3.zero;
- float distance = Mathf.Infinity;
- foreach (Vector3 position in contentPositions)
- {
- if (Vector3.Distance(start, position) < distance)
- {
- distance = Vector3.Distance(start, position);
- closest = position;
- }
- }
- return closest;
- }
- [System.Serializable]
- public struct MoveInfo
- {
- public enum IndexType { childIndex, positionIndex }
- [Tooltip("Child Index means the Index corresponds to the content item at that index in the hierarchy.\n" +
- "Position Index means the Index corresponds to the content item in that snap position.\n" +
- "A higher Position Index in a Horizontal Scroll Snap means it would be further to the right.")]
- public IndexType indexType;
- [Tooltip("Zero based")]
- public int index;
- [Tooltip("If this is true the snap scroll will jump to the index, otherwise it will lerp there.")]
- public bool jump;
- [Tooltip("If jump is false this is the time it will take to lerp to the index")]
- public float duration;
- /// <summary>
- /// Creates a MoveInfo that jumps to the index
- /// </summary>
- /// <param name="_indexType">Whether you want to get the child at the index or the snap position at the index</param>
- /// <param name="_index">Where you want it to jump</param>
- public MoveInfo(IndexType _indexType, int _index)
- {
- indexType = _indexType;
- index = _index;
- jump = true;
- duration = 0;
- }
- /// <summary>
- /// Creates a MoveInfo
- /// </summary>
- /// <param name="_indexType">Whether you want to get the child at the index or the snap position at the index</param>
- /// <param name="_index">Where you want it to jump</param>
- /// <param name="_jump">Whether you want it to jump or lerp to the index</param>
- /// <param name="_duration">How long it takes to lerp to the index</param>
- public MoveInfo(IndexType _indexType, int _index, bool _jump, float _duration)
- {
- indexType = _indexType;
- index = _index;
- jump = _jump;
- duration = _duration;
- }
- }
- }
- }
|