Events and UI in Unity
In version 4.6 Unity introduced its new event system, which also powered the UI framework ever since. While this certainly was a step in the right direction, the system is still[1] far from being perfect, and one should not expect the finesse usually found in “real” event systems, for instance those of the bigger frameworks for web development. Further, official documentation is sparse to say the least, but fortunately many developers have shared their insights in the Unity forums and the web by now, so with a little bit of research the new event system’s challenges can be overcome.
The Problem with Dropdowns
A problem that is more related to the UI framework itself, but also touches the event system is the following: The expanding part of a dropdown will not scroll to the currently highlighted item automatically. It seems the developers only had mouse- and touch-inputs in mind when designing the widget, but utilising the event system, we can fix this in a single handler method.
A Solution – the Self-Scrolling Dropdown
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; public class EventSensitiveScrollRect : MonoBehaviour, IUpdateSelectedHandler { private static float SCROLL_MARGIN = 0.3f; // how much to "overshoot" when scrolling, relative to the selected item's height private ScrollRect sr; public void Awake() { sr = this.gameObject.GetComponent<ScrollRect>(); } public void OnUpdateSelected(BaseEventData eventData) { // helper vars float contentHeight = sr.content.rect.height; float viewportHeight = sr.viewport.rect.height; // what bounds must be visible? float centerLine = eventData.selectedObject.transform.localPosition.y; // selected item's center float upperBound = centerLine + (eventData.selectedObject.GetComponent<RectTransform>().rect.height / 2f); // selected item's upper bound float lowerBound = centerLine - (eventData.selectedObject.GetComponent<RectTransform>().rect.height / 2f); // selected item's lower bound // what are the bounds of the currently visible area? float lowerVisible = (contentHeight - viewportHeight) * sr.normalizedPosition.y - contentHeight; float upperVisible = lowerVisible + viewportHeight; // is our item visible right now? float desiredLowerBound; if (upperBound > upperVisible) { // need to scroll up to upperBound desiredLowerBound = upperBound - viewportHeight + eventData.selectedObject.GetComponent<RectTransform>().rect.height * SCROLL_MARGIN; } else if (lowerBound < lowerVisible) { // need to scroll down to lowerBound desiredLowerBound = lowerBound - eventData.selectedObject.GetComponent<RectTransform>().rect.height * SCROLL_MARGIN; } else { // item already visible - all good return; } // normalize and set the desired viewport float normalizedDesired = (desiredLowerBound + contentHeight) / (contentHeight - viewportHeight); sr.normalizedPosition = new Vector2(0f, Mathf.Clamp01(normalizedDesired)); } } |
Put this script on the Template
GameObject that is child to the Dropdown. This GameObject should already hold a Scroll Rect
component. Please note that your InputModule must call ExecuteEvents.ExecuteHierarchy()
for UpdateSelected events (not just ExecuteEvents.Execute()), so that they bubble up from the dropdown’s items to the ScrollRect!
Remarks
- The code above of course is kept extra-verbose to make things clear. Naturally, there is potential for optimisation!
- Speaking of optimisation: The method will be executed every tick of the UI system, which might be unnecessary in most cases. However, it doesn’t really hurt to call it that often and it should not be an issue regarding performance. If it is, you might listen for move/submit events rather than UpdateSelected.
- If you want to go for a slightly neater class layout, you may consider to extend the Dropdown behaviour itself and have the events bubble up one more level. For this, change the code in
Awake()
accordingly and also check theselectedObject
is not the Dropdown itself inOnUpdateSelected()
. And of course the eternal classic: don’t forget to call the base class’ method. - This is not an animating solution, but that can easily be achieved with a little bit of Lerp magic. Just don’t forget to stop and/or update your target position if there is new input by the user.
I found a neat solution to getting this to OnUpdateSelected was to override the Toggle selectable in the Item object that’s a child of Template.
///
/// Used in conjunction with the event sensitive scrollrect to move it with the controller.
///
public class DropdownMover : Toggle
{
public EventSensitiveScrollRect e;
protected override void Reset()
{
base.Awake();
e = GetComponentInParent();
}
public override void OnSelect(BaseEventData eventData)
{
base.OnSelect(eventData);
e.OnUpdateSelected(eventData);
}
}
Worth noting, this won’t actually add the EventSensitiveScrollRect unless you make Template active before adding this component since GetComponentInParent only works on active gameobjects, but this avoids sending the callback every update.
Thank you so much, this has been really helpful!
Hello, I am experiencing the same issue with the dropdown menu, but i cant get this script to work, and i think its because i am using the new Input System because i wanted to have controller support. I have no idea how to fix this and it is driving me crazy. If you have any idea how I can fix this i would be so grateful.
The code above was made for Unity 5.3, long before the new input system was implemented. Without knowing the latter too well I would also assume that this solution does not work with current versions. Since then I have abandoned Unity’s own input system altogether, so unfortunately I can’t be of too much assistance anymore.
I currently use 2021* and the drop downs do not automatically scroll. I did find this script that works perfectly.
https://gist.github.com/mandarinx/eae10c9e8d1a5534b7b19b74aeb2a665