Scrolling Scroll Rect with Buttons

I am trying to create something like the following for my level selection


(taken from bing)

So, I want to allow the player to scroll through the levels by clicking/holding the left/right arrows.

By using Scroll Rect I can create something like this easily. However, I don’t see a way to implement the buttons instead of a drag or scrollbar.

I tried creating two Buttons at a higher level than the Scroll Rect (so they aren’t scrolled out of view as well).

However, I don’t see what function I can call in my On Click in order to scroll the Scroll Rect.

Anyone know if this is possible?

Thank you

Try adding Scrollbar, hide it’s graphic components and do it via Scrollbar.value with code, if the Value is 0 then you switch your left button to .interactive = false, and if value is 1 then you do that with your right button

I know it’s a hack but maybe that will do :smile:

1 Like

Gah, second try ;-(
Right, you can’t connect to the Scrollbar control as it does not expose publicly any methods or properties to alter the position of it’s content. However by delving in to the ScrollBar’s code (nice we can do this now) you can see all it does is to alter the transform position of the Content GO.

So to make this move using the buttons, just create a script that has a method to alter the transform.position of the GO it is attached to. Then attach the script to the Content GO. Finally wire the button Click event to the Content object and select the new method you just created in the script (I also added a parameter so I could control from the click the amount and direction by using either positive or negative values) using either the editor or from code.

Now I would recommend extending this example to also adhere to the bounds of the ScrollRect in the same way the ScrollRect code does, just so that it doesn’t go off screen!
If you need me to put an example somewhere, just let me know.

1 Like

That’s all it takes (works for me):

using System;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Button))]
public class ScrollbarIncrementer : MonoBehaviour
{
    public Scrollbar Target;
    public Button TheOtherButton;
    public float Step = 0.1f;

    public void Increment()
    {
        if (Target == null || TheOtherButton == null) throw new Exception("Setup ScrollbarIncrementer first!");
        Target.value = Mathf.Clamp(Target.value + Step, 0, 1);
        GetComponent<Button>().interactable = Target.value != 1;
        TheOtherButton.interactable = true;
    }

    public void Decrement()
    {
        if (Target == null || TheOtherButton == null) throw new Exception("Setup ScrollbarIncrementer first!");
        Target.value = Mathf.Clamp(Target.value - Step, 0, 1);
        GetComponent<Button>().interactable = Target.value != 0;;
        TheOtherButton.interactable = true;
    }
}
  1. Copy the code to a new script
  2. Add Scrollbar
  3. Hide all Image components from Scrollbar
  4. Put that script on both buttons
  5. Add new RuntimeOnly OnClick on each button and assign them to Increment() and Decrement() functions from ScrollbarIncrementer
  6. Set Target on each script to Scrollbar and TheOtherButton to the other button
  7. works (assuming your ScrollRect is set up properly)
12 Likes

@SimonDarksideJ yes, an example would be amazing!

Thanks :slight_smile:

@thormond That method looks pretty good. I’m still interested in SimonDarksideJ’s method as it seems a little bit simpler. However, I did test your method out and it works fairly well. If I wanted to extend it to scroll when the buttons are held down, would I need to use an Event Trigger on the buttons and update your script to update the position until a point up event is found?

You would have to add EventTrigger component and add this code while referencing OnPointerDown(either true or false on checkbox) and OnPointerUp in the inspector

public float HoldFrequency = 0.1f;
    public void OnPointerDown(bool increment)
    {
        InvokeRepeating("IncrementDecrementSequence", 0.5f, HoldFrequency);
    }

    public void OnPointerUp()
    {
        CancelInvoke("IncrementDecrementSequence");
    }

    public void IncrementDecrementSequence(bool increment)
    {
        if(increment) Increment();
        else          Decrement();
    }

or like this

public float HoldFrequency = 0.1f;
    public void OnPointerDown(bool increment)
    {
        StartCoroutine("IncrementDecrementSequence", increment);
    }

    public void OnPointerUp()
    {
        StopCoroutine("IncrementDecrementSequence");
    }

    IEnumerator IncrementDecrementSequence(bool increment)
    {
        yield return new WaitForSeconds(HoldFrequency);
        if (increment) Increment();
        else           Decrement();
        StartCoroutine("IncrementDecrementSequence", increment);
    }
2 Likes

Sure, go wild (actually in testing, you don’t need to control the bounds of the content, the ScrollRect won’t let you!)

Attached is a very simple demo, 1 script as follows:

public class MoveContent : MonoBehaviour {

    public void MoveContentPane(float value)
    {
        var pos = transform.position;
        pos.x += value;
        transform.position = pos;
    }
}

Which simply gives us a method to alter the position of the GO it is attached to based on the value you pass in, in this case the Congent GO of the ScrollRect.
The buttons then simply need to hook in their “click” event to this method on the Content GO passing the value it needs to move by (positive for right and negative for left) as follows:
1940853--125505--upload_2015-1-29_22-8-41.png
Very simple solution, only draw back with the Button control is that it only has a Click method. If you wanted hold as well you would need to extend the button control in your own class and add the IPointerDown interface as well.

1940853–125504–ScrollRectButtons.unitypackage (8.09 KB)

4 Likes

Thanks guys, these both look great :slight_smile:

1 Like

Thank you!!

@SimonDarksideJ

Hero of the day!
Really thanks. This work great with menu that change the number of elements like mine because move panel all time at same distance!!!

You might also want to check out the ScrollRect implementations in the UI Extensions project (link in sig). Especially the new UIVerticalScroller (only in dev code at the mo)

you’re brilliant, this code works great and is simple.

  1. I just moved the scroll bar off screen

I have Unity 5.6.1f1 and my version of Simon’s simple demo works perfectly with not many rows. However, if there is a large number of rows, the scroll rect moves correctly on the first button press but on second button press it goes back to its starting position and will not move again ( exact breakpoint not identified but it succeeds with 7 and 18 and fails with 118). I have struggled and struggled with this but for the life me I cannot see why it is doing what it does.

The rows are held within a panel having a vertical layout and content fitter. This panel holds dynamically instantiated child panels each having a button child with horizontal layout for a number of text children each having their own layout element. I got this structure from some tutorial somewhere.

You can see from the first press & debug output below that the scroll rect top (pivot) is moved up to 153.5 and contents are displayed correctly but the 2nd press & debug output says unity thinks the scroll rect top is at 181.5 but contents are displayed at 125.5 and the 3rd press & debug output says unity thinks the top is at 125.5 which is where displays and will move no more.

Any advice will be much appreciated. Although familiar with coding, I am relatively new to C# & Unity so be gentle and simple! Thanks.

the debug output is:
before move: contents top y= 125.5 contents bottom y pos=-3094.5

after move: contents top y pos=153.5 contents bottom y pos=-3066.5

before move: contents top y= 153.5 contents bottom y pos=-3066.5

after move: contents top y pos=181.5 contents bottom y pos=-3038.5

before move: contents top y= 125.5 contents bottom y pos=-3094.5

after move: contents top y pos=153.5 contents bottom y pos=-3066.5

The code is below

using UnityEngine;

public class Admin_Financial_Log_Scroll_Buttons_script : MonoBehaviour
{
    private Admin_Financial_Log_Control_script myscriptcontrol;

    private int number_of_rows;
    private float row_height;

    public GameObject contents_object;
    public Transform contents_pane;
    private float contents_height;
    private float contents_top_y;
    private float contents_bottom_y;

    public GameObject viewing_object;
    public Transform viewing_pane;
    private float viewing_height;
    private float viewing_top_y;
    private float viewing_bottom_y;

    void Start ()
    {
        myscriptcontrol = GameObject.Find("Controlscript").GetComponent<Admin_Financial_Log_Control_script>();
        viewing_object= GameObject.Find("PanelToViewLogContentsRowsScrolling");
        contents_object = GameObject.Find("PanelforLogContentsRowsComplete");
        myscriptcontrol.First_Time_These_Transactions = true;
    }

    public void First_Time()
    {
        myscriptcontrol.First_Time_These_Transactions = false;
        contents_pane = contents_object.GetComponent<Transform>();
        contents_top_y = contents_pane.position.y;
        contents_height = contents_pane.GetComponent<RectTransform>().rect.height;
        contents_bottom_y = contents_top_y - contents_height;

        viewing_pane = viewing_object.GetComponent<Transform>();
        viewing_top_y = viewing_pane.position.y;//pivot=1
        viewing_height = viewing_pane.GetComponent<RectTransform>().rect.height;
        viewing_bottom_y = viewing_pane.position.y-viewing_height;//pivot=1
    
        number_of_rows = myscriptcontrol.mytransactionslist.Count + 1; //incl grand totals
        if (myscriptcontrol.show_content_text.text == "Items") { number_of_rows -= 1; }
        row_height = contents_height / number_of_rows;

    }

    public void MoveContentPane(float updown) //-1=up ie panel moves downwards whilst +1=down ie panel moves upwards
    {
        if (myscriptcontrol.First_Time_These_Transactions == true) { First_Time(); } //set viewing panes details

        contents_pane = contents_object.GetComponent<Transform>();
        var pos = contents_pane.position;
        Debug.Log("before move: contents top y= "+ contents_pane.position.y + " contents bottom y pos=" + (contents_pane.position.y-contents_height));

        pos.y += row_height * updown;
        contents_bottom_y = pos.y - contents_height;

        if (  contents_bottom_y > viewing_bottom_y ) //ie bottom of scroll panel above bottom of viewing panel
        {
            StartCoroutine( myscriptcontrol.ScrollbarBottom());
            Debug.Log("bottom boundary gap : 'contents bottom' would be="+ contents_bottom_y + " and bottom limit="+viewing_bottom_y);
            return;
        }
       
        if(pos.y < viewing_top_y)
        {
            StartCoroutine(myscriptcontrol.ScrollbarTop());
            Debug.Log("top boundary gap : 'contents top' would be=" + contents_top_y + " and top limit=" + viewing_top_y);
            return;
        }

        contents_pane.position = pos ;   
    
        Debug.Log("after move:  contents top y pos=" + contents_pane.position.y+" contents bottom y pos="+contents_bottom_y);
    }
}

Update to above…
I have replaced the scrollbar with a slider with the result that both slider and up/down buttons now work at all times. This is useful because with a large number of entries the slider moves the viewing panel up/down by many rows whilst the buttons move it up/down by only one.

It seems to me that the Unity scrollbar/scrollrect script can cause a problem with ‘position’ in certain circumstances like mine.

If you are interested in how the buttons and slider now work - I deleted the vertical scrollbar and created a slider in its place. In the inspector I set the sliders “onvaluechanged” command to invoke the “passthrough” method shown below. The script is attached to the slider and it finds the relevant scroll rect when it starts

The button script is also shown below and is invoked by the usual “oncommandclick” with the up button passing through 1 and the down button passing through -1.

using UnityEngine;
using UnityEngine.UI;

public class Admin_Financial_Log_SliderBar_script : MonoBehaviour
{

    public ScrollRect MyScrollRect;

    private void Start()
    {
        MyScrollRect = GameObject.Find("PanelToViewLogContentsRowsScrolling").GetComponent<ScrollRect>();
    }

    public void PassThrough(float scrollValue)
    {
        MyScrollRect.verticalNormalizedPosition = scrollValue;
    }
}

using UnityEngine;
using UnityEngine.UI;
public class Admin_Financial_Log_SliderButtons_script : MonoBehaviour
{
    private Admin_Financial_Log_Control_script myscriptcontrol;
    private int number_of_rows;
    private float row_height;
    private GameObject vertical_slider_object;
    private Slider vertical_slider;
    public GameObject contents_object;
    public Transform contents_pane;
    private float contents_height;
    void Start()
    {
        myscriptcontrol = GameObject.Find("Controlscript").GetComponent<Admin_Financial_Log_Control_script>();
        contents_object = GameObject.Find("PanelforLogContentsRowsComplete");
        vertical_slider_object = GameObject.Find("SliderVerticalContents");
        myscriptcontrol.First_Time_These_Transactions = true;
    }
    public void First_Time()
    {
        vertical_slider = vertical_slider_object.GetComponent<Slider>();
        myscriptcontrol.First_Time_These_Transactions = false;
        contents_pane = contents_object.GetComponent<Transform>();
        contents_height = contents_pane.GetComponent<RectTransform>().rect.height;
        number_of_rows = myscriptcontrol.mytransactionslist.Count + 1; //incl grand totals
        if (myscriptcontrol.show_content_text.text == "Items") { number_of_rows -= 1; }
        row_height = contents_height / number_of_rows;
    }
    public void MoveContentPane(float updown) //1=slider moves up an -1=down 
    {
        if (myscriptcontrol.First_Time_These_Transactions == true) { First_Time(); } //set row height
        var slider = vertical_slider_object.GetComponent<Slider>();
        slider.value += row_height * updown/contents_height;
        vertical_slider_object.GetComponent<Slider>().value = slider.value;
   }
}

Im having an issue getting my scrollbar to even work. Code below builds scroll view

 private void Build_Recipe_List()
    {
        string uri = Game_Data._EndPoint ;

        _User.Email = Game_Data._Rune_Master_User.Email;
        StartCoroutine(RecipeDataWebRequest(uri, _User));


        foreach (DataRow dataRow in _Recipe_Data.Rows)
        {
            foreach (DataColumn Column in _Recipe_Data.Columns)
            {
                bool unlocked = Convert.ToBoolean(dataRow[Column]);
                string Objname = Column.ToString();
                string recipe = string.Empty;               

                if (unlocked)
                {
                    recipe = Objname.Replace("Recipe_", "").Replace("_", " ");
                    Add_Button(recipe, Objname);
                }
            }
        }
    }

    public void Add_Button(string recipe = "", string objName = "")
    {
        //Create Copy of Button Template
        var copy = Instantiate(_btnCraftingTemplate);

        //Add Copy to Parent Object
        copy.transform.parent = _Content.transform;       

        int copyindex = _index;
        copy.GetComponent<Button>().name = objName;
        copy.GetComponent<Button>().image.sprite = Image_Game_Data._Scroll_Icon;
        copy.GetComponent<Button>().onClick.AddListener(() => { Debug.Log("index number " + copyindex); });
        copy.GetComponentInChildren<Text>().text = recipe;
        _index++;
    }

Click event on buttons:

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

[RequireComponent(typeof(Button))]
public class btnScroll : MonoBehaviour
{
    public Scrollbar Target;
    public Button TheOtherButton;
    public float Step = 0.1f;

    public void Increment()
    {
        if (Target == null || TheOtherButton == null) throw new Exception("Setup ScrollbarIncrementer first!");
        Target.value = Mathf.Clamp(Target.value + Step, 0, 1);
        GetComponent<Button>().interactable = Target.value != 1;
        TheOtherButton.interactable = true;
    }

    public void Decrement()
    {
        if (Target == null || TheOtherButton == null) throw new Exception("Setup ScrollbarIncrementer first!");
        Target.value = Mathf.Clamp(Target.value - Step, 0, 1);
        GetComponent<Button>().interactable = Target.value != 0; ;
        TheOtherButton.interactable = true;
    }
}

Anyone still looking for answer I created a github repository with a sample scene. GitHub - jinincarnate/unity-scrollbar-buttons: A naive approach to implement smooth scrolling on Unity's Scroll Rect with buttons as unity doesn't provide buttons on its scroll bar

2 Likes

great script, though it seems to not step perfectly between the items for me (ie, i press right, it moves but the right element is centralised), . any ideas why? Im assuming I need to adjust step, but not sure what its based on.

I found solution here:
https://stackoverflow.com/questions/40931773/scroll-rect-manual-scrolling-through-script-unity-c-sharp
and little bit modify

Put this script on Scroll Rect

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


public class ScrollSnapHelper : MonoBehaviour
{
    public Coroutine lastLerpCoroutine;
    ScrollRect scroll;
    public void Start()
    {
        scroll = GetComponent<ScrollRect>();
    }

    public void CancelLerp() 
    {
        if (lastLerpCoroutine != null)
        {
            StopCoroutine(lastLerpCoroutine);
            lastLerpCoroutine = null;
        }
    }
    public void LerpToSelectedItem(RectTransform selectedRect)
    {
        CancelLerp();
        lastLerpCoroutine =  StartCoroutine(LerpToPage(selectedRect));
    }
    private IEnumerator LerpToPage(RectTransform selectedRect)
    {
        Vector2 lerpTo = (Vector2)scroll.transform.InverseTransformPoint(scroll.content.position) - (Vector2)scroll.transform.InverseTransformPoint(selectedRect.position);
        bool lerp = true;
        Canvas.ForceUpdateCanvases();

        while (lerp)
        {
            float decelerate = Mathf.Min(10f * Time.deltaTime, 1f);
            scroll.content.anchoredPosition = Vector2.Lerp(scroll.transform.InverseTransformPoint(scroll.content.position), lerpTo, decelerate);
            if (Vector2.SqrMagnitude((Vector2)scroll.transform.InverseTransformPoint(scroll.content.position) - lerpTo) < 0.25f)
            {
                scroll.content.anchoredPosition = lerpTo;
                lerp = false;
            }
            yield return null;
        }
    }
}

Put this script on selectable scroll items. Dont forget to select first item in event system

using UnityEngine;
using UnityEngine.EventSystems;

public class ScrollSelectableSnap : MonoBehaviour, ISelectHandler, IPointerEnterHandler, IPointerExitHandler
{
    public bool isTouching;
    private ScrollSnapHelper snapHelper;
    private RectTransform rectTransform;

    void Start()
    {
        rectTransform = transform as RectTransform;
        snapHelper = transform.GetComponentInParent<ScrollSnapHelper>();    
    }
    public void OnSelect(BaseEventData eventData)
    {
        if (isTouching) return;
        snapHelper.LerpToSelectedItem(rectTransform);
    }
 

    public void OnPointerEnter(PointerEventData eventData)
    {
        snapHelper.CancelLerp();
        isTouching = true;
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        isTouching = false;
    }
}
2 Likes

Another good solution on youtube

1 Like