- Home /
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.
You would like to know the answer to this as well. Would be useful to define a x/y snapping parameter.
I'm trying to do this now. I previously tried associating a scrollbar to the scrollRect and setting steps in the scrollbar. But it jumps to the positions. So it doesn't look very well. I'll post my results if any. One decision I'm facing is how I want to define the snap positions: by storing coords of by checking custom components attached to the content's children (ie, different pages). $$anonymous$$gestions are welcome.
I have the same problem. DragEnd almost never gets called for me.
I may end up using one of the solutions below or writing one myself, but I REALLY wish this was something configurable in the editor. It's basically a staple of touch game menus, and touch interfaces in general! Just look at the iOS home screen!
Answer by SimonDarksideJ · Feb 05, 2015 at 10:25 PM
A fully working version of this is n the UI Extensions repo on BitBucket, complete with button support :D https://bitbucket.org/ddreaper/unity-ui-extensions/src/6928c4428fb3392f0e9735df44aafee3b347933c/Scripts/HorizontalScrollSnap.cs?at=default
Hi there, are you able to shed some light on how to use this exactly? do we apply it to the parent that has the layoutgroup component on it?
The simplest way to use it is to download the .UnityPackage in to your project. This will make available all the components contained within UI Extensions. It will also add a Editor $$anonymous$$enu option to instantiate one for you :D
Still got a fair way to go with docs for the suite, when I have time :D
Thanks this saved me a ton of time! dont understand why people haven't vote more on this answer, is the best in here I$$anonymous$$HO.
Cheers!
Thanks for mentioning that rhodnius. There are so many solutions listed, its hard to know whats relevant and up to date!
Please note that the above link is no longer maintained, and this component's new location is this: https://bitbucket.org/UnityUIExtensions/unity-ui-extensions/src/master/Scripts/Layout/HorizontalScrollSnap.cs
Answer by AlejoLab · Sep 13, 2014 at 04:12 AM
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] = i * stepSize;
}
}
else
{
points[0] = 0;
}
}
void Update()
{
if(LerpH)
{
scroll.horizontalNormalizedPosition = Mathf.Lerp( scroll.horizontalNormalizedPosition, targetH, 10*scroll.elasticity*Time.deltaTime);
if(Mathf.Approximately(scroll.horizontalNormalizedPosition, targetH)) LerpH = false;
}
if(LerpV)
{
scroll.verticalNormalizedPosition = Mathf.Lerp( scroll.verticalNormalizedPosition, targetV, 10*scroll.elasticity*Time.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:
On Drag call "OnDrag()"
On PointerUp call "DragEnd()"
Hope it helps. Although it would be better if they officially implement it.
Thanks I'll try it out. Apparently I'm too new here to give any thumbs up yet. But you will get one here. (y).
At line 69. The "LerpH" should actually be "LerpV". Otherwise, it won't lerp vertical axis.
Thanks was looking at doing this myself but got stuck on the steps and came across your answer
stepSize = 1/(float)(screens-1);
for(int i = 0; i < screens; i++)
{
points[i] = i * stepSize;
}
Answer by Eyhren · Nov 25, 2014 at 10:48 PM
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] = 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 :)
At line 84. The "LerpH" should actually be "LerpV". Otherwise, it won't lerp vertical axis.
Also, line 68 should be "targetV" and vertical scroll won't work unless you change "int target = FindNearest(scroll.horizontalNormalizedPosition, points);" to "int target = FindNearest(scroll.verticalNormalizedPosition, points);" at line 61
Thanks @cmberryau and @Eyhren ! I added two functions for snapping to the previous/next item (optionally including wrapping) which I call on button clicks. $$anonymous$$aybe they also proof useful to somebody else:
public bool wrapAround = false;
float stepSizeTolerance = 0.99f;
public void SnapToNext(){
// Assu$$anonymous$$g left to right (horizontal only)
targetH = scroll.horizontalNormalizedPosition + stepSize;
if(targetH > 1 + stepSizeTolerance * stepSize){
if(wrapAround)
targetH = 0;
else
targetH = 1;
}
else{
targetH = points[FindNearest(targetH, points)];
}
LerpH = true;
}
public void SnapToPrevious(){
// Assu$$anonymous$$g left to right (horizontal only)
targetH = scroll.horizontalNormalizedPosition - stepSize;
if(targetH < 0 - stepSizeTolerance * stepSize){
if(wrapAround)
targetH = 1;
else
targetH = 0;
}
else{
targetH = points[FindNearest(targetH, points)];
}
LerpH = true;
}
Answer by miniscruff · Dec 03, 2014 at 09:11 AM
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,
}
Answer by BMayne · Nov 26, 2014 at 02:37 AM
So a note to everyone who posted code here.
DON'T USE EVENT TRIGGER :)
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)?
Thank you for that comment, it sounds important, but it is not quite clear to me, me being someone who is not intimately familiar with the systems involved.
So, some feedback, offered sincerely:
"For example" <- I am GUESSING the code after that is an example of how one SHOULD do it, not how one shouldn't. But it's immediately followed by "You are not going to have a good time". So now I'm not sure.
"Don't trust me look at the code below." <- I don't know where that code came from. It doesn't seem to be from the posts above. And yes I notice all the interfaces, but why would that be good or bad? I don't understand that at all.
Again: genuine feedback, I'm not being sarcastic or passive-aggressive (one has to add that these days on the internet :)).
I dont get what you mean dude, do you mean it's bad to import all those interfaces?
I'm someone who prefers to keep as much OUT of the inspector as I possibly can.
@kerihobo It's bad to implement all the interfaces simply because it means the component is listening for and acting on all events that are produced by the EventSystem (even if you don't configure any of the events). Now one or two of these isn't too bad but the more you add the slower it is going to get until it really starts to effect your performance.
This is why all the UI component scripts that Unity provide out of the box ONLY implement those interfaces each control needs. The same pattern should be used when you create your own control scripts.
Only the EventTrigger component when you want to test or play, never for production (unless for some reason you have to)