Adding iPhone Touch Scrolling to a UnityGUI ScrollView

When running on the iPhone, UnityGUI ScrollViews act as if the player is using a mouse. The player can tap on or drag the scrollbar, but they can’t drag the list itself up and down with their finger. And naturally, that’s the first thing my playtester tried to do. So it would be useful to be able to add touch behavior to UnityGUI scrolling lists.


Pawns Puzzle Picker

Here’s how I did it. This article may be useful to someone porting UnityGUI code to the iPhone, or who just wants a working example of how to use the different iPhone touch phases.

(For a downloadable Unity package demonstrating everything in this post, scroll to the bottom.)

Design

The desired behaviors are:

  1. Allow the player to drag the list up and down
  2. Single-tap selects a row
  3. List has inertia, drifts to stop when the user lets go

Note that when a touch begins, we don’t yet know whether the player is starting a tap or a drag. And if they drag a short distance and then let go, did they tap that row or not? To avoid ambiguity many apps have the user press a “Done” button to confirm their selection. Other possibilities are to require a double-tap on a row, or treat it as a tap only if it is released quickly.

I decided to copy behavior I liked in Things for iPhone, which highlights the row when it is first touched. But if the user starts to drag it, the highlight disappears. So a tap can be slow as long as the finger doesn’t move. I like this because you can see what you’ve selected, and cancel it (with a short drag) if you missed your target.

Implementation

It should be possible to access touches during the OnGUI logic. I didn’t do it that way. Instead, my OnGUI code draws the scrolling list but no longer responds to clicks (although as I note below, that code can be left in for cross-platform purposes.) The code for detecting and responding to touches is now all in Update. (This is just a matter of preference, but by separating the row drawing from the code that handles input, I suspect it will be easier to replace the iPhone touch logic when porting the code to a new platform.)

1. Responding to Drags

The code for dragging the list turns out to be easy. All we need to do is detect when a touch delta has occurred. We then adjust the list’s current scroll position by the same amount.

if (touch.phase == TouchPhase.Moved)
{
    // dragging
    scrollPosition.y += touch.deltaPosition.y;
}

Depending on what controls you have in your scrolling list, you may find that the one that is being touched for the drag highlights, and stays highlighted after the player lets go. The solution is either to use a control that doesn’t behave that way (e.g. a Label instead of a Button) or to change the GUIStyle/GUISkin to make selected controls look the same as unselected.

2. Responding to Taps

Things can get a little trickier for detecting single-taps on a control in the list.

It’s tempting to keep the standard GUI click detection code in place. That can be used in some cases, but not if we want the behavior that I described above, where the row is highlighted when the touch begins, and can turn into a selection or a drag depending on if the finger moves or not.

So instead I took out the GUI click-response code and manage the taps myself. The trick here is in figuring out which row was tapped from the touch coordinates. Touch coordinates need to be adjusted as follows:

  • UnityGUI y-coordinates are inverted when compared to touch coordinates.
  • Touch coordinates need to be adjusted by the amount that the list has been scrolled.
  • UnityGUI windows and scrollframes also add offsets to the coordinates.

In my example the scrolling list is in a ScrollView, which in turn is in a Window. So the code to calculate which row a tap was on looks like this:

private int TouchToRowIndex(Vector2 touchPos)
{
    float y = Screen.height - touchPos.y;  // invert y coordinate
    y += scrollPosition.y;  // adjust for current scroll position
    y -= windowMargin.y;    // adjust for window y offset
    y -= listMargin.y;      // adjust for scrolling list offset within the window
    int irow = (int)(y / rowSize.y);
 
    irow = Mathf.Min(irow, numRows);  // they might have touched below last row
    return irow;
}

We also need to test if a tap is actually inside the scrollable list. (Again, we have to offset the list rectangle with the coordinates of the Window it is inside.)

bool IsTouchInsideList(Vector2 touchPos)
{
	Vector2 screenPos    = new Vector2(touchPos.x, Screen.height - touchPos.y);  // invert y coordinate
        Vector2 listSize     = new Vector2(windowRect.width - 2*listMargin.x, windowRect.height - 2*listMargin.y);
	Rect rAdjustedBounds = new Rect(listMargin.x + windowMargin.x, listMargin.y + windowMargin.y, listSize.x, listSize.y);
 
	return rAdjustedBounds.Contains(screenPos);
}

Now the code to handle drags and taps is as follows. I also added code to handle the Cancelled touch phase, which like the Moved phase cancels the selection. (Taps that are not inside the list are ignored; drags that go outside the list cancel the touch.)

fInsideList = IsTouchInsideList(touch.position);
if (touch.phase == TouchPhase.Began && fInsideList)
{
	selected = TouchToRowIndex(touch.position);
}
else if (touch.phase == TouchPhase.Canceled || !fInsideList)
{
	selected = -1;
}
else if (touch.phase == TouchPhase.Moved &&  && fInsideList)
{
	// dragging
	selected = -1;   // cancel the selection in favor of the drag
	scrollPosition.y += touch.deltaPosition.y;
}
else if (touch.phase == TouchPhase.Ended)
{
       // Was it a tap, or a drag-release?
       if ( selected > -1 )
       {
           Debug.Log("Player selected row " + selected);
       }
}

We also need to highlight the row that is being touched, at least until the player starts to drag it. I couldn’t find a documented way to tell UnityGUI to draw the next row with a different state (i.e. Hover or Focus, instead of Normal.) So instead I have a separate GUIStyle for selected rows. In the OnGUI code that draws the rows, I have the following:

       	if ( iRow == selected )
               	GUI.Button(rBtn, rowLabel, rowSelectedStyle);
       	else
       		GUI.Button(rBtn, rowLabel);

3. Inertia

When the user drags and then lets go a standard iOS scrolling list, it doesn’t stop moving immediately. We want that same effect of drifting to a stop. I do this by calculating an initial velocity when the touch ends. This moves the list for half a second, or until a new touch begins.

When the code detects that the touch has ended (TouchPhase.Ended) it stores the y-component of last touch’s deltaPosition. Dividing this by the deltaTime gives the starting velocity, which is gradually reduced to zero over the next half-second. This doesn’t have exactly the same feel that iOS lists have, but it’s very close.

Here is the code that sets up the inertia when the touch phase has just ended. It stores the time of the release and how fast the list was being scrolled at the time.

if (touch.phase == TouchPhase.Ended)
{
     // Was it a tap, or a drag-release?
     if ( selected > -1 )
     {
         Debug.Log("Player selected row " + selected);
     }
     else
     {
         // impart momentum, using last delta as the starting velocity
	 // ignore delta < 10; rounding issue can cause ultra-high velocity
	 if (Mathf.Abs(touch.deltaPosition.y) >= 10) 
	     scrollVelocity = (int)(touch.deltaPosition.y / touch.deltaTime);
	 timeTouchPhaseEnded = Time.time;
     }
}

I ran into a surprising bug where small finger movements would scroll the list extremely quickly. It turns out that a tiny movement of the finger can get rounded up to a delta of 5 pixels. This is a small amount, but given a fast framerate, this rounded amount represents a monstrously high velocity! The simple solution was for my inertia code to ignore very small movements. Less than 10 pixels seemed to work well in my tests on an iPhone 2g. (It’s possible that higher-resolution iPhones and iPads round by a different amounts.)

Here is the code that runs when there are no touches (or multiple touches, which I chose to ignore.) If the list has some leftover velocity from the last drag, it moves the list for a while longer. This uses the variables that were stored by the previous code sample.

if (Input.touchCount != 1)
{
    selected = -1; 
 
    if ( scrollVelocity != 0.0f )
    {
	// slow down over time
	float t = (Time.time - timeTouchPhaseEnded) / inertiaDuration;
	float frameVelocity = Mathf.Lerp(scrollVelocity, 0, t);
	scrollPosition.y += frameVelocity * Time.deltaTime;
 
	// after N seconds, we've stopped
	if (t >= inertiaDuration) scrollVelocity = 0.0f;
    }
   return;
}

Testing reveals that the list works well now. But there is a question of what to do with the original scrollbar. Dragging on it is a bit weird because it moves in the opposite direction as the list. In other words, you drag the list up to move down, but you drag the scrollbar up to move up. For Pawns I simply made the scrollbar very narrow to discourage tapping on it, but it remains as a visual cue that this is a scrollable list.

Miscellaneous

Note that if you make your controls respond only to touches, they can only be tested via UnityRemote or with an actual iPhone or iPad. So I found it useful to keep the original click logic in the OnGUI method as well. The code can be wrapped with a check to make it run only for non-iOS builds:

        if ( fClicked && Application.platform != RuntimePlatform.IPhonePlayer )
        {
           Debug.Log("Player mouse-clicked on row ");
        }

However, note that this code still runs in Unity Remote, which can lead to cases where a touch is interpreted both as touch and as a click.

One last optimization: we can reduce the number of drawcalls a lot by only drawing rows that are actually visible- a trick I blogged about earlier.

// draw call optimization: don't actually draw the row if it is not visible
// rBtn is the rectangle of the row we are about to draw
// rScrollFrame is the rectangle of the scrollview
if ( rBtn.yMax >= scrollPosition.y && 
     rBtn.yMin <= (scrollPosition.y + rScrollFrame.height) )
{
    	// ... GUI drawing commands for the current row ...
}

Demo

The following Unity 4 package (UPDATED 7/4/2013) contains sample GUI scripts that demonstrate a simple scrolling list that responds to iPhone/iPad touches. (In theory these should work on Android as well.) One script is written in C#, the other in JavaScript, but they are otherwise identical.
GUITouchScroll Package for Unity 4

Import it into your project. This will create a new folder, GUITouchScroll. Drag one of the GUITouchScroll prefabs that’s in the folder into your Scene view to create the sample scrolling list– one prefab uses the C# script, the other Javascript. The number of rows, and the window and list dimensions can be adjusted in the Inspector. The example uses the default UnityGUI skin, but this also can be replaced in the Inspector. You can also adjust the inertia, which is how quickly the list stops moving after you let go of a drag. (Fun tip: if you set inertia to 10 seconds a fast drag can leave the list bouncing up and down a few times!)

To test the scrolling and tap behavior, run the scene with Unity Remote or on an iOS device. When a row is tapped, a line of text will be output to the Unity debugging console. (Keep in mind that the scrolling will not look or feel very smooth on Unity Remote, due to the reduced frame rate.)

Important: If you are developing for Android please note that the touch events apparently work a little differently, which breaks the inertia. Please see the comments below from Ali, Greg, and Roberto for ways to fix it. My thanks to them!

(7/4/2013) I have added code to section 2 for detecting whether the touch is inside the scrolling list or not. Thanks for reporting that bug, everyone! This code has also been tested with Unity 4 now and it works without modification.

106 thoughts on “Adding iPhone Touch Scrolling to a UnityGUI ScrollView

  1. Matt Post author

    I was about to apologize for not having time to debug people’s enhancements for them, but it appears I’m too late even to do that. 🙂 I’m very pleased you solved it already.

  2. atromgame

    When stop moving the list its stop immediatly. is it possible when remove finder the list move smoothly and stop ?

  3. Matt Post author

    Actually the code is intended to slow down rather than stop abruptly. That’s the section of the article where I talk about inertia.

    There are two reasons you might not be seeing it.

    1) If you are testing using Unity Remote, the frame rate is low and it’s very hard to see the effect unless you happen to lift your finger at just the right time. A real build and deploy to a device should work though.

    2) If you are running on Android, the touch events apparently work a little differently. See the comments by Ali, with hints on how he fixed it.

    Hope this helps.

  4. pavees

    I tried to make it as Horizontal scroll view but when the second Item in the List Reaches the 0 value in X, the scroll disappears. Could you pls explain why it is so?

  5. GeorgeSoft

    I Dont Know what’s this but Check this out!

    int index = -1;
    var touch = new Touch();

    if (Input.touches.Length != 0)
    {
    touch = Input.GetTouch(0);
    }

    if (touch.phase == TouchPhase.Began)
    {
    if (PositionInRect(touch.position, scrollList[i].rect))
    {
    index = i;
    }
    }
    }

    if (touch.phase == TouchPhase.Moved)
    {
    scroll.vector.y += touch.deltaPosition.y;
    }

    if (touch.phase == TouchPhase.Ended)
    {
    index = -1;
    }

    5min of work and thinking 😀

  6. Sanket

    Hey it just works perfect on android as well. but as you mentioned in one of the previous comment regarding the scroll outside the scrollview.Did you managed to find solution for that bug.I am finding the same problem.One more question how to make it horizontal scrollview rather than a vertical one. I am quite new to unity.
    Hope to see your reply soon.!

  7. Matt Post author

    Unfortunately I am completely swamped with other things at the moment. I am gratified that people are still finding this example useful, and I hope to solve the issue of taps outside the scrollview. It shouldn’t be too hard, but my attempt at a quick and dirty fix didn’t work.

    For horizontal scrolling, I would suggest going to the Unity docs and working up a simple horizontal scroll GUI (without touch controls) just to understand the moving parts, then compare to my vertical scroll example and see what changes you need to make. I’m not sure if it would be easier to apply my tricks to a fresh horizontal example, or to modify my vertical code to flip it.. But either way, try a simple horizontal scroller.

    Good luck!

  8. Matt Post author

    Good news– I fixed the bug where taps and drags outside of the scrolling area were not being ignored. See section 2 for details. The downloadable demo has been updated. (Note: it’s a Unity 4 package now, and will not import into Unity 3. Let me know if this is a problem for anyone.)

  9. jim

    How do I make these “row numbers” into working buttons that will, for example, load unity levels?

  10. Matt Post author

    Jim, find the line that says “Debug.Log(“Player selected row ” + selected);”. That prints to the Unity console the number of the row that the user tapped on. Just replace that with your own code.

  11. Marc

    Hi Matt,
    Great work
    I would like to ask on how can I use your code. I cant figure it out… I also downloaded your unitypackage.

    My problem:
    I want to scroll by touch the contents of my GUI.window..
    My window contains GUI.Label, GUI.TextArea, GUI.Box…

    I spent almost 3 days just to make it work but still cant figure it out..

    If there are other solutions pls guide me..

    Im new in Unity3d..

    Thanks in advance

  12. Matt Post author

    Marc, you should at least be able to import the package, and create a scrollable list from one of my prefabs in a blank scene. Those steps are in the Demo section. Assuming that works, try making small incremental changes to the working code to add the features you want. If you are a beginner its better to make small changes and test each time so you’ll see immediately when something has gone wrong. Another trick is to keep copies of what’s working – either in source control or by making experimental changes to copies of your scripts. Then when something stops working you can compare to a recent version that worked and see what went wrong.

    If you are just starting out with UnityGUI it’s probably worth looking for other GUI tutorials, or make some simpler scenes. I’m covering a very specific problem here and it’s probably not a good introduction to UnityGUI.

  13. Bloahboy

    Hi Matt!

    First of all, I would like to thank you for your solution as it helped me a lot.
    I can’t seem to shake off this one problem though.
    Everything works super fine when I import a new custom package (yours) to Unity and it puts it above Standard Assets. It works just as I want it too when I alter the code suiting my problems and my needs.
    But the problem comes when I move the *.cs file and its prefab under StandardAssets/MainMenuAssets. What it does is, it doesn’t show me my rows anymore nor can I simulate the scroll function again. The second part of the problem doesn’t bother me so much, as I can still test it on my phone, but the first one with it not showing my rows anymore is bothering me a lot.
    Where/When does it come to the problem? When I input this code in my other *.cs file:
    this.GetComponent().StoreScrollGUI();
    StoreScrollGUI() is your onGUI() function, but I had to rename it, also I had to rename your Update function to StoreScrollUpdate()
    I’ve put both StoreScrollUpdate() and StoreScrollGUI() to public, so I can access them at any time.
    If I put my GetComponent line into comment brackets and rename the functions back to original, everything works fine again.
    Please help me, because I’ve been losing my mind over this problem for the past 4 hours :/
    Thank you in advance.

  14. Matt Post author

    It sounds like you have renamed OnGUI, and that simply can’t be done. That method is like Update(), OnMouseDown(), and other built-in methods. By naming it that way you are telling Unity to call it at certain times, and if it doesn’t have that name, it won’t get called.

    OnGUI is particularly weird, in that it gets called more often that you think. Because it contains code for rendering the controls AND for handling events (user clicks on a control, for example) it can get called more than one time per frame.

    I’m not sure why you need to rename it, but that seems to be the core of your problem. You’ll need to find another way to do whatever it is you are doing. Maybe OnGUI could call StoreScrollGUI? (Ditto for Update, which we also need to be called by the Unity engine.)

    Hope this helps.

  15. Sascha

    Hi there,
    I’m using your scroll implementation in a simple scroll view. On iOS it’s working fine, on Android the scrolling is really slow. I tried to change the touch.deltaPosition.y to fit my needs but with no result. Scrolling stays slow.

    I’m only using:

    if (touch.phase == TouchPhase.Moved)
    {
    // dragging
    scrollPosition.y += touch.deltaPosition.y;
    }

    Any suggestions how I can speed up scrolling on Android?

  16. Matt Post author

    If by slow you mean sluggish or jerky, then it sounds like you have something hogging performance in that scene. Check the number of draw calls, and read up on how to lower that number. If you have more than one game object with a GUI script on it, try deactivating some of them to see if that helps- UnityGUI is a bit of a hog. Note that even if a OnGUI script isn’t doing anything, its still killing performance just by existing! I wrote about that here: http://www.mindthecube.com/blog/2010/09/avoiding-performance-cost-of-ongui

    If by slow you mean it smoothly operates but does not actually follow your finger, then that’s weird. As you can see in the code, it reads your finger delta and applies the same delta to the scroll. Either the sampling rate is low (i.e. performance is so bad that Unity is missing some of your drags) or there’s something strange happening.

    Maybe there’s some scaling going on such that your scroll units are not 1:1 with the pixels? In that case, if you can find out what it is, you could multiple touch.deltaPosition.y by the scaling factor. The only way I’m aware of for this to happen is the GUI Matrix, which I have no experience with. But people use it to make their GUI match different resolutions. Let’s say that was being used to scale the GUI up to double its size; you’d probably have to multiple touch.deltaPosition.y by 2.

    So I guess search the code for references to GUI.matrix, and/or print out the values you are getting from touch.deltaPosition to see if they look reasonable. If you drag from the top to the bottom, do the values sum up to the screen height? Etc.

    Good luck!

  17. Bloahboy

    Hi again Matt!

    I solved my problem half way through. I imported all of your code into my code, sampled it, divided it into a few smaller functions, just so it would suit my code.

    I’ve encountered one problem though, and it’s a bit annoying for me, so that’s why I’m going to ask you for your help again.
    So the problem goes like this:
    – I copy your code into my code -> make some changes to the look, etc.
    – I maintain your onGUI() function call as it should be
    – I put your prefab into my main camera scene
    ———————–
    result? everything works fine, with the difference that the scrollable area with its rows is now in every view mode I have, all up until I start the game.

    Problem number 1:
    If I delete the prefab out of the main camera, nothing shows up, but the background of the scrollable surface, meaning I lose all of my rows (items in the store), meaning they don’t show up at all?
    Why is that? And how can I fix it?

    Sub – problem aka problem number 2:
    How to call your code in only one of the view modes (scenes) and for it not to be displayed in all of the scenes?

    Thank you for your help!

  18. Matt Post author

    Sounds like you have made some great progress.

    The scrolling display should only appear whenever the OnGUI script is on an active game object in the scene. (When you drag the prefab into the scene, you are creating such a game object.) There are a number of ways to hide and show the scrolling dialog. Obviously deleting the game object would work, and you’d have to instantiate another copy to bring it back. Or you can call myGameObject.setActive(false) to hide it. (Do NOT hide it by having the OnGUI method run but return early. Just having OnGUI in a script causes a performance hit even if it doesn’t draw anything!)

    Your 2nd question is really the same thing. If its showing in all your scene then the script must be on an active object in the scene. If you only added it to the first scene but see it in later scenes, I’m guessing the onGUI script is attached to a game object that has been set to persist across levels (i.e. DoNotDestroyOnLoad).

    I hope this helps. Good luck!

  19. Marc

    Hi matt,

    I tried to use your code for android and it works great using the solution of Ali because all i need is the inertia effect..

    now when i tried to put in my code with only gui window and some guilabel or text without buttons like in the demo you provided

    now i cant touch scroll my guiwindow and if someone please help me regarding in this…

    in the demo i have no problem but if i want to have the same effect in my guiwindow without buttons still no luck

    i tried this approach and still no luck..

    first i put this in my update

    if (touch.phase == TouchPhase.Moved)
    {
    // dragging
    scrollPosition.y += touch.deltaPosition.y;
    }

    then insert the code for inertia
    if (touch.phase == TouchPhase.Moved)
    {
    // dragging
    scrollPosition.y += touch.deltaPosition.y;
    scrollVelocity = (int)(touch.deltaPosition.y / touch.deltaTime);
    timeTouchPhaseEnded = Time.time;
    }

    NOW HERE IS MY FULL LINE OF CODES

    using UnityEngine;
    using System.Collections;
    [ExecuteInEditMode]
    public class About : MonoBehaviour {

    private Rect windowRect0 = new Rect(0 , 0,Screen.width, Screen.height);

    public GUISkin thisOrangeGUISkin;
    bool isHighlighted = false;

    private Vector2 ScrollPosition = Vector2.zero;
    private float scrollVelocity = 0f;
    private float timeTouchPhaseEnded = 0f;

    public Vector2 scrollPosition;

    // Update is called once per frame
    public void Update () {
    if (Input.GetKeyDown(KeyCode.Escape))
    {
    AutoFade.LoadLevel(“Menu” ,0.10f,0.10f,Color.black);
    audio.Play();
    }
    }

    void OnGUI(){
    GUI.skin = thisOrangeGUISkin;
    isHighlighted = true;

    if( isHighlighted ){
    drawButton();
    }
    }

    void drawButton(){
    windowRect0 = GUI.Window(0, windowRect0, DoMyWindow, “”);
    }

    void DoMyWindow(int windowID) {

    GUILayout.Button(“”);
    GUILayout.Label(“”,”textfield”);
    GUILayout.Space(25);

    ScrollPosition = GUILayout.BeginScrollView(ScrollPosition);
    GUILayout.BeginVertical();
    GUILayout.BeginHorizontal();
    GUILayout.Box(“ARTIPBook\nversion 1.0.0”);
    GUILayout.EndHorizontal();
    GUILayout.EndVertical();

    GUILayout.EndScrollView();

    foreach(Touch touch in Input.touches){
    if (touch.phase == TouchPhase.Moved)
    {
    // dragging
    this.scrollPosition.y += touch.deltaPosition.y;
    this.scrollPosition.y = touch.deltaPosition.y;
    if (Mathf.Abs(touch.deltaPosition.y) >= 10)
    scrollVelocity = (int)(touch.deltaPosition.y / touch.deltaTime);
    timeTouchPhaseEnded = Time.time;
    }
    }
    }

    }

  20. Matt Post author

    Marc, it looks like you are missing most of the logic that responds to touches. I see the bit that checks for TouchPhase.Moved, but not the other touch phases.

    (A trick to try when you have code that breaks, is to compare it to an earlier version that worked. There are plenty of free programs out there that compare two text files and show you the differences, or you can just look at them side-by-side, line-by-line. If that doesn’t help you can edit the files to make them more and more alike– eventually to find the change that broke it.)

  21. Bloahboy

    Hi Matt!

    Sorry if I’m bugging you too much, I only have 1 more question left, due to me solving all of the other problems I had before.

    How do you make a spacing between the rows?

    example:

    row

    row

    row
    __________________
    instead of this:
    row
    row
    row

    I hope you understand my question.
    Thank you for your answer 🙂

  22. Matt Post author

    There are a few ways to space the rows, depending on what you want.

    1. You can make the rows taller using the Row Size variables in the example, but that doesn’t actually insert spaces (i.e. the buttons still touch each other.)

    2. You could probably get a good effect by assigning a guiskin to the GUITouchScroll object (my example uses the default skin, so Options Skin is set to None.) You could then adjust the Button styles to add padding. It’s been a while since I played with GUI skins so I can’t tell you exactly which settings to use, you’ll just have to play with it.

    3. A fairly simple solution is to insert a GUILabel between each GUIButton. Let’s say you define the spacing as:
    public int vspacing = 5;
    Put that right next to the rowSize declaration at the top.

    Then replace the “Rect rBtn = new Rect(blah blah)” line with two lines:
    Rect rBtn = new Rect(0, 0, rowSize.x, rowSize.y - vspacing);
    Rect rSpace = new Rect(0,0, rowSize.x, vspacing);

    So we’ve subtracted vspacing from the button height. As the last line inside that for-loop, add:
    GUI.Label(rSpace,"");

    That should give you adjustable spacing between the buttons. (Just make sure to keep rowSize.y greater than vspacing.)

    A little more work is probably needed in TouchToRowIndex, otherwise taps on the blank space between buttons will register as a tap on the button just above. I’ll just leave that as an exercise for the reader. 🙂

    Hope this gives you some ideas.

  23. Bloahboy

    Hi Matt!

    I’d like to thank you for your help. Just needed a boost, I was too tired, at least so it seems. Thank you for the guidance, I’ve managed to succeed in what I wanted 🙂

    Again, thank you for your help. 🙂

    If I ever need something more, I sure know where to ask 😛

  24. Ben

    Hi,
    the scrolling effect looks awesome.

    Though, I’m currently in need of more than one GUI.Box on the X line. I managed to add extra row of buttons on the right side, but I can’t seem to edit the script enough to make it detect the next row.

    Here’s what I did to two methods:

    void Update()
    {
    if (Input.touchCount != 1)
    {
    selectedY = -1;
    selectedX = -1;

    if ( scrollVelocity != 0.0f )
    {
    // slow down over time
    float t = (Time.time - timeTouchPhaseEnded) / inertiaDuration;
    if (scrollPosition.y = (numRows*rowSize.y - listSize.y))
    {
    // bounce back if top or bottom reached
    scrollVelocity = -scrollVelocity;
    }

    float frameVelocity = Mathf.Lerp(scrollVelocity, 0, t);
    scrollPosition.y += frameVelocity * Time.deltaTime;

    // after N seconds, we've stopped
    if (t >= 1.0f) scrollVelocity = 0.0f;
    }
    return;
    }

    Touch touch = Input.touches[0];
    bool fInsideList = IsTouchInsideList(touch.position);

    if (touch.phase == TouchPhase.Began && fInsideList)
    {
    selectedX = TouchToColumnIndex(touch.position);
    selectedY = TouchToRowIndex(touch.position);
    scrollVelocity = 0.0f;
    }
    else if (touch.phase == TouchPhase.Canceled || !fInsideList)
    {
    selectedY = -1;
    selectedX = -1;
    }
    else if (touch.phase == TouchPhase.Moved && fInsideList)
    {
    // dragging
    selectedY = -1;
    selectedX = -1;
    scrollPosition.y += touch.deltaPosition.y;
    }
    else if (touch.phase == TouchPhase.Ended)
    {
    // Was it a tap, or a drag-release?
    if ( selectedY > -1 && fInsideList )
    {
    // if selected == , then certain perk is selected
    Debug.Log("Player selected row " + selectedY + selectedX);
    }
    else
    {
    // impart momentum, using last delta as the starting velocity
    // ignore delta = 10)
    scrollVelocity = (int)(touch.deltaPosition.y / touch.deltaTime);

    timeTouchPhaseEnded = Time.time;
    }
    }

    }

    void DoWindow (int windowID)
    {
    Rect rScrollFrame = new Rect(listMargin.x, listMargin.y, listSize.x, listSize.y);
    Rect rList = new Rect(0, 0, rowSize.x, numRows*rowSize.y);

    scrollPosition = GUI.BeginScrollView (rScrollFrame, scrollPosition, rList, false, false);

    Rect rBtn = new Rect(0, 0, rowSize.x, rowSize.y - vspacing);
    Rect rBtnX = new Rect(120, 0, rowSize.x, rowSize.y - vspacing);
    Rect rSpace = new Rect(0,0, rowSize.x, vspacing);

    for (int iRow = 0; iRow = scrollPosition.y &&
    rBtn.yMin = scrollPosition.y &&
    rBtnX.yMin <= (scrollPosition.y + rScrollFrame.height) )
    {
    bool fClicked = false;
    bool fClicked1 = false;
    string rowLabel = "Row Number " + iRow;

    if ( iRow == selectedY )
    {
    fClicked = GUI.Button(rBtn, rowLabel, rowSelectedStyle);
    fClicked1 = GUI.Button(rBtnX, rowLabel, rowSelectedStyle);
    }
    else
    {
    fClicked = GUI.Button(rBtn, rowLabel);
    fClicked1 = GUI.Button(rBtnX, rowLabel);
    }
    }

    rBtn.y += rowSize.y;
    rBtnX.y += rowSize.y;
    GUI.Label(rSpace,"");
    }
    GUI.EndScrollView();
    }

    I guess, from what you can see is that just copy pasted some parts to make it, not fully sure of how this works. xD
    Any hints on how I should be working on this?
    Thanks

  25. Matt Post author

    The for-loop has several problems. iRow isn’t getting incremented, and it uses = twice which is an assignment operator, not a comparison.

    Since that loop counts out the # of rows, I don’t think you actually need to change that part. Try putting that part back the way it was, including the if-statement that used to follow it (the optimization to avoid drawing rows that aren’t visible.)

    I see you are calculating two rectangles and drawing two buttons side-by-side, which I think is all that you need to change in DoWindow.

  26. Ben

    Hey, thanks for the swift reply!
    There seems to be a problem with my copy pasting… the if-statement and everything is supposed to be there, but I have no idea why it was left out….

    Yes, I am trying to put more than 1 button per row instead of the default. That’s why I’m experimenting with two buttons side by side currently. When I click on the first button, of course, I managed to Debug log it. But I need to do the same for the second button, or any extra buttons to the right. So, problem is, I do not know where to type a Debug log for the second button or consequent buttons to reply.

    void DoWindow (int windowID)
    {
    Rect rScrollFrame = new Rect(listMargin.x, listMargin.y, listSize.x, listSize.y);
    Rect rList = new Rect(0, 0, rowSize.x, numRows*rowSize.y);

    scrollPosition = GUI.BeginScrollView (rScrollFrame, scrollPosition, rList, false, false);

    Rect rBtn = new Rect(0, 0, rowSize.x, rowSize.y - vspacing);
    Rect rBtnX = new Rect(120, 0, rowSize.x, rowSize.y - vspacing);
    Rect rSpace = new Rect(0,0, rowSize.x, vspacing);

    for (int iRow = 0; iRow = scrollPosition.y &&
    rBtn.yMin = scrollPosition.y &&
    rBtnX.yMin <= (scrollPosition.y + rScrollFrame.height) )
    {
    // find out how to put GUI Buttons here
    bool fClicked = false;
    bool fClicked1 = false;
    string rowLabel = "Row Number " + iRow;

    if ( iRow == selectedY )
    {
    fClicked = GUI.Button(rBtn, rowLabel, rowSelectedStyle);
    fClicked1 = GUI.Button(rBtnX, rowLabel, rowSelectedStyle);
    }
    else
    {
    fClicked = GUI.Button(rBtn, rowLabel);
    fClicked1 = GUI.Button(rBtnX, rowLabel);
    }

    // Allow mouse selection, if not running on iPhone.
    if ( fClicked && Application.platform != RuntimePlatform.IPhonePlayer )
    {
    Debug.Log("Player mouse-clicked on row " + iRow);
    }

    }

    rBtn.y += rowSize.y;
    rBtnX.y += rowSize.y;
    GUI.Label(rSpace,"");
    }
    GUI.EndScrollView();
    }

  27. Matt Post author

    Ben,
    Try copying the IF-statement lines:
    if ( fClicked && Application.platform != RuntimePlatform.IPhonePlayer )
    {
    Debug.Log("Player mouse-clicked on row " + iRow);
    }

    and have the second one check the variable fClicked1 instead of fClicked.

    That should get you a little closer. Note that as currently written, the row highlight will apply to the entire row rather than one button. You’ll probably want to apply the rowSelectedStyle to only the button they are touching, so instead of “if iRow == selectedY” you’d also need to check “iCol == selectedX”. And of course those will have to be set properly.

    Lastly, the code
    // Was it a tap, or a drag-release?
    if ( selectedY > -1 && fInsideList )

    should probably also check that selectedX > -1. Otherwise, they could select a row but not be on a legal column, and the code would think they’d tapped on that row.

    Hope this helps.

  28. Ben

    Thanks!! It’s working great!
    Sorry for the long post which is easily answerable with a short post haha

    Now I’ll just need to find out how to swap these buttons for different textures.

  29. muzzammil

    On one scene it works ok.

    But on other scene it gives me an error
    GUI Error: You are pushing more GUIClips than you are popping. Make sure they are balanced)

    Please help

  30. Matt Post author

    See: http://answers.unity3d.com/questions/12178/gui-error-you-are-pushing-more-guiclips-than-you-a.html
    If you aren’t using yield anywhere in your code, look for a “BeginXXX” call that doesn’t have a corresponding “EndXXX” call.

    If you can’t spot the change, try diff-ing with an earlier version that worked better. If your two scenes are similar, compare them to each other.

    If you don’t have anything similar to compare it to, try the following. Comment out the inside of the for-loop and see if the error goes away. If it does, then the problem is with the code that draws each row. Otherwise, it’s the code that sets up the window and scrolling area. Repeat this exercise to narrow down where the problem is.

    Hope this helps.

  31. Greg

    Hi Matt! Thank you for sharing (and updating) such a useful package!

    Is it be possible to have two different GUITouchScroll objects visible in the same scene? I have tried but not found the way, yet.

  32. Greg

    Hi Matt! I learned it was relating to the windowID, and changed the second element to 1 instead of 0.

    Thanks again for making this available!

  33. Greg

    I’ve got a contribution and a question! This includes the Android inertia update from Ali. In order to have real-time dragging, which I didn’t notice at first, replace:

    else if (touch.phase == TouchPhase.Moved && fInsideList)
    { // Inertia
    selected = -1;
    scrollVelocity = touch.deltaPosition.y/Mathf.Abs(touch.deltaTime);
    timeTouchPhaseEnded = Time.time;
    }

    With:
    else if(touch.phase != TouchPhase.Ended && touch.phase != TouchPhase.Canceled && fInsideList)
    {
    selected = -1;
    scrollPosition.y += touch.deltaPosition.y;
    if (touch.phase == TouchPhase.Moved && fInsideList)
    { // Inertia
    selected = -1;
    scrollVelocity = touch.deltaPosition.y/Mathf.Abs(touch.deltaTime);
    timeTouchPhaseEnded = Time.time;
    }
    }

    This provides real-time dragging with inertia upon touch release. Please comment if there are any suggestions or corrections to the contribution.

    Now my question: How can I make it more difficult to select a row? I find it is too easy to select the row while scrolling through the list. I’d like to prevent row selection if there is any movement outside of a clean touch/release. I’ve been doing a lot of trial and error attempts, but don’t believe I’ve zeroed-in on what I need to adjust.

    Thanks!

  34. Matt Post author

    I don’t know why Android real-time dragging should be different from iOS, but clearly it is. Based on the changes you and Ali submitted,, it appears the TouchPhase.Moved doesn’t track movement the same way as on iOS. I appreciate the contributions!

    To make it more difficult to select a row, perhaps you could try comparing the starting scroll position (you’ll have to store it in a variable) to the scroll position when they let go. If there has been any scrolling at all, cancel the selection (selected=-1).

    This wouldn’t protect against sideways drag, or a drag that doesn’t move the list at all, but I think those won’t bother the player.

  35. Roberto

    Hi, I’ve a problem with the code for the scrolling inertia.
    This part of the code:
    if (Mathf.Abs(touch.deltaPosition.x) >= 1)
    scrollVelocity = (int)(touch.deltaPosition.x / touch.deltaTime);

    give touch.deltaPosition.x always at zero, infact I don’t have any movement after the Ended phase of the Touch.
    I did many changes to your code to adapt to my needs and everything works except the inertia.
    Thanks anyway for your contribution and your work.
    Roberto

  36. Matt Post author

    Roberto, if you are running on Android, see the comments for fixes that Android users have suggested. Inertia required a change to the code.

    I’ll repeat something I mentioned in the article as well– if you are testing with Unity Remote, the frame rate is slow and you will almost never get inertia to happen. If you drag and let go at just the right moment you will sometimes see it, but it can take many attempts. If that’s what’s happening try testing inertia with a real build.

    If those suggestions don’t help, check that my original version works for you (vertically). Assuming it does, then there’s probably a bug in your changes. Although the scrollbar in my example can’t scroll horizontally you should still see a non-zero touch.deltaPosition.x so you could print out that value to verify. Assuming you do, then compare the original code to your code changes and maybe you’ll see what’s wrong.

    Hope this helps!

  37. Roberto

    Thanks Matt and sorry for the useless question. I’ve read later that the issue was already rised and solved. I did many changes to your code in order to make i work horizontally and everything was fine except this.
    Anyway I’ve found my personal solution to the problem.
    I think that Android reset the deltaPosition vector once is entered in the TouchPhase.Ended. Then, in order to track the drag movement as a spin, I’ve saved the touch in the other phases (TouchPhase.Began and TouchPhase.Moved).
    It’s just a backup of the touch and is not so computationally expensive.
    Here the part of the code I used:

    if (touch.phase == TouchPhase.Began && fInsideList){
    selected = TouchToRowIndex_v2(touch.position);
    scrollVelocity = 0.0f;
    touchDeltaMove = touch;
    }else if (touch.phase == TouchPhase.Canceled || !fInsideList){
    selected = -1;
    touchDeltaMove = touch;
    }else if (touch.phase == TouchPhase.Moved && fInsideList){
    // dragging
    selected = -1;
    scroller.x -= touch.deltaPosition.x;

    touchDeltaMove = touch;

    }else if (touch.phase == TouchPhase.Ended){
    // Was it a tap, or a drag-release?
    if ( selected > -1 && fInsideList ){
    Debug.Log(“Player selected row ” + selected);
    }else{
    // impart momentum, using last delta as the starting velocity
    // ignore delta = 10)
    scrollVelocity = (int)(-touchDeltaMove.deltaPosition.x / touchDeltaMove.deltaTime);

    timeTouchPhaseEnded = Time.time;
    }
    }

    Hope it helps 🙂
    Thanks for your time Matt.
    Cheers!

  38. Matt Post author

    Glad you figured it out! And thanks for the detail about why the Android inertia wasn’t working. It doesn’t really sound like an Android bug– it makes sense that there’s no delta when Ended occurs. But it’s unfortunate that iOS works differently. I’ll keep that in mind if I ever rework the example.

  39. Lokesh

    Hi Matt

    Great work, I tried the same code in my android app and it works perfectly fine except just this line (below), scroll doesn’t follow my finger movement, if I move my finger fast on the screen, the scroll would leave behind 🙁

    else if (touch.phase == TouchPhase.Moved && fInsideList)
    {
    // dragging
    selectedY = -1;
    selectedX = -1;
    scrollPosition.x += touch.deltaPosition.x;
    }

  40. Martin

    @Lokesh @Matt,
    to fix Android scroll speed issue, replace

    else if (touch.phase == TouchPhase.Moved && fInsideList)
    {
    // dragging
    selectedY = -1;
    selectedX = -1;
    scrollPosition.x += touch.deltaPosition.x;
    }

    with

    else if (touch.phase == TouchPhase.Moved && fInsideList)
    {
    // dragging
    selectedY = -1;
    selectedX = -1;
    scrollPosition.x += touch.deltaPosition.x * (Time.deltaTime / touch.deltaTime);
    }

    ref: http://answers.unity3d.com/questions/204490/android-delta-touch.html

  41. Matt Post author

    Thanks, Martin.

    The Unity Answers link has a good explanation. If I’m understanding it completely, this solution seems to rely on the touch movements being relatively uniform. For example, if there are 3 touch movements during the frame, my code sees the last one only, hence it must be scaled. But if the first two movements were unlike the last one then it might jump or lag a little. I think this is why it didn’t work smoothly for every device. Nonetheless it is clearly is an improvement over my code, and a good solution for many devices.

    Note also the comment that deltaTime was sometimes zero for one person, so that division by 0 should probably be guarded against.

    Thanks, Martin!

  42. Martin

    No problem, I actually found my scroll view sometimes breaks if I scroll too quickly, so I added in some pre-bounds checking. My specific implementation is this

    if (Time.deltaTime > 0.0f && touch.deltaTime > 0.0f) {
    float newYPos = scrollPosition.y + (touch.deltaPosition.y * Time.deltaTime / touch.deltaTime);
    newYPos = Mathf.Max (0, newYPos);
    newYPos = Mathf.Min((numRows*rowSize.y – listSize.y), newYPos);
    scrollPosition.y = newYPos;
    }

    no need to compute (numRows*rowSize.y – listSize.y) every frame, but thats always the slap-dash code hackery way. Maybe even check touch.deltaPosition.y isn’t near 0 too

  43. Adre

    Hello,
    i have one problem about this code ( and thanks, it helped me A LOT ).

    Sometimes it selects the row even if i am scolling, i have to scoll half of the screen to not make it happen.

    Any suggestion?

  44. Matt Post author

    @Adre That’s kind of strange. If you were using the code I posted without any changes (and I’m not saying you should!) then it seems like the test “touch.phase == TouchPhase.Moved && fInsideList” is failing for some reason. Either the OS isn’t firing TouchPhase.Moved, or more likely, the code thinks the touch is outside of the list for some reason.

    Maybe add a breakpoint or logging to the line that (in my sample) is commented “cancel the selection in favor of the drag”, to see when it is firing. Print out whether fInsideList is true or not. Etc.

    Hope you figure it out!

  45. Matthew

    Hay Matt

    First off thanks for the package, I attempted to create scroll menu myself, but falling short at the velocity of the scroll and mistaking a button press for a drag. I have two questions, First why is it that if you swipe the scroll bar towards the end is rebounds ?
    Secondly how would you make it that there is always one of the buttons in the middle of the visible view ?

    I would much apreshiate the knowledge.

  46. Matt Post author

    The code that does the rebound is marked with a comment: “bounce back if top or bottom reached”. The velocity is simply negated. If you set it to 0 instead, the list would stop dead at the top or bottom. I was simply trying to copy the effect of the native iOS lists, which have bounces like that.

    If you want a button to always appear centered, you need to decide how the list gets the button there. One case might be, if the list is moving slowly enough then it could stop the next time a button is almost perfectly centered. But there is another case, where the user scrolls it with their finger and lets go without a button being centered, and the list isn’t moving (no momentum). In that case you might have to nudge the list up or down to center the nearest button.

    An easier way would be to only scroll the list in increments of the button height. If you are going to do that, I wouldn’t bother with a draggable list at all. Just give the user two buttons, one that moves the list up one button at a time. and the other for moving it downwards.

    Hope this helps!

Leave a Reply

Your email address will not be published. Required fields are marked *