6
\$\begingroup\$

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?

enter image description here

UPDATE

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

enter image description here

\$\endgroup\$
  • 4
    \$\begingroup\$ Your UI has the black dot optical illusion, not sure if you are aware, very annoying. \$\endgroup\$ – jgallant Dec 9 '15 at 11:45
  • \$\begingroup\$ The aesthetics are not final. Im just prototyping the structure of the menu ;) I know it looks hideous now :D \$\endgroup\$ – Enrique Moreno Tent Dec 9 '15 at 11:50
2
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Excuse me! How does m_up ^ m_down work in this case? \$\endgroup\$ – Jacob Dec 28 '16 at 7:46
  • \$\begingroup\$ 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). \$\endgroup\$ – idbrii Apr 4 '18 at 0:37
  • \$\begingroup\$ 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. \$\endgroup\$ – Neph Oct 10 '18 at 13:11
0
\$\begingroup\$

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).

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ There's a two-dimensional variant of the script I linked: UIScrollToSelectionXY.cs. I have not tried it. \$\endgroup\$ – idbrii Apr 4 '18 at 0:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.