# Unity - Autoscroll when selecting buttons out of viewport

I have a ScrollView element, that contains many buttons. I want that when I select buttons (via arrow keys), the panel scrolls so that the selected button is contained within the viewport.

How could I achieve this?

UPDATE

This is example of my structure (with 2 buttons only, but you get the gist...)

• Your UI has the black dot optical illusion, not sure if you are aware, very annoying. Dec 9, 2015 at 11:45
• The aesthetics are not final. Im just prototyping the structure of the menu ;) I know it looks hideous now :D Dec 9, 2015 at 11:50

A simple solution is to set the value of the ScrollRect's verticalNormalizedPosition (0f - 1f). Here's the solution I came up with (doesn't include horizontal elements, but similar logic could be applied for your use case).

ButtonSelectionController (attached to ScrollView)

[RequireComponent(typeof(ScrollRect))]
public class ButtonSelectionController : MonoBehaviour
{
[SerializeField]
private float               m_lerpTime;
private ScrollRect          m_scrollRect;
private Button[]            m_buttons;
private int                 m_index;
private float               m_verticalPosition;
private bool                m_up;
private bool                m_down;

public void Start()
{
m_scrollRect        = GetComponent<ScrollRect>();
m_buttons           = GetComponentsInChildren<Button>();
m_buttons[m_index].Select();
m_verticalPosition  = 1f - ((float)m_index / (m_buttons.Length - 1));
}

public void Update()
{
m_up    = Input.GetKeyDown(KeyCode.UpArrow);
m_down  = Input.GetKeyDown(KeyCode.DownArrow);

if (m_up ^ m_down)
{
if (m_up)
m_index = Mathf.Clamp(m_index - 1, 0, m_buttons.Length - 1);
else
m_index = Mathf.Clamp(m_index + 1, 0, m_buttons.Length - 1);

m_buttons[m_index].Select();
m_verticalPosition = 1f - ((float)m_index / (m_buttons.Length - 1));
}

m_scrollRect.verticalNormalizedPosition = Mathf.Lerp(m_scrollRect.verticalNormalizedPosition, m_verticalPosition, Time.deltaTime / m_lerpTime);
}
}


All button children of the ScrollView are got during Start, and the initial button is selected. During Update we're then XOR-ing the up and down arrows so as not to constantly scroll up and down.

I'm then clamping the value of the index to use to the number of button available, before selecting the button at the new index. The position of the ScrollRect is then set to a percentage based on which button is selected from the list (we're subtracting the value from 1f because the position variable works from the bottom up). The new position is then lerped to over the time specified by m_lerpTime.

Hope this helped you.

• Excuse me! How does m_up ^ m_down work in this case? Dec 28, 2016 at 7:46
• I think m_up ^ m_down is intended to prevent any action if both buttons are pressed. I'm not sure why (up would take precedence which doesn't seem crazy). Apr 4, 2018 at 0:37
• Thanks, this does indeed work with the arrow keys but unfortunately it doesn't with a controller and scrolling with the mouse stops working too (using the default Horizontal and Vertical axis) - both with the mouse wheel and draggin the scrollbar.
– Neph
Oct 10, 2018 at 13:11

If you want to use BaseInputModule's movement (which could support gamepad with an appropriate InputModule), you'll want to modify verticalNormalizedPosition based on the position of your selected object relative to its anchor and mask region. You can see a working example in UIScrollToSelection.cs.

I found it has some bad early out cases for my multiple layer hierarchy ("check if scrolling is possible"), unsmooth upward scrolling, and no scrollwheel handling (IScrollHandler, IBeginDragHandler, IEndDragHandler).

If you wanted to do it yourself:

• get the selected element (EventSystem.current.currentSelectedGameObject.GetComponent<RectTransform>())
• project its position into your view (anchoredPosition.y + scrollRect.content.anchoredPosition.y)
• imagine what's the first and last valid position for your element (0 and mask height less the height of one element)
• then check if your element is before the first valid or after the last valid and add or decrement accordingly.

To avoid fighting with the mouse, you should implement IScrollHandler, IBeginDragHandler, IEndDragHandler and disable your autoscroll (I also clear the currentSelectedGameObject).

• There's a two-dimensional variant of the script I linked: UIScrollToSelectionXY.cs. I have not tried it. Apr 4, 2018 at 0:36