Can a Scroll Rect snap to elements?

Hi unity people… I would like to use the new gui to make a UI where the users swipe to scroll from one full screen page to another. Is it possible to make the Scroll Rect snap to the individual page, so the page will be smoothly animated into a centered position once you stop dragging.

A fully working version of this is n the UI Extensions repo on BitBucket, complete with button support :smiley:
https://bitbucket.org/ddreaper/unity-ui-extensions/src/6928c4428fb3392f0e9735df44aafee3b347933c/Scripts/HorizontalScrollSnap.cs?at=default

I made this for the snap. It’s not very pretty but works to some extent.
Anyway my conclusion is that I should be able to do this with a Scroll bar with steps in it. Making the scroll rect react according to it’s defined elasticity property. I will be sending this as a bug/feature request to the Unity team.

Anyway, here it is the code. Add this component next to your scrollRect:

ScrollRectSnap.cs

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class ScrollRectSnap : MonoBehaviour {

	float[] points;
	[Tooltip("how many screens or pages are there within the content (steps)")]
	public int screens = 1;
	float stepSize;

	ScrollRect scroll;
	bool LerpH;
	float targetH;
	[Tooltip("Snap horizontally")]
	public bool snapInH = true;

	bool LerpV;
	float targetV;
	[Tooltip("Snap vertically")]
	public bool snapInV = true;

	// Use this for initialization
	void Start ()
	{
		scroll = gameObject.GetComponent<ScrollRect>();
		scroll.intertia = false;

		if(screens > 0)
		{
			points = new float[screens];
			stepSize = 1/(float)(screens-1);
		
			for(int i = 0; i < screens; i++)
			{
				points _= i * stepSize;_
  •  	}*
    
  •  }*
    
  •  else*
    
  •  {*
    
  •  	points[0] = 0;*
    
  •  }*
    
  • }*

  • void Update()*

  • {*

  •  if(LerpH)*
    
  •  {*
    

_ scroll.horizontalNormalizedPosition = Mathf.Lerp( scroll.horizontalNormalizedPosition, targetH, 10scroll.elasticityTime.deltaTime);_

  •  	if(Mathf.Approximately(scroll.horizontalNormalizedPosition, targetH)) LerpH = false;			*
    
  •  }*
    
  •  if(LerpV)*
    
  •  {*
    

_ scroll.verticalNormalizedPosition = Mathf.Lerp( scroll.verticalNormalizedPosition, targetV, 10scroll.elasticityTime.deltaTime);_

  •  	if(Mathf.Approximately(scroll.verticalNormalizedPosition, targetV)) LerpV = false;			*
    
  •  }*
    
  • }*

  • public void DragEnd()*

  • {*

  •  if(scroll.horizontal && snapInH)*
    
  •  {*
    
  •  	targetH = points[FindNearest(scroll.horizontalNormalizedPosition, points)];*
    
  •  	LerpH = true;*
    
  •  }*
    
  •  if(scroll.vertical && snapInV)*
    
  •  {*
    
  •  	targetH = points[FindNearest(scroll.verticalNormalizedPosition, points)];*
    
  •  	LerpH = true;*
    
  •  }*
    
  • }*

  • public void OnDrag()*

  • {*

  •  LerpH = false;*
    
  •  LerpV = false;*
    
  • }*

  • int FindNearest(float f, float array)*

  • {*

  •  float distance = Mathf.Infinity;*
    
  •  int output = 0;*
    
  •  for(int index = 0; index < array.Length; index++)*
    
  •  {*
    
  •  	if(Mathf.Abs(array[index]-f) < distance)*
    
  •  	{*
    
  •  		distance = Mathf.Abs(array[index]-f);*
    
  •  		output = index;*
    
  •  	}*
    
  •  }*
    
  •  return output;	*
    
  • }*
    }

and you also need to add an event trigger:
[32380-eventtrigger.jpg|32380]*
*
- On Drag call “OnDrag()”
- On PointerUp call “DragEnd()”
Hope it helps. Although it would be better if they officially implement it.

So my attempt at this only supports 1 direction at a time, cause this is what we only do, we never do any grids or anything. I also added an animation curve and switched to using a coroutine instead of the update function. As well as using the interfaces instead of the events thing. I also added a Reset function that will auto find the scroll rect ( this is a common theme for my projects, it is really nice to have ). I fully commented the whole script and it is really easy to use with even a couple of error checks ( what will probably be the common mess ups ). Modifying it to use both vertical and horizontal wont be too hard, but is not something we ever use. With all these points I feel this is a very efficient and clean way of doing the snapping. I may add slight improvements / suggestions as well, but for 20 minutes and no starting point ( I wrote it from scratch ) it is not that bad.

using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

/// <summary>
/// Snap a scroll rect to its children items. All self contained.
/// Note: Only supports 1 direction
/// </summary>
public class DragSnapper : UIBehaviour, IEndDragHandler, IBeginDragHandler
{
    public ScrollRect scrollRect; // the scroll rect to scroll
    public SnapDirection direction; // the direction we are scrolling
    public int itemCount; // how many items we have in our scroll rect

    public AnimationCurve curve = AnimationCurve.Linear(0f, 0f, 1f, 1f); // a curve for transitioning in order to give it a little bit of extra polish
    public float speed; // the speed in which we snap ( normalized position per second? )

    protected override void Reset()
    {
        base.Reset();

        if (scrollRect == null) // if we are resetting or attaching our script, try and find a scroll rect for convenience 
            scrollRect = GetComponent<ScrollRect>();
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        StopCoroutine(SnapRect()); // if we are snapping, stop for the next input
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        StartCoroutine(SnapRect()); // simply start our coroutine ( better than using update )
    }

    private IEnumerator SnapRect()
    {
        if (scrollRect == null)
            throw new System.Exception("Scroll Rect can not be null");
        if (itemCount == 0)
            throw new System.Exception("Item count can not be zero");

        float startNormal = direction == SnapDirection.Horizontal ? scrollRect.horizontalNormalizedPosition : scrollRect.verticalNormalizedPosition; // find our start position
        float delta = 1f / (float)(itemCount - 1); // percentage each item takes
        int target = Mathf.RoundToInt(startNormal / delta); // this finds us the closest target based on our starting point
        float endNormal = delta * target; // this finds the normalized value of our target
        float duration = Mathf.Abs((endNormal - startNormal) / speed); // this calculates the time it takes based on our speed to get to our target

        float timer = 0f; // timer value of course
        while (timer < 1f) // loop until we are done
        {
            timer = Mathf.Min(1f, timer + Time.deltaTime / duration); // calculate our timer based on our speed
            float value = Mathf.Lerp(startNormal, endNormal, curve.Evaluate(timer)); // our value based on our animation curve, cause linear is lame

            if (direction == SnapDirection.Horizontal) // depending on direction we set our horizontal or vertical position
                scrollRect.horizontalNormalizedPosition = value;
            else
                scrollRect.verticalNormalizedPosition = value;

            yield return new WaitForEndOfFrame(); // wait until next frame
        }
    }
}

// The direction we are snapping in
public enum SnapDirection
{
    Horizontal,
    Vertical,
}

Good evening folks,

I was thinking about building a system exactly like this myself, but a quick use of the googles brought me to you lovely people and saved me a couple of hours’ work. I’ve since had a tinker with cmberryau’s script in an attempt to get it feeling slightly more springy and gesture-based - similar to the feel of the home screen on an iPad. While you can still change screens by dragging until the next panel is the one that fills the screen, you can now also change panels with a cute little flick.

Set up the Drag and PointerUp events as with the previous version of the script. Though I haven’t experimented too extensively with them, my recommended settings are:

  • elasticity: 0.1

  • snap speed: 10

  • inertia cutoff magnitude: 800 <— this is what gives it the snappiness

    using UnityEngine;
    using System.Collections;
    using UnityEngine.UI;

    public class ScrollRectSnap : MonoBehaviour
    {

     float[] points;
     [Tooltip("how many screens or pages are there within the content (steps)")]
     public int screens = 1;
     [Tooltip("How quickly the GUI snaps to each panel")]
     public float snapSpeed;
     public float inertiaCutoffMagnitude;
     float stepSize;
    
     ScrollRect scroll;
     bool LerpH;
     float targetH;
     [Tooltip("Snap horizontally")]
     public bool snapInH = true;
    
     bool LerpV;
     float targetV;
     [Tooltip("Snap vertically")]
     public bool snapInV = true;
    
     bool dragInit = true;
     int dragStartNearest;
    
     // Use this for initialization
     void Start()
     {
         scroll = gameObject.GetComponent<ScrollRect>();
         scroll.inertia = true;
    
         if (screens > 0)
         {
             points = new float[screens];
             stepSize = 1 / (float)(screens - 1);
    
             for (int i = 0; i < screens; i++)
             {
                 points _= i * stepSize;_
    

}
}
else
{
points[0] = 0;
}
}

void Update()
{
if (LerpH)
{
scroll.horizontalNormalizedPosition = Mathf.Lerp(scroll.horizontalNormalizedPosition, targetH, snapSpeed * Time.deltaTime);
if (Mathf.Approximately(scroll.horizontalNormalizedPosition, targetH)) LerpH = false;
}
if (LerpV)
{
scroll.verticalNormalizedPosition = Mathf.Lerp(scroll.verticalNormalizedPosition, targetV, snapSpeed * Time.deltaTime);
if (Mathf.Approximately(scroll.verticalNormalizedPosition, targetV)) LerpV = false;
}
}

public void DragEnd()
{
int target = FindNearest(scroll.horizontalNormalizedPosition, points);

if (target == dragStartNearest && scroll.velocity.sqrMagnitude > inertiaCutoffMagnitude * inertiaCutoffMagnitude)
{
if (scroll.velocity.x < 0)
{
target = dragStartNearest + 1;
}
else if (scroll.velocity.x > 1)
{
target = dragStartNearest - 1;
}
target = Mathf.Clamp(target, 0, points.Length - 1);
}

if (scroll.horizontal && snapInH && scroll.horizontalNormalizedPosition > 0f && scroll.horizontalNormalizedPosition < 1f)
{
targetH = points[target];
LerpH = true;
}
if (scroll.vertical && snapInV && scroll.verticalNormalizedPosition > 0f && scroll.verticalNormalizedPosition < 1f)
{
targetH = points[target];
LerpH = true;
}

dragInit = true;
}

public void OnDrag()
{
if (dragInit)
{
dragStartNearest = FindNearest(scroll.horizontalNormalizedPosition, points);
dragInit = false;
}

LerpH = false;
LerpV = false;
}

int FindNearest(float f, float[] array)
{
float distance = Mathf.Infinity;
int output = 0;
for (int index = 0; index < array.Length; index++)
{
if (Mathf.Abs(array[index] - f) < distance)
{
distance = Mathf.Abs(array[index] - f);
output = index;
}
}
return output;
}
}
Hopefully you can now get your menus feeling buttery-iOS smooth too :slight_smile:

So a note to everyone who posted code here.

DON’T USE EVENT TRIGGER :slight_smile:

Unity’s input system is based on interfaces. If you want drag events just inherit from the correct interfaces all you get all the input with all the details of the events.

For example:

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class MyDragRec :  UIBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler
{
	#region IBeginDragHandler
		public void OnBeginDrag (PointerEventData eventData)
		{
			//MAGIC INTERFACES!
		}
	#endregion

	#region IEndDragHandler
		void OnEndDrag (PointerEventData eventData)
		{
			//Since I use the Interface I get the function callbacks 
		}
	#endregion

	#region IDragHandler
		public void OnDrag (PointerEventData eventData)
		{
			//The more you know!
		}
	#endregion
}

You are not going to have a good time

Using EventTrigger is not a good way at all. It eats all input from every source and this can lead to a lot of input issues down the road. Don’t trust me look at the code below.

namespace UnityEngine.EventSystems
{
	[AddComponentMenu("Event/Event Trigger")]
	public class EventTrigger : MonoBehaviour, IEventSystemHandler, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler, IDragHandler, IDropHandler, IScrollHandler, IUpdateSelectedHandler, ISelectHandler, IDeselectHandler, IMoveHandler
	{
		[Serializable]
		public class TriggerEvent : UnityEvent<BaseEventData>
		{
		}
... blah, blah, blah

Notice all the interfaces (scroll to the right)?

Hi there!

I found a very easy way to solve the item snapping on a ScrollRect.
In my case I use DOTween (free on the Asset Store) to animate the snapping value.

Hope it helps!

using System;
using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[RequireComponent(typeof(ScrollRect))]
public class SnapScrollRect : MonoBehaviour, IBeginDragHandler, IEndDragHandler {

    public event Action<int> OnItemCentered = delegate { };

    public float time = 1f;
    public Ease ease = Ease.OutExpo;

    private ScrollRect scrollRect;
    private Tweener tweener;

    void Start() {
        scrollRect = GetComponent<ScrollRect>();
        scrollRect.inertia = false;
    }

    public void OnBeginDrag(PointerEventData eventData) {
        if (tweener != null && tweener.IsActive()) {
            tweener.Kill();
        }
    }

    public void OnEndDrag(PointerEventData eventData) {
        int itemCount = scrollRect.content.childCount;
        float step = 1;

        if (itemCount > 2) {
            step = 1f / (itemCount - 1);
        }

        float target = Mathf.Clamp01(Mathf.RoundToInt(ScrollValue / step) * step);

        tweener = DOTween.To(() => ScrollValue, (v) => ScrollValue = v, target, time).SetEase(ease);

        int index = Mathf.RoundToInt(target * (itemCount - 1));
        OnItemCentered(index);
    }

    private float ScrollValue {
        get {
            return scrollRect.horizontal ?
                scrollRect.horizontalNormalizedPosition :
                scrollRect.verticalNormalizedPosition;
        }

        set {
            if (scrollRect.horizontal) {
                scrollRect.horizontalNormalizedPosition = value;
            } else {
                scrollRect.verticalNormalizedPosition = value;
            }
        }
    }
}

AlejoLab, I’ve extended your script a little. I’ve limited it to Horizontal movement only (but you could just change the values easy).

the ScrollRect normalised position values do not go into negative values when they should be. So instead you can use just the Transforms to make sure it bounces back normally.

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;

[RequireComponent(typeof(ScrollRect))]
public class HorizontalScrollSnap : MonoBehaviour
{
    [Tooltip("the container the screens or pages belong to")]
    public Transform ScreensContainer;
    [Tooltip("how many screens or pages are there within the content")]
    public int Screens = 1;
    [Tooltip("which screen or page to start on (starting at 1 for you designers)")]
    public int StartingScreen = 1;
    
    private List<Vector3> _positions;
    private ScrollRect _scroll_rect;
    private Vector3 _lerp_target;
    private bool _lerp;

    // Use this for initialization
    void Start()
    {
        _scroll_rect = gameObject.GetComponent<ScrollRect>();
        _scroll_rect.intertia = false;
        _lerp = false;

        _positions = new List<Vector3>();

        if (Screens > 0)
        {
            for (int i = 0; i < Screens; ++i)
            {
                _scroll_rect.horizontalNormalizedPosition = (float) i / (float)(Screens - 1);
                _positions.Add(ScreensContainer.localPosition);
            } 
        }

        _scroll_rect.horizontalNormalizedPosition = (float)(StartingScreen - 1) / (float)(Screens - 1);
    }

    void Update()
    {
        if (_lerp)
        {
            ScreensContainer.localPosition = Vector3.Lerp(ScreensContainer.localPosition, _lerp_target, 10 * Time.deltaTime);
            if (Vector3.Distance(ScreensContainer.localPosition, _lerp_target) < 0.001f)
            {
                _lerp = false;
            }
        }
    }

    public void DragEnd()
    {
        if (_scroll_rect.horizontal)
        {
            _lerp = true;
            _lerp_target = FindClosestFrom(ScreensContainer.localPosition, _positions);
        }
    }

    public void OnDrag()
    {
        _lerp = false;
    }

    Vector3 FindClosestFrom(Vector3 start, List<Vector3> positions)
    {
        Vector3 closest = Vector3.zero;
        float distance = Mathf.Infinity;

        foreach (Vector3 position in _positions)
        {
            if (Vector3.Distance(start, position) < distance)
            {
                distance = Vector3.Distance(start, position);                
                closest = position;
            }
        }

        return closest;
    }
}

If anyone is interested I Overwrited the original scrollrect to support snapping there are few bugs when calculating scrollTo position but fairly works for me…

but it automatically finds children in content rect and try to align to them.
Also it is possibleto scroll to specified item based on index or simply position.

namespace UnityEngine.UI
{
    using System;
    using UnityEngine;
    using UnityEngine.Events;
    using UnityEngine.EventSystems;
    
    [ExecuteInEditMode, AddComponentMenu("NHK/Drum Scroller", 0x21), SelectionBase, RequireComponent(typeof(RectTransform))]
    public class DrumScroller : UIBehaviour, IEventSystemHandler, IBeginDragHandler, IInitializePotentialDragHandler, IDragHandler, IEndDragHandler, IScrollHandler, ICanvasElement
    {
        [SerializeField]
        private RectTransform
            m_Content;
        private Bounds m_ContentBounds;
        private Vector2 m_ContentStartPosition = Vector2.zero;
        private readonly Vector3[] m_Corners = new Vector3[4];
        [SerializeField]
        private float
            m_DecelerationRate = 0.135f;
        private bool m_Dragging;
        [SerializeField]
        private float
            m_Elasticity = 0.1f;
        [NonSerialized]
        private bool
            m_HasRebuiltLayout;
        [SerializeField]
        private bool
            m_Horizontal = true;
        [SerializeField]
        private Scrollbar
            m_HorizontalScrollbar;
        [SerializeField]
        private bool
            m_autoAlign = true;
        [SerializeField]
        private bool
            m_Inertia = true;
        [SerializeField]
        private MovementType
            m_MovementType = MovementType.Elastic;
        [SerializeField]
        private DrumScrollerEvent
            m_OnValueChanged = new DrumScrollerEvent();
        private Vector2 m_PointerStartLocalCursor = Vector2.zero;
        private Bounds m_PrevContentBounds;
        private Vector2 m_PrevPosition = Vector2.zero;
        private Bounds m_PrevViewBounds;
        [SerializeField]
        private float
            m_ScrollSensitivity = 1f;
        private Vector2 m_Velocity;
        [SerializeField]
        private bool
            m_Vertical = true;
        [SerializeField]
        private Scrollbar
            m_VerticalScrollbar;
        private Bounds m_ViewBounds;
        private RectTransform m_ViewRect;
        protected int m_nearestChildIndex = -1;
        protected RectTransform[] m_children;
        protected bool m_autoScrolling;
        protected Vector2 m_autoScrollPosition;

        protected DrumScroller()
        {
        }

        protected override void Awake()
        {
            base.Awake();
            //RebuildChildren();
        }

        private Vector2 CalculateOffset(Vector2 delta)
        {
            Vector2 zero = Vector2.zero;
            if (this.m_MovementType != MovementType.Unrestricted)
            {
                Vector2 min = this.m_ContentBounds.min;
                Vector2 max = this.m_ContentBounds.max;
                if (this.m_Horizontal)
                {
                    min.x += delta.x;
                    max.x += delta.x;
                    if (min.x > this.m_ViewBounds.min.x)
                    {
                        zero.x = this.m_ViewBounds.min.x - min.x;
                    } else if (max.x < this.m_ViewBounds.max.x)
                    {
                        zero.x = this.m_ViewBounds.max.x - max.x;
                    }
                }
                if (!this.m_Vertical)
                {
                    return zero;
                }
                min.y += delta.y;
                max.y += delta.y;
                if (max.y < this.m_ViewBounds.max.y)
                {
                    zero.y = this.m_ViewBounds.max.y - max.y;
                    return zero;
                }
                if (min.y > this.m_ViewBounds.min.y)
                {
                    zero.y = this.m_ViewBounds.min.y - min.y;
                }
            }
            return zero;
        }
        
        private void EnsureLayoutHasRebuilt()
        {
            if (!this.m_HasRebuiltLayout && !CanvasUpdateRegistry.IsRebuildingLayout())
            {
                Canvas.ForceUpdateCanvases();
            }
        }
        
        private Bounds GetBounds()
        {
            if (this.m_Content == null)
            {
                return new Bounds();
            }
            Vector3 rhs = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
            Vector3 vector2 = new Vector3(float.MinValue, float.MinValue, float.MinValue);
            Matrix4x4 worldToLocalMatrix = this.viewRect.worldToLocalMatrix;
            this.m_Content.GetWorldCorners(this.m_Corners);
            for (int i = 0; i < 4; i++)
            {
                Vector3 lhs = worldToLocalMatrix.MultiplyPoint3x4(this.m_Corners *);*

rhs = Vector3.Min(lhs, rhs);
vector2 = Vector3.Max(lhs, vector2);
}
Bounds bounds = new Bounds(rhs, Vector3.zero);
bounds.Encapsulate(vector2);
return bounds;
}

public override bool IsActive()
{
return (base.IsActive() && (this.m_Content != null));
}

protected virtual void LateUpdate()
{
if (this.m_Content != null)
{
Vector2 vectorRef = Vector2.zero;
int num4 = 0;
Vector2 vectorRef2 = Vector2.zero;

this.EnsureLayoutHasRebuilt();
this.UpdateBounds();
float unscaledDeltaTime = Time.unscaledDeltaTime;
Vector2 offset = this.CalculateOffset(Vector2.zero);

if (!m_autoScrolling)
{
if (!this.m_Dragging && ((offset != Vector2.zero) || (this.m_Velocity != Vector2.zero)))
{
Vector2 anchoredPosition = this.m_Content.anchoredPosition;
for (int i = 0; i < 2; i++)
{
if ((this.m_MovementType == MovementType.Elastic) && (offset != 0f))
{
float currentVelocity = this.m_Velocity ;
anchoredPosition = Mathf.SmoothDamp(this.m_Content.anchoredPosition , this.m_Content.anchoredPosition + offset , ref currentVelocity, this.m_Elasticity, float.PositiveInfinity, unscaledDeltaTime);
this.m_Velocity = currentVelocity;
} else if (this.m_Inertia)
{
this.m_Velocity *= Mathf.Pow(this.m_DecelerationRate, unscaledDeltaTime);

if (Mathf.Abs(this.m_Velocity ) < 1f)
{
this.m_Velocity = 0f;
}
anchoredPosition += (this.m_Velocity * unscaledDeltaTime);
} else
{
this.m_Velocity = 0f;
}
}

if (this.m_autoAlign && children != null)
{
Vector2 viewAnchoredPosition = viewRect.TransformPoint(viewRect.anchoredPosition);
Vector2 childPosition;
Vector2 nearestChildPosition = viewAnchoredPosition;
int childrenLength = children.Length;
if (children != null && childrenLength > 0)
{
float childDistance, nearestChildDistance = float.MaxValue;
for (int i = 0; i < childrenLength; i++)
{
childPosition = children .TransformPoint(viewRect.anchoredPosition);
childDistance = Vector2.Distance(childPosition, viewAnchoredPosition);
if (childDistance < nearestChildDistance)
{
nearestChildDistance = childDistance;
nearestChildPosition = childPosition;
m_nearestChildIndex = i;
}
}

Vector2 myDelta = (viewAnchoredPosition - nearestChildPosition) * 20f;
this.m_Velocity += myDelta;
anchoredPosition += myDelta * unscaledDeltaTime;
}
}

if (this.m_Velocity != Vector2.zero)
{
if (this.m_MovementType == MovementType.Clamped)
{
offset = this.CalculateOffset(anchoredPosition - this.m_Content.anchoredPosition);
anchoredPosition += offset;
}
this.SetContentAnchoredPosition(anchoredPosition);
}
}
} else {

Vector2 anchoredPosition = Vector2.Lerp(this.m_Content.anchoredPosition, m_autoScrollPosition, unscaledDeltaTime * 10f);
//Debug.Log(this.m_Content.anchoredPosition+" "+m_autoScrollPosition);

if (anchoredPosition != m_autoScrollPosition)
{
if (this.m_MovementType == MovementType.Clamped)
{
offset = this.CalculateOffset(anchoredPosition - this.m_Content.anchoredPosition);
anchoredPosition += offset;
}
this.SetContentAnchoredPosition(anchoredPosition);
} else {
m_autoScrolling = false;
}
}

if (this.m_Dragging && this.m_Inertia)
{
m_autoScrolling = false;
Vector3 to = (Vector3)((this.m_Content.anchoredPosition - this.m_PrevPosition) / unscaledDeltaTime);
this.m_Velocity = Vector3.Lerp((Vector3)this.m_Velocity, to, unscaledDeltaTime * 10f);
}
if (((this.m_ViewBounds != this.m_PrevViewBounds) || (this.m_ContentBounds != this.m_PrevContentBounds)) || (this.m_Content.anchoredPosition != this.m_PrevPosition))
{
this.UpdateScrollbars(offset);
this.m_OnValueChanged.Invoke(this.normalizedPosition);
this.UpdatePrevData();
}
}
}

public virtual void OnBeginDrag(PointerEventData eventData)
{
if ((eventData.button == PointerEventData.InputButton.Left) && this.IsActive())
{
this.UpdateBounds();
this.m_PointerStartLocalCursor = Vector2.zero;
RectTransformUtility.ScreenPointToLocalPointInRectangle(this.viewRect, eventData.position, eventData.pressEventCamera, out this.m_PointerStartLocalCursor);
this.m_ContentStartPosition = this.m_Content.anchoredPosition;
this.m_Dragging = true;
}
}

protected override void OnDisable()
{
CanvasUpdateRegistry.UnRegisterCanvasElementForRebuild(this);
if (this.m_HorizontalScrollbar != null)
{
this.m_HorizontalScrollbar.onValueChanged.RemoveListener(new UnityAction(this.SetHorizontalNormalizedPosition));
}
if (this.m_VerticalScrollbar != null)
{
this.m_VerticalScrollbar.onValueChanged.RemoveListener(new UnityAction(this.SetVerticalNormalizedPosition));
}
this.m_HasRebuiltLayout = false;
base.OnDisable();
}

public virtual void OnDrag(PointerEventData eventData)
{
Vector2 vector;
if (((eventData.button == PointerEventData.InputButton.Left) && this.IsActive()) && RectTransformUtility.ScreenPointToLocalPointInRectangle(this.viewRect, eventData.position, eventData.pressEventCamera, out vector))
{
this.UpdateBounds();
Vector2 vector2 = vector - this.m_PointerStartLocalCursor;
Vector2 position = this.m_ContentStartPosition + vector2;
Vector2 vector4 = this.CalculateOffset(position - this.m_Content.anchoredPosition);
position += vector4;
if (this.m_MovementType == MovementType.Elastic)
{
if (vector4.x != 0f)
{
position.x -= RubberDelta(vector4.x, this.m_ViewBounds.size.x);
}
if (vector4.y != 0f)
{
position.y -= RubberDelta(vector4.y, this.m_ViewBounds.size.y);
}
}
this.SetContentAnchoredPosition(position);
}
}

protected override void OnEnable()
{
base.OnEnable();
if (this.m_HorizontalScrollbar != null)
{
this.m_HorizontalScrollbar.onValueChanged.AddListener(new UnityAction(this.SetHorizontalNormalizedPosition));
}
if (this.m_VerticalScrollbar != null)
{
this.m_VerticalScrollbar.onValueChanged.AddListener(new UnityAction(this.SetVerticalNormalizedPosition));
}
CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
}

public virtual void OnEndDrag(PointerEventData eventData)
{
if (eventData.button == PointerEventData.InputButton.Left)
{
this.m_Dragging = false;
}
}

public virtual void OnInitializePotentialDrag(PointerEventData eventData)
{
this.m_Velocity = Vector2.zero;
}

public virtual void OnScroll(PointerEventData data)
{
if (this.IsActive())
{
this.EnsureLayoutHasRebuilt();
this.UpdateBounds();
Vector2 scrollDelta = data.scrollDelta;
scrollDelta.y *= -1f;
if (this.vertical && !this.horizontal)
{
float introduced2 = Mathf.Abs(scrollDelta.x);
if (introduced2 > Mathf.Abs(scrollDelta.y))
{
scrollDelta.y = scrollDelta.x;
}
scrollDelta.x = 0f;
}
if (this.horizontal && !this.vertical)
{
float introduced3 = Mathf.Abs(scrollDelta.y);
if (introduced3 > Mathf.Abs(scrollDelta.x))
{
scrollDelta.x = scrollDelta.y;
}
scrollDelta.y = 0f;
}
Vector2 position = this.m_Content.anchoredPosition + ((Vector2)(scrollDelta * this.m_ScrollSensitivity));
if (this.m_MovementType == MovementType.Clamped)
{
position += this.CalculateOffset(position - this.m_Content.anchoredPosition);
}
this.SetContentAnchoredPosition(position);
this.UpdateBounds();
}
}

public virtual void Rebuild(CanvasUpdate executing)
{
if (executing == CanvasUpdate.PostLayout)
{
this.RebuildChildren();
this.UpdateBounds();
this.UpdateScrollbars(Vector2.zero);
this.UpdatePrevData();
this.m_HasRebuiltLayout = true;
}
}

private static float RubberDelta(float overStretching, float viewSize)
{
return (((1f - (1f / (((Mathf.Abs(overStretching) * 0.55f) / viewSize) + 1f))) * viewSize) * Mathf.Sign(overStretching));
}

protected virtual void SetContentAnchoredPosition(Vector2 position)
{
if (!this.m_Horizontal)
{
position.x = this.m_Content.anchoredPosition.x;
}
if (!this.m_Vertical)
{
position.y = this.m_Content.anchoredPosition.y;
}
if (position != this.m_Content.anchoredPosition)
{
this.m_Content.anchoredPosition = position;
this.UpdateBounds();
}
}

private void SetHorizontalNormalizedPosition(float value)
{
this.SetNormalizedPosition(value, 0);
}

private void SetNormalizedPosition(float value, int axis)
{
this.EnsureLayoutHasRebuilt();
this.UpdateBounds();
float num = this.m_ContentBounds.size [axis] - this.m_ViewBounds.size [axis];
float num2 = this.m_ViewBounds.min [axis] - (value * num);
float num3 = (this.m_Content.localPosition [axis] + num2) - this.m_ContentBounds.min [axis];
Vector3 localPosition = this.m_Content.localPosition;
if (Mathf.Abs((float)(localPosition [axis] - num3)) > 0.01f)
{
localPosition [axis] = num3;
this.m_Content.localPosition = localPosition;
this.m_Velocity [axis] = 0f;
this.UpdateBounds();
}
}

private void SetVerticalNormalizedPosition(float value)
{
this.SetNormalizedPosition(value, 1);
}

public virtual void StopMovement()
{
this.m_Velocity = Vector2.zero;
}
/*
Transform ICanvasElement.get_transform()
{
return base.transform;
}

bool ICanvasElement.IsDestroyed()
{
return base.IsDestroyed();
}
*/
private void UpdateBounds()
{
this.m_ViewBounds = new Bounds((Vector3)this.viewRect.rect.center, (Vector3)this.viewRect.rect.size);
this.m_ContentBounds = this.GetBounds();
if (this.m_Content != null)
{
Vector3 size = this.m_ContentBounds.size;
Vector3 center = this.m_ContentBounds.center;
Vector3 vector3 = this.m_ViewBounds.size - size;
if (vector3.x > 0f)
{
center.x -= vector3.x * (this.m_Content.pivot.x - 0.5f);
size.x = this.m_ViewBounds.size.x;
}
if (vector3.y > 0f)
{
center.y -= vector3.y * (this.m_Content.pivot.y - 0.5f);
size.y = this.m_ViewBounds.size.y;
}
this.m_ContentBounds.size = size;
this.m_ContentBounds.center = center;
}
}

private void UpdatePrevData()
{
if (this.m_Content == null)
{
this.m_PrevPosition = Vector2.zero;
} else
{
this.m_PrevPosition = this.m_Content.anchoredPosition;
}
this.m_PrevViewBounds = this.m_ViewBounds;
this.m_PrevContentBounds = this.m_ContentBounds;
}

private void UpdateScrollbars(Vector2 offset)
{
if (this.m_HorizontalScrollbar != null)
{
this.m_HorizontalScrollbar.size = Mathf.Clamp01((this.m_ViewBounds.size.x - Mathf.Abs(offset.x)) / this.m_ContentBounds.size.x);
this.m_HorizontalScrollbar.value = this.horizontalNormalizedPosition;
}
if (this.m_VerticalScrollbar != null)
{
this.m_VerticalScrollbar.size = Mathf.Clamp01((this.m_ViewBounds.size.y - Mathf.Abs(offset.y)) / this.m_ContentBounds.size.y);
this.m_VerticalScrollbar.value = this.verticalNormalizedPosition;
}
}

public void RebuildChildren()
{
if (content == null)
{
m_children = null;
return;
}

m_children = new RectTransform[content.transform.childCount];
int i = 0;
foreach (RectTransform child in content.transform)
{
m_children = child;
i++;
}
}

public RectTransform content
{
get
{
return this.m_Content;
}
set
{
this.m_Content = value;
RebuildChildren();
}
}

public float decelerationRate
{
get
{
return this.m_DecelerationRate;
}
set
{
this.m_DecelerationRate = value;
}
}

public float elasticity
{
get
{
return this.m_Elasticity;
}
set
{
this.m_Elasticity = value;
}
}

public bool horizontal
{
get
{
return this.m_Horizontal;
}
set
{
this.m_Horizontal = value;
}
}

public float horizontalNormalizedPosition
{
get
{
this.UpdateBounds();
if (this.m_ContentBounds.size.x <= this.m_ViewBounds.size.x)
{
return ((this.m_ViewBounds.min.x <= this.m_ContentBounds.min.x) ? ((float)0) : ((float)1));
}
return ((this.m_ViewBounds.min.x - this.m_ContentBounds.min.x) / (this.m_ContentBounds.size.x - this.m_ViewBounds.size.x));
}
set
{
this.SetNormalizedPosition(value, 0);
}
}

public Scrollbar horizontalScrollbar
{
get
{
return this.m_HorizontalScrollbar;
}
set
{
if (this.m_HorizontalScrollbar != null)
{
this.m_HorizontalScrollbar.onValueChanged.RemoveListener(new UnityAction(this.SetHorizontalNormalizedPosition));
}
this.m_HorizontalScrollbar = value;
if (this.m_HorizontalScrollbar != null)
{
this.m_HorizontalScrollbar.onValueChanged.AddListener(new UnityAction(this.SetHorizontalNormalizedPosition));
}
}
}

public bool inertia
{
get
{
return this.m_Inertia;
}
set
{
this.m_Inertia = value;
}
}

public bool autoAlign
{
get
{
return this.m_autoAlign;
}
set
{
this.m_autoAlign = value;
}
}

public MovementType movementType
{
get
{
return this.m_MovementType;
}
set
{
this.m_MovementType = value;
}
}

public Vector2 normalizedPosition
{
get
{
return new Vector2(this.horizontalNormalizedPosition, this.verticalNormalizedPosition);
}
set
{
this.SetNormalizedPosition(value.x, 0);
this.SetNormalizedPosition(value.y, 1);
}
}

public DrumScrollerEvent onValueChanged
{
get
{
return this.m_OnValueChanged;
}
set
{
this.m_OnValueChanged = value;
}
}

public float scrollSensitivity
{
get
{
return this.m_ScrollSensitivity;
}
set
{
this.m_ScrollSensitivity = value;
}
}

public Vector2 velocity
{
get
{
return this.m_Velocity;
}
set
{
this.m_Velocity = value;
}
}

public bool vertical
{
get
{
return this.m_Vertical;
}
set
{
this.m_Vertical = value;
}
}

public float verticalNormalizedPosition
{
get
{
this.UpdateBounds();
if (this.m_ContentBounds.size.y <= this.m_ViewBounds.size.y)
{
return ((this.m_ViewBounds.min.y <= this.m_ContentBounds.min.y) ? ((float)0) : ((float)1));
}
return ((this.m_ViewBounds.min.y - this.m_ContentBounds.min.y) / (this.m_ContentBounds.size.y - this.m_ViewBounds.size.y));
}
set
{
this.SetNormalizedPosition(value, 1);
}
}

public Scrollbar verticalScrollbar
{
get
{
return this.m_VerticalScrollbar;
}
set
{
if (this.m_VerticalScrollbar != null)
{
this.m_VerticalScrollbar.onValueChanged.RemoveListener(new UnityAction(this.SetVerticalNormalizedPosition));
}
this.m_VerticalScrollbar = value;
if (this.m_VerticalScrollbar != null)
{
this.m_VerticalScrollbar.onValueChanged.AddListener(new UnityAction(this.SetVerticalNormalizedPosition));
}
}
}

public int nearestChildIndex
{
get
{
return m_nearestChildIndex;
}
}

public RectTransform[] children
{
get {
if(m_children == null)
RebuildChildren();

return m_children;
}
}

protected RectTransform viewRect
{
get
{
if (this.m_ViewRect == null)
{
this.m_ViewRect = (RectTransform)base.transform;
}
return this.m_ViewRect;
}
}

public enum MovementType
{
Unrestricted,
Elastic,
Clamped
}

public void ScrollTo(Vector2 position, bool immediate = false)
{
EnsureLayoutHasRebuilt();

if (!immediate)
{
SetContentAnchoredPosition(position);
this.UpdateScrollbars(m_Content.anchoredPosition);
this.m_OnValueChanged.Invoke(this.normalizedPosition);
this.UpdatePrevData();
} else
{
m_autoScrolling = true;
m_autoScrollPosition = position;
}
}

public void ScrollTo(float x, float y, bool immediate = false)
{
EnsureLayoutHasRebuilt();

if (immediate)
{
SetContentAnchoredPosition(new Vector2(x, y));
this.UpdateScrollbars(m_Content.anchoredPosition);
this.m_OnValueChanged.Invoke(this.normalizedPosition);
this.UpdatePrevData();
} else
{
m_autoScrolling = true;
m_autoScrollPosition.x = x;
m_autoScrollPosition.y = y;
}
}

public void ScrollTo(int index, bool immediate = false)
{
EnsureLayoutHasRebuilt();

if (children == null || children.Length == 0)
return;

if (immediate)
{
m_nearestChildIndex = index;
SetContentAnchoredPosition((viewRect.anchoredPosition - children [index].anchoredPosition) + viewRect.sizeDelta * 0.5f + new Vector2(0f, 50f));
this.UpdateScrollbars(m_Content.anchoredPosition);
this.m_OnValueChanged.Invoke(this.normalizedPosition);
this.UpdatePrevData();
} else
{
m_autoScrolling = true;
m_autoScrollPosition = (viewRect.anchoredPosition - children [index].anchoredPosition) + viewRect.sizeDelta * 0.5f + new Vector2(0f, 50f);
}
}

[Serializable]
public class DrumScrollerEvent : UnityEvent
{
}
}
}

Heya folks

I made a video tutorial on how to Snap a Scroll Rect to UI Element. It’s just another way of doing it, i guess there are countless ways of doing that… Anyway i hope you like it :).

Part 01: Unity 5 UI Tutorial - Scroll Rect Snap to Element : Part 01 - YouTube

Part 02: Unity 5 UI Tutorial - Scroll Rect Snap to Element : Part 02 - YouTube

cheers

Hi everyone! Thanks for all the scripts on this thread, they helped me get started.
I didn’t like how the existing implementations used a scroll bar or used Input.GetMouseButton so I came up with a simpler solution. Works fine for me until now, I tested it on iPhone 6.
Just add the component on a game object with a correctly configured scroll rect and configure the public values. Hope this helps someone!

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class ScrollRectSnapHelper : MonoBehaviour, IEndDragHandler, IBeginDragHandler{

    public int NumberOfScreens = 3;
    [Tooltip("How fast does it tween to next page when Flick gesture is detected")]
    public float ThrowSpeed = 5;
    [Tooltip("Percent scroll needs to be moved to detect Flick gesture")]
    public float FlickPercentThreshold = .05f;
    [Tooltip("Maximum time length from drag begin to drag end to clasify as a Flick gesture")]
    public float FlickTimeThreshold = .2f;

    private float screenStep, desiredScreenPos, dragStartPos, flickStartTimeStamp;
    private bool canAnimate = false;
    private ScrollRect scrollRect;

    // Use this for initialization
    void Start () 
    {
        scrollRect = GetComponent<ScrollRect>();
        screenStep = 1.0f / (NumberOfScreens - 1.0f);
    }

    // Update is called once per frame
    void Update () 
    {
        if (canAnimate)
            scrollRect.horizontalNormalizedPosition = Mathf.Lerp(scrollRect.horizontalNormalizedPosition, desiredScreenPos, Time.deltaTime * ThrowSpeed);
    }

    public void OnBeginDrag (PointerEventData data) 
    {
        canAnimate = false;
        dragStartPos = scrollRect.horizontalNormalizedPosition;
        flickStartTimeStamp = Time.time;
    }

    public void OnEndDrag (PointerEventData data) 
    {
        desiredScreenPos = Mathf.Round(scrollRect.horizontalNormalizedPosition/screenStep)*screenStep;

        if (Time.time - flickStartTimeStamp < FlickTimeThreshold && 
            Mathf.Abs(scrollRect.horizontalNormalizedPosition - dragStartPos) > FlickPercentThreshold )
        {
            desiredScreenPos = Mathf.Clamp01( desiredScreenPos + screenStep * Mathf.Sign(scrollRect.horizontalNormalizedPosition - dragStartPos));
        }

        canAnimate = true;
    }
}

Hi Guys and Gals,

I tried both scripts and both didn’t work for me. As in when I did a drag or a swipe it always returned to the first page. I started with cmberryau’s script (since I only needed Horizontal scroll list) and after a little debugging I found that for some reason ScrollRect.horizontalNormalizedPosition was not working the way the script needed it to. So I modified it a little bit and got it to work.

Tested on Unity 4.6.0 RC 2, Mac OSX 10.9.5

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;

[RequireComponent(typeof(ScrollRect))]
public class HorizontalScrollSnap : MonoBehaviour
{
	[Tooltip("the container the screens or pages belong to")]
	public Transform ScreensContainer;
	[Tooltip("how many screens or pages are there within the content")]
	public int Screens = 1;
	[Tooltip("which screen or page to start on")]
	public int StartingScreen = 1;
	
	private List<Vector3> 	m_Positions;
	private ScrollRect 		m_ScrollRect;
	private Vector3 		m_LerpTarget;
	private bool 			m_Lerp;
	private RectTransform	m_ScrollViewRectTrans;

	void Start()
	{
		m_ScrollRect = gameObject.GetComponent<ScrollRect>();
		m_ScrollViewRectTrans = gameObject.GetComponent<RectTransform>();
		m_ScrollRect.inertia = false;
		m_Lerp = false;
		
		m_Positions = new List<Vector3>();
		
		if (Screens > 0)
		{
			Vector3 startPos = ScreensContainer.localPosition;
			Vector3 endPos = ScreensContainer.localPosition + Vector3.left * ((Screens - 1) * m_ScrollViewRectTrans.rect.width);

			for (int i = 0; i < Screens; ++i)
			{
				float horiNormPos = (float) i / (float)(Screens - 1);
				// this does not seem to have an effect [Tested on Unity 4.6.0 RC 2]
				m_ScrollRect.horizontalNormalizedPosition = horiNormPos; 
				m_Positions.Add(Vector3.Lerp(startPos, endPos, horiNormPos));
			} 
		}

		// this does not seem to have an effect [Tested on Unity 4.6.0 RC 2]
		m_ScrollRect.horizontalNormalizedPosition = (float)(StartingScreen - 1) / (float)(Screens - 1);
	}
	
	void FixedUpdate()
	{
		if (m_Lerp)
		{
			ScreensContainer.localPosition = Vector3.Lerp(ScreensContainer.localPosition, m_LerpTarget, 10 * Time.deltaTime);
			if (Vector3.Distance(ScreensContainer.localPosition, m_LerpTarget) < 0.001f)
			{
				m_Lerp = false;
			}
		}
	}

	/// <summary>
	/// Bind this to UnityEditor Event trigger Pointer Up
	/// </summary>
	public void DragEnd()
	{
		if (m_ScrollRect.horizontal)
		{
			m_Lerp = true;
			m_LerpTarget = FindClosestFrom(ScreensContainer.localPosition, m_Positions);
		}
	}

	/// <summary>
	/// Bind this to UnityEditor Event trigger Drag
	/// </summary>
	public void OnDrag()
	{
		m_Lerp = false;
	}
	
	Vector3 FindClosestFrom(Vector3 start, List<Vector3> positions)
	{
		Vector3 closest = Vector3.zero;
		float distance = Mathf.Infinity;
		
		foreach (Vector3 position in m_Positions)
		{
			if (Vector3.Distance(start, position) < distance)
			{
				distance = Vector3.Distance(start, position);                
				closest = position;
			}
		}
		
		return closest;
	}
}

Assumptions :

  • Scroll view is scrollable from Left
  • Scroll element size matches scroll view size (full screen scroll list / view)

[35443-screen+shot+2014-11-18+at+14.35.41.png|35443]

You also obviously need to add an event trigger as the previous post suggest

  • On Drag call “OnDrag()”
  • On PointerUp call “DragEnd()”

Hope this helps someone.

Hi everyone!
first Thanks for this script, its amazing, i’ve implemented it on my game, and worked perfectly in a minute.

I’ve foudn a bug with this :frowning:
I have made a scroll with 5 panels (Like 5 screens with buttons and stuff on each screen)
if i scroll dragging from the background of the screen everything works fine, but i drag touching a button, the screen stops working, the panel is dragged entirely, but with no snapping, i tried adding a evenet trigger and inform the pointer up, but it still dont work :frowning:
Any hints on this?

Hey everyone! I made a little script for this issue, which works really fine.
All you need to do is:

  • Attach this script to some gameobject

  • Create a ScrollRect

  • Create a LayoutGroup within the ScrollRect

  • Create your screens within the LayoutGroup

  • Attach a scrollbar to your ScrollRect

  • Put the scrollbar in the public scrollbar slot of this script

  • Set screenSize to your number of screens

  • And finally add the script to the OnValuChanged option of the scrollbar and select the function ScreenSnap

    using UnityEngine;
    using UnityEngine.UI;
    using System.Collections;

    public class ScrollbarSnap : MonoBehaviour {

     public int screenSize = 9;			//number of screens in your scrollRect
     public Scrollbar mainScrollbar;		//scrollbar attached to your scrollRect
    
     private int snapValue = 4;			//changes the area in which the screen snaps, the higher the smaller the area	
     private float screenSteps;			//number of steps between the whole number of screens
     private float[] screenScrollValue;	//exact value for each screen, used by scrollbar.value
    
     // Use this for initialization
     void Start () {
     	screenSteps = 1.0f / (screenSize - 1.0f);
     	screenScrollValue = new float[screenSize];
     	for(int i = 0; i < screenScrollValue.Length; i++)
     	{
     		screenScrollValue _= i * screenSteps;_
    
  •  }*
    
  • }*

  • // Update is called once per frame*

  • void Update () {*

  • }*

  • public void ScreenSnap ()*

  • {*

  •  for(int i = 0; i < screenScrollValue.Length; i++)*
    
  •  {*
    

if(mainScrollbar.value <= screenScrollValue + screenSteps / snapValue && mainScrollbar.value >= screenScrollValue - screenSteps / snapValue)
* {*
_ mainScrollbar.value = screenScrollValue*;
}
}
}
}*_

If you have been having a difficult time getting any of the above solutions to do what you need, I recommend checking out picker uGUI in the asset store.

I picked it up last night, easy to implement and on top of that the developer was able to get nested scrolling to work with it. Highly recommended.

thx for idea, @Goal_Janna

I fix you a little. Hope this helps someone too

public int screenSize = 9;            //number of screens in your scrollRect
public Scrollbar mainScrollbar;        //scrollbar attached to your scrollRect

public float freeDistance = 50f; //ignored mouse distance

float[] screenScrollValue;    //exact value for each screen, used by scrollbar.value

bool isMouseDown = false;
Vector2 startMousePosition = Vector2.zero;
Vector2 curMousePosition = Vector2.zero;

enum Direction
{
    None,
    Left,
    Right
}

Direction curDirection = Direction.None;

int curStep = 0;

// Use this for initialization
void Start()
{
    var screenSteps = 1.0f / (screenSize - 1.0f);
    screenScrollValue = new float[screenSize];
    for (int i = 0; i < screenScrollValue.Length; i++)
    {
        screenScrollValue _= i * screenSteps;_

}
curStep = 0;
ScreenSnap();
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0))
{
isMouseDown = true;
startMousePosition = Input.mousePosition;
curMousePosition = Input.mousePosition;
curDirection = Direction.None;
}
if (isMouseDown)
{
if (startMousePosition.x < curMousePosition.x - freeDistance)
{
curDirection = Direction.Left;
}
else if (startMousePosition.x > curMousePosition.x + freeDistance)
{
curDirection = Direction.Right;
}
else
{
curDirection = Direction.None;
}
curMousePosition = Input.mousePosition;
}
if (Input.GetMouseButtonUp(0))
{
if (isMouseDown)
{
switch (curDirection)
{
case Direction.Left:
curStep–;
break;
case Direction.Right:
curStep++;
break;
}
}
isMouseDown = false;
}
if (!isMouseDown)
{
ScreenSnap();
}
}
void ScreenSnap()
{
if (curStep < 0)
curStep = 0;
if (curStep >= screenScrollValue.Length)
curStep = screenScrollValue.Length - 1;

mainScrollbar.value = screenScrollValue[curStep];
}

Hello, guys! Here is my solution. Just add this to your RectScroll.

Because RectScroll has no events about the list stops moving by inertia, I had to create coroutine to wait it for the stop.

Depending on your tasks, feel free to modify the code for yourself.
My solution is for a case of vertical list stretched in width. Therefore, the priority was the height of items that alternated in the list one after another: element, separator, element, separator, etc. so I found their unique sizes and get summ of their sizeDelta.y to know items steps

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Linq;

namespace UI
{
	[RequireComponent(typeof(ScrollRect))]
	public class VerticalAutoSnapUiContoller : MonoBehaviour, IBeginDragHandler, IEndDragHandler
	{
		[Header("General settings")]
		[SerializeField] private int framesForCorrection = 10;

		private List<Coroutine> coroutinesInProccess = new List<Coroutine>();
		private ScrollRect contextScrollRect;

		private void Awake()
		{
			contextScrollRect = GetComponent<ScrollRect>();
		}

		public void OnBeginDrag(PointerEventData eventData)
		{
			StopAutoSnapping();
		}

		public void OnEndDrag(PointerEventData eventData)
		{
			BeginAutoSnapping();
		}

		public void BeginAutoSnapping()
		{
			StopAutoSnapping();

			coroutinesInProccess.Add(StartCoroutine(AutoSnappingRoutine()));
		}

		public void StopAutoSnapping()
		{
			foreach(var coroutine in coroutinesInProccess)
				if(coroutine!=null)
					StopCoroutine(coroutine);

			coroutinesInProccess.Clear();
		}

		private IEnumerator AutoSnappingRoutine()
		{
			if (contextScrollRect.content.childCount <= 1)
				yield break;

			Coroutine currentStep = StartCoroutine(WaitForInertiaStopsRoutine());
			coroutinesInProccess.Add(currentStep);

			yield return currentStep;

			currentStep = StartCoroutine(MoveToClosestPositionRoutine());
			coroutinesInProccess.Add(currentStep);

			yield return currentStep;
		}

		private IEnumerator WaitForInertiaStopsRoutine()
		{
			bool isMoving = true;
			Vector2 previousPosition = contextScrollRect.content.anchoredPosition;
			float passedDistance = float.MaxValue;

			while (isMoving)
			{
				yield return null;

				passedDistance = Vector2.Distance(previousPosition, contextScrollRect.content.anchoredPosition);
				previousPosition = contextScrollRect.content.anchoredPosition;

				isMoving = passedDistance > 5;
			}
		}

		private IEnumerator MoveToClosestPositionRoutine()
		{
			contextScrollRect.StopMovement();

			float heightStep = GetHeightStep();

			Vector3 targetPosition = contextScrollRect.content.anchoredPosition;
			targetPosition.y = Mathf.Round(targetPosition.y / heightStep) * heightStep;

			Vector3 initPosition = contextScrollRect.content.anchoredPosition;

			for (int i = 0; i < framesForCorrection; i++)
			{
				contextScrollRect.content.anchoredPosition = Vector3.Lerp(initPosition, targetPosition, i / (float)framesForCorrection);

				yield return null;
			}

			contextScrollRect.content.anchoredPosition = targetPosition;
		}

		private float GetHeightStep()
		{
			List<Vector2> contentSizes = new List<Vector2>();

			foreach (RectTransform childRect in contextScrollRect.content)
				contentSizes.Add(childRect.sizeDelta);

			contentSizes = contentSizes.Distinct().ToList();

			return contentSizes.Sum(x => x.y);
		}
	}
}

How can I implement the functionality of shop system like in clash of clans into unity project. I used simple snap to make it look similar, but it is not working. Can anyone help or suggest something related to make the function of shop system in clash of clan in my unity project.

To detect the scroll has reached the end is ok but when it comes to like I have 4 scroll rect in my project and they are aligned like one is parent scrollrect and others are there chid of that parent scroll but the problem is that after the first scroll child go to the last position it do not go to the second child of the parent scroll. What can I do to achieve that?? Is there any method that I can call to activate the parent scroll which will go to the Second child of the parent scroll and Vise-versa???