Dropdown autoscroller

Unity’s dropdown system in 5.4.1 is unusable in production because it doesn’t scroll the list to maintain view of the selected item when using the arrow keys. It also does not show the dropdown list on arrow key press, which is a standard behavior that should have been implemented.

This script fixes that problem.

One issue with my script is if the mouse is over the dropdown list, Unity will select the current item according to the current mouse position. This fights / overrides the selection from the arrow keys. The fix would be to disable the mouse on arrow key press, and reenable the mouse when the mouse is moved. This script doesn’t do that, but that can be performed at a higher level.

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

public class DropdownAutoscroller : MonoBehaviour {

    [Tooltip("Assign to the dropdown that should automatically scroll according to the currently selected item.")]
    public Dropdown dropdown;

    // Use this for initialization
    void Start ()
    {
    }
   
    // Update is called once per frame
    void Update () {
        if (EventSystem.current.currentSelectedGameObject == dropdown.gameObject)
        {
            if (Input.GetButtonUp("Vertical"))
            {
                Transform dropdownListTransform = dropdown.gameObject.transform.FindChild("Dropdown List");
                if (dropdownListTransform == null)
                {
                    // Show the dropdown when the user hits the arrow keys if the dropdown is not already showing
                    dropdown.Show();
                }
            }
        }
        else
        {   
            PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current);
            eventDataCurrentPosition.position = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
            List<RaycastResult> results = new List<RaycastResult>();
            EventSystem.current.RaycastAll(eventDataCurrentPosition, results);
            if (results.Count > 0)
            {
                if (results[0].gameObject.transform.IsChildOf(dropdown.gameObject.transform))
                {
                    // Pointer over the list, use default behavior
                    return;
                }
            }
           
            // Autoscroll list as the selected object is changed from the arrow keys
            if (EventSystem.current.currentSelectedGameObject.transform.IsChildOf(dropdown.gameObject.transform))
            {
                if (EventSystem.current.currentSelectedGameObject.name.StartsWith("Item "))
                {
                    // Skip disabled items
                    Transform parent = EventSystem.current.currentSelectedGameObject.transform.parent;
                    int activeChildren = 0;
                    int totalChildren = parent.childCount;
                    for (int childIndex = 0; childIndex < totalChildren; childIndex++)
                    {
                        if (parent.GetChild(childIndex).gameObject.activeInHierarchy)
                        {
                            activeChildren++;
                        }
                    }
                    int myActiveIndex = 0;
                    for (int childIndex = 0; childIndex < totalChildren; childIndex++)
                    {
                        if (parent.GetChild(childIndex).gameObject == EventSystem.current.currentSelectedGameObject)
                        {
                            break;
                        }
                        else if (parent.GetChild(childIndex).gameObject.activeInHierarchy)
                        {
                            myActiveIndex++;
                        }
                    }

                    if (activeChildren > 1)
                    {
                        GameObject scrollbarGameObject = GameObject.Find ("Scrollbar");
                        if (scrollbarGameObject != null && scrollbarGameObject.activeInHierarchy)
                        {
                            Scrollbar scrollbar = scrollbarGameObject.GetComponent<Scrollbar>();
                            if (scrollbar.direction == Scrollbar.Direction.TopToBottom)
                                scrollbar.value = (float) myActiveIndex / (float) (activeChildren-1);
                            else
                                scrollbar.value = 1.0f - (float) myActiveIndex / (float) (activeChildren-1);
                        }
                    }
                }
            }
        }
    }
}
2 Likes

Thanks, great script!

This works great. Using Text mesh pro dropdowns and this made it way easier to autoscroll

I don’t know it will work on your task or not. I simplified the above code and applied it the below way, and it worked fine on my project. :slight_smile:

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class ExpandedDropdown : Dropdown
{
    public override void OnPointerClick(PointerEventData eventData)
    {
        base.OnPointerClick(eventData);

        if (eventData.selectedObject.GetComponentInChildren<Scrollbar>() != null || !IsActive() || !IsInteractable())
            return;

        Scrollbar scrollbar = gameObject.GetComponentInChildren<ScrollRect>()?.verticalScrollbar;
        if (options.Count > 1 && scrollbar != null)
        {
            if (scrollbar.direction == Scrollbar.Direction.TopToBottom)
                scrollbar.value = Mathf.Max(0.001f, (float) (value) / (float) (options.Count - 1));
            else
                scrollbar.value = Mathf.Max(0.001f, 1.0f - (float) (value) / (float) (options.Count - 1));
        }
    }
}
3 Likes

Excuse me but how can I add this to an already existing TMP Dropdwon? I’m a bit of a begginner and I’m twinkering with UI.

Thanks a bunch!

Is this logic expected to go to Unity’s Dropdown classes? As RakNet I also thought it’s pretty standard behavior, e.g., it’s supported in HTML

Here’s a script you can add (I modified VeryHappyYoung’s code a bit)
It depends on order of components on the GameObject so be sure to place this below actual Dropdown.

using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using static UnityEngine.UI.Scrollbar;

/// <summary>Adds support for auto-scrolling to selected value</summary>
/// <remarks>Place this after actual Dropdown Component, depends on GetComponents order logic</remarks>
public class AutoscrollDropdown : MonoBehaviour, IPointerClickHandler {
  [SerializeField] private TMP_Dropdown target = null!;

  public void OnPointerClick(PointerEventData _) {
    if (!target.GetComponentInChildren<Scrollbar>() || !target.IsActive() || !target.IsInteractable()) {
      return;
    }

    var scrollbar = GetComponentInChildren<ScrollRect>()?.verticalScrollbar;
    if (target.options.Count > 1 && scrollbar != null) {
      var valuePosition = (float)target.value / (target.options.Count - 1);
      var value = scrollbar.direction == Direction.TopToBottom ? valuePosition : 1f - valuePosition;
      scrollbar.value = Mathf.Max(.001f, value);
    }
  }
}

BTW, @VeryHappyYoung , could you explain eventData.selectedObject.GetComponentInChildren<Scrollbar>() != null logic (why != null, not == null)? It didn’t work for me (and also thrown NullReferenceException when nothing was selected yet) so I changed it to !target.GetComponentInChildren<Scrollbar>()

3 Likes

I came up with this method.

Basically you have the items take their child index and divide it by the siblings to get an approximation of what the scroll value should be. Then when a child object is selected, you set the scrollbar’s value to that number.

2 Likes