ScrollView With Drag Scrolling

Here’s mine with a DRAG_LOGGING feature you can enable if you need it. YMMV:

using UnityEngine;
using UnityEngine.UIElements;

namespace Robants.UI.VisualElements
{
    public class DragScrollView : ScrollView
    {
        public bool Interactable = true;
        public bool ContainsMouse { get; private set; } = false;
        public bool MouseDown { get; private set; } = false;
        public Vector2 ScrollRootOffset { get; private set; }
        public Vector2 MouseDownLocation { get; private set; }

#if DRAG_LOGGING
        static int _nextId;
        int _id;
#endif

        public DragScrollView(ScrollViewMode mode) : base(mode)
        {
            horizontalScrollerVisibility = ScrollerVisibility.Hidden;
            verticalScrollerVisibility = ScrollerVisibility.Hidden;
            DoRegisterCallbacks();
#if DRAG_LOGGING
            _id = _nextId++;
#endif
        }

        VisualElement MouseOwner => this;

        protected virtual void DoRegisterCallbacks()
        {
            MouseOwner.RegisterCallback<MouseUpEvent>(OnMouseUp);
            MouseOwner.RegisterCallback<MouseDownEvent>(OnMouseDown);
            MouseOwner.RegisterCallback<MouseMoveEvent>(OnMouseMove);
        }

        void HandleDrag(IMouseEvent e)
        {
            Vector2 deltaPos = e.mousePosition - MouseDownLocation;
#if DRAG_LOGGING
            Debug.Log($"DragScroll #{_id}: Drag delta = {deltaPos}");
#endif
            scrollOffset = ScrollRootOffset - deltaPos;
        }

        protected virtual void OnMouseMove(MouseMoveEvent e)
        {
            if (MouseDown && Interactable)
            {
                if (MouseCaptureController.HasMouseCapture(MouseOwner))
                    HandleDrag(e);
                else
                {
#if DRAG_LOGGING
                    Debug.Log($"DragScroll #{_id}: Lost Mouse Capture; IsMouseCaptured = {MouseCaptureController.IsMouseCaptured()}");
#endif
                }
            }
            e.StopPropagation();
        }

        protected virtual void OnMouseUp(MouseUpEvent e)
        {
#if DRAG_LOGGING
            Debug.Log($"DragScroll #{_id}: OnMouseUp {e.mousePosition}");
#endif
            MouseCaptureController.ReleaseMouse(MouseOwner);
            MouseDown = false;
            e.StopPropagation();
        }

        protected virtual void OnMouseDown(MouseDownEvent e)
        {
#if DRAG_LOGGING
            Debug.Log($"DragScroll #{_id}: OnMouseDown {e.mousePosition}");
#endif
            if (!worldBound.Contains(e.mousePosition))
            {
#if DRAG_LOGGING
                Debug.Log($"DragScroll #{_id}: Release Mouse {e.mousePosition}");
#endif
                MouseCaptureController.ReleaseMouse(MouseOwner);
            }
            else if (Interactable)
            {
#if DRAG_LOGGING
                Debug.Log($"DragScroll #{_id}: Capture Mouse {e.mousePosition}");
#endif
                MouseOwner.CaptureMouse();
                MouseDownLocation = e.mousePosition;
                ScrollRootOffset = scrollOffset;
                MouseDown = true;
                e.StopPropagation();
            }
        }
    }
}

This was basically a copy/paste from my base draggable element into a scroll view. It’s got a few things in it (like MouseOwner) you can simplify.

If you have passive child elements in your scroll view you need to turn off PickingMode on those to let the clicks get through to the scroll view:

7 Likes

@Arkenhammer Thanks, it seems to be working!

Trying to make draggable scrollview loopable.
If anyone interest, I uploaded a sample project on GitHub. :stuck_out_tongue:
https://github.com/lilacsky824/Unity-UIToolkit-Snapping-Draggable-ScrollView
8808802--1198156--2023-02-15 22-48-40.gif

2 Likes

@Arkenhammer your solution works nicely.
I also did some improvements to handle dragging on children elements and scrolling out of the bound.

Now you should see this Control on the UI Builder.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

namespace UI.Components
{
    public class DragScrollView : ScrollView
    {
        public new class UxmlFactory : UxmlFactory<DragScrollView, UxmlTraits> { }

        public new class UxmlTraits : ScrollView.UxmlTraits
        {
            UxmlBoolAttributeDescription Interactable = new UxmlBoolAttributeDescription { name = "Interactable", defaultValue = true };
            UxmlBoolAttributeDescription IgnoreChildren = new UxmlBoolAttributeDescription { name = "IgnoreChildren", defaultValue = false };

            public UxmlTraits() : base() { Interactable.defaultValue = true; }

            public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
            {
                get
                {
                    yield return new UxmlChildElementDescription(typeof(VisualElement));
                }
            }

            public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
            {
                base.Init(ve, bag, cc);
                ((DragScrollView)ve).Interactable = Interactable.GetValueFromBag(bag, cc);
                ((DragScrollView)ve).IgnoreChildren = IgnoreChildren.GetValueFromBag(bag, cc);
            }
        }

        public bool Interactable = true;
        public bool IgnoreChildren
        {
            get => ignoreChildren; set
            {
                if (ignoreChildren != value)
                {
                    if (value) UnregisterChildrenCallbacks(); else RegisterChildrenCallbacks();
                }
                ignoreChildren = value;
            }
        }
        public bool ContainsMouse { get; private set; } = false;
        public bool MouseDown { get; private set; } = false;
        public Vector2 ScrollRootOffset { get; private set; }
        public Vector2 MouseDownLocation { get; private set; }

        private bool ignoreChildren;
        private List<VisualElement> registered = new();

#if DRAG_LOGGING
    static int _nextId;
    int _id;
#endif

        public DragScrollView() : this(ScrollViewMode.Vertical) { }
        public DragScrollView(ScrollViewMode scrollViewMode) : base(scrollViewMode)
        {
            DoRegisterCallbacks();
        }

        VisualElement MouseOwner => this;

        protected virtual void DoRegisterCallbacks()
        {
            MouseOwner.RegisterCallback<MouseUpEvent>(OnMouseUp);
            MouseOwner.RegisterCallback<MouseDownEvent>(OnMouseDown);
            MouseOwner.RegisterCallback<MouseMoveEvent>(OnMouseMove);

            if (!IgnoreChildren)
            {
                RegisterChildrenCallbacks();
            }
        }
        protected virtual void UnregisterCallbacks()
        {
            MouseOwner.RegisterCallback<MouseUpEvent>(OnMouseUp);
            MouseOwner.RegisterCallback<MouseDownEvent>(OnMouseDown);
            MouseOwner.RegisterCallback<MouseMoveEvent>(OnMouseMove);

            if (!IgnoreChildren)
            {
                UnregisterChildrenCallbacks();
            }
        }

        protected virtual void RegisterChildrenCallbacks()
        {
            List<VisualElement> childrenVE = this.Query().ToList();

            foreach (var b in childrenVE)
            {
                b.RegisterCallback<MouseUpEvent>(OnMouseUp, TrickleDown.TrickleDown);
                b.RegisterCallback<MouseDownEvent>(OnMouseDown, TrickleDown.TrickleDown);
                b.RegisterCallback<MouseMoveEvent>(OnMouseMove, TrickleDown.TrickleDown);
                registered.Add(b);
            }
        }
        protected virtual void UnregisterChildrenCallbacks()
        {
            List<VisualElement> childrenVE = this.Query().ToList();

            foreach (var b in childrenVE)
            {
                b.UnregisterCallback<MouseUpEvent>(OnMouseUp, TrickleDown.TrickleDown);
                b.UnregisterCallback<MouseDownEvent>(OnMouseDown, TrickleDown.TrickleDown);
                b.UnregisterCallback<MouseMoveEvent>(OnMouseMove, TrickleDown.TrickleDown);
                registered.Remove(b);
            }
            registered.Clear();
        }

        void HandleDrag(IMouseEvent e)
        {
            Vector2 deltaPos = e.mousePosition - MouseDownLocation;
#if DRAG_LOGGING
        Debug.Log($"DragScroll #{_id}: Drag delta = {deltaPos}");
#endif

            deltaPos = ScrollRootOffset - deltaPos;
            switch (mode)
            {
                case ScrollViewMode.Vertical:
                    deltaPos.x = scrollOffset.x;
                    break;
                case ScrollViewMode.Horizontal:
                    deltaPos.y = scrollOffset.y;
                    break;
                default:
                    break;
            }
            scrollOffset = deltaPos;
        }

        protected virtual void OnMouseMove(MouseMoveEvent e)
        {
            if (MouseDown && Interactable)
            {
                if (MouseCaptureController.HasMouseCapture(MouseOwner))
                    HandleDrag(e);
                else
                {
#if DRAG_LOGGING
                Debug.Log($"DragScroll #{_id}: Lost Mouse Capture; IsMouseCaptured = {MouseCaptureController.IsMouseCaptured()}");
#endif
                }
            }
            e.StopPropagation();
        }

        protected virtual void OnMouseUp(MouseUpEvent e)
        {
#if DRAG_LOGGING
        Debug.Log($"DragScroll #{_id}: OnMouseUp {e.mousePosition}");
#endif
            MouseCaptureController.ReleaseMouse(MouseOwner);

            // Update elastic behavior
            if (touchScrollBehavior == TouchScrollBehavior.Elastic)
            {
                m_LowBounds = new Vector2(
                    Mathf.Min(horizontalScroller.lowValue, horizontalScroller.highValue),
                    Mathf.Min(verticalScroller.lowValue, verticalScroller.highValue));
                m_HighBounds = new Vector2(
                    Mathf.Max(horizontalScroller.lowValue, horizontalScroller.highValue),
                    Mathf.Max(verticalScroller.lowValue, verticalScroller.highValue));

                ExecuteElasticSpringAnimation();
            }

            MouseDown = false;
            e.StopPropagation();
        }

        protected virtual void OnMouseDown(MouseDownEvent e)
        {
#if DRAG_LOGGING
        Debug.Log($"DragScroll #{_id}: OnMouseDown {e.mousePosition}");
#endif
            if (!worldBound.Contains(e.mousePosition))
            {
#if DRAG_LOGGING
            Debug.Log($"DragScroll #{_id}: Release Mouse {e.mousePosition}");
#endif
                MouseCaptureController.ReleaseMouse(MouseOwner);
            }
            else if (Interactable)
            {
#if DRAG_LOGGING
            Debug.Log($"DragScroll #{_id}: Capture Mouse {e.mousePosition}");
#endif
                MouseOwner.CaptureMouse();
                MouseDownLocation = e.mousePosition;
                ScrollRootOffset = scrollOffset;
                MouseDown = true;
                e.StopPropagation();
            }
        }

        // Copied from Unity scource code: https://github.com/Unity-Technologies/UnityCsReference/blob/master/ModuleOverrides/com.unity.ui/Core/Controls/ScrollView.cs
        private bool hasInertia => scrollDecelerationRate > 0f;
        private Vector2 m_Velocity;
        private Vector2 m_SpringBackVelocity;
        private Vector2 m_LowBounds;
        private Vector2 m_HighBounds;
        private IVisualElementScheduledItem m_PostPointerUpAnimation;

        void ExecuteElasticSpringAnimation()
        {
            ComputeInitialSpringBackVelocity();

            if (m_PostPointerUpAnimation == null)
            {
                m_PostPointerUpAnimation = schedule.Execute(PostPointerUpAnimation).Every(30);
            }
            else
            {
                m_PostPointerUpAnimation.Resume();
            }
        }

        private void PostPointerUpAnimation()
        {
            ApplyScrollInertia();
            SpringBack();

            // This compares with epsilon.
            if (m_SpringBackVelocity == Vector2.zero && m_Velocity == Vector2.zero)
            {
                m_PostPointerUpAnimation.Pause();
            }
        }

        private void ComputeInitialSpringBackVelocity()
        {
            if (touchScrollBehavior != TouchScrollBehavior.Elastic)
            {
                m_SpringBackVelocity = Vector2.zero;
                return;
            }

            if (scrollOffset.x < m_LowBounds.x)
            {
                m_SpringBackVelocity.x = m_LowBounds.x - scrollOffset.x;
            }
            else if (scrollOffset.x > m_HighBounds.x)
            {
                m_SpringBackVelocity.x = m_HighBounds.x - scrollOffset.x;
            }
            else
            {
                m_SpringBackVelocity.x = 0;
            }

            if (scrollOffset.y < m_LowBounds.y)
            {
                m_SpringBackVelocity.y = m_LowBounds.y - scrollOffset.y;
            }
            else if (scrollOffset.y > m_HighBounds.y)
            {
                m_SpringBackVelocity.y = m_HighBounds.y - scrollOffset.y;
            }
            else
            {
                m_SpringBackVelocity.y = 0;
            }
        }

        private void SpringBack()
        {
            if (touchScrollBehavior != TouchScrollBehavior.Elastic)
            {
                m_SpringBackVelocity = Vector2.zero;
                return;
            }

            var newOffset = scrollOffset;

            if (newOffset.x < m_LowBounds.x)
            {
                newOffset.x = Mathf.SmoothDamp(newOffset.x, m_LowBounds.x, ref m_SpringBackVelocity.x, elasticity,
                    Mathf.Infinity, Time.unscaledDeltaTime);
                if (Mathf.Abs(m_SpringBackVelocity.x) < 1)
                {
                    m_SpringBackVelocity.x = 0;
                }
            }
            else if (newOffset.x > m_HighBounds.x)
            {
                newOffset.x = Mathf.SmoothDamp(newOffset.x, m_HighBounds.x, ref m_SpringBackVelocity.x, elasticity,
                    Mathf.Infinity, Time.unscaledDeltaTime);
                if (Mathf.Abs(m_SpringBackVelocity.x) < 1)
                {
                    m_SpringBackVelocity.x = 0;
                }
            }
            else
            {
                m_SpringBackVelocity.x = 0;
            }

            if (newOffset.y < m_LowBounds.y)
            {
                newOffset.y = Mathf.SmoothDamp(newOffset.y, m_LowBounds.y, ref m_SpringBackVelocity.y, elasticity,
                    Mathf.Infinity, Time.unscaledDeltaTime);
                if (Mathf.Abs(m_SpringBackVelocity.y) < 1)
                {
                    m_SpringBackVelocity.y = 0;
                }
            }
            else if (newOffset.y > m_HighBounds.y)
            {
                newOffset.y = Mathf.SmoothDamp(newOffset.y, m_HighBounds.y, ref m_SpringBackVelocity.y, elasticity,
                    Mathf.Infinity, Time.unscaledDeltaTime);
                if (Mathf.Abs(m_SpringBackVelocity.y) < 1)
                {
                    m_SpringBackVelocity.y = 0;
                }
            }
            else
            {
                m_SpringBackVelocity.y = 0;
            }

            scrollOffset = newOffset;
        }

        // Internal for tests.
        internal void ApplyScrollInertia()
        {
            if (hasInertia && m_Velocity != Vector2.zero)
            {
                m_Velocity *= Mathf.Pow(scrollDecelerationRate, Time.unscaledDeltaTime);

                if (Mathf.Abs(m_Velocity.x) < 1 ||
                    touchScrollBehavior == TouchScrollBehavior.Elastic && (scrollOffset.x < m_LowBounds.x || scrollOffset.x > m_HighBounds.x))
                {
                    m_Velocity.x = 0;
                }

                if (Mathf.Abs(m_Velocity.y) < 1 ||
                    touchScrollBehavior == TouchScrollBehavior.Elastic && (scrollOffset.y < m_LowBounds.y || scrollOffset.y > m_HighBounds.y))
                {
                    m_Velocity.y = 0;
                }

                scrollOffset += m_Velocity * Time.unscaledDeltaTime;
            }
            else
            {
                m_Velocity = Vector2.zero;
            }
        }

    }
}
2 Likes

My clicked events for the buttons inside the scrollview do not seem to register unless I use ignore children but then the scrollview does not work. What am I missing? I just stated out with ui toolkit so I might be missing open doors.

Still unsure if I’m not missing something, but this fixed the clicked events of my buttons not coming through. Still needs some testing but I need something that works right now and I’m not sure this is the way so.

        private static IEventHandler clickedObj;
        private long startTime;
        private const long clickTime = 130;

        void CheckClickTime(MouseUpEvent e)
        {  
            if ( (e.timestamp - startTime) < clickTime && clickedObj != null)
            {
                using ( var ev = new NavigationSubmitEvent() {target = clickedObj} ) clickedObj.SendEvent(ev);
            }
            clickedObj = null;
        }

        void SetClickTime(MouseDownEvent e)
        {
            startTime = e.timestamp;
            clickedObj = e.target;
        }

        protected virtual void OnMouseUp(MouseUpEvent e)
        {
#if DRAG_LOGGING
    Debug.Log($"DragScroll #{_id}: OnMouseUp {e.mousePosition}");
#endif

            CheckClickTime(e);
       
            MouseCaptureController.ReleaseMouse(MouseOwner);
            // Update elastic behavior
            if (touchScrollBehavior == TouchScrollBehavior.Elastic)
            {
                m_LowBounds = new Vector2(
                    Mathf.Min(horizontalScroller.lowValue, horizontalScroller.highValue),
                    Mathf.Min(verticalScroller.lowValue, verticalScroller.highValue));
                m_HighBounds = new Vector2(
                    Mathf.Max(horizontalScroller.lowValue, horizontalScroller.highValue),
                    Mathf.Max(verticalScroller.lowValue, verticalScroller.highValue));
                ExecuteElasticSpringAnimation();
            }
            MouseDown = false;
            e.StopPropagation();
        }
        protected virtual void OnMouseDown(MouseDownEvent e)
        {
#if DRAG_LOGGING
Debug.Log($"DragScroll #{_id}: OnMouseDown {e.mousePosition}");
#endif

            SetClickTime(e);
         
            if (!worldBound.Contains(e.mousePosition))
            {
#if DRAG_LOGGING
    Debug.Log($"DragScroll #{_id}: Release Mouse {e.mousePosition}");
#endif

                MouseCaptureController.ReleaseMouse(MouseOwner);

            } else if (Interactable) {
           
#if DRAG_LOGGING
    Debug.Log($"DragScroll #{_id}: Capture Mouse {e.mousePosition}");
#endif

                MouseOwner.CaptureMouse();
                MouseDownLocation = e.mousePosition;
                ScrollRootOffset = scrollOffset;
                MouseDown = true;
                e.StopPropagation();
            }
        }

This is not possible, since a lot of references in this class can only be accessed from inside the package (which since it is integrated into unity, cannot be modified).

I created my own solution a long time ago, where there was not even a drag scrolling for touch implemented.
Now with the Unity version 2022.3.18, there was a bugfix related to the scrollmode “elastic”.
I tried it out and it worked sort of, even with my custom scrolling script.
BUT: the elastic scrolling only works if there was one Touchscroll or Mousewheel action.
Probably because there is some initalisation code on that events.
But since most Methods are private or internal, I can’t access them from outside.

Could you please:
A - Yes this behavior is not the default one, but it is fairly common. Just give us the option to decide for our selfs
B - Give more access for us to create our own extensions (by dont make everything private or internal)
C - Fix the bug :smile:

3 Likes

Apparently it is even worse. The ui toolkit touch scrolling does not work on windows builds:

After weeks of fighting with UI Toolkit in my game prototype and finding too many hidden limitations like this one here, I’m deciding to go back to uGUI.

I gave UI Toolkit a chance after reading a lot about it and watching nice videos. Over these weeks I could confirm how nice it is to work on the UI elements detached from the GameObjects in the scene and would really love to replace uGUI with UI Toolkit one day, but it still has a long path for this to be a reality. :frowning:

In the meantime, I will just keep an eye on its evolution and hope it gets better and better with every update.

3 Likes

Wow. I put off using UI Toolkit for the longest time because I was afraid that it was going to lack basic functionality from uGUI, but it’s hard to believe that it’s still lacking basic functionality after all these years. Day one of moving over our app framework from uGUI, and not only does mouse dragging not work in a scroller but (apparently) it doesn’t work with Windows touchscreens either.

And the official response appears to be “Oh yeah, we intentionally took that feature away instead of just making it optional.”

4 Likes

this is such a stupid response, how is NOT HAVING a basic, STANDARD feature the WANTED BEHAVIOUR

4 Likes

I have just checked in Unity 6 and the behavior is still the same. The scroll view is not responding to mouse drag events in the editor (it does in the device simulator), but I’m developing an app for a Windows Touch Screen…

I noticed the blatant “wanted behavior” in the source code:

image

Is there a workaround to this? Are we forced to rewrite the entire behavior just to handle Windows screens?

6 Likes

It’s ridiculous that we need to jump so many loops to get a behavior that has existed in UGUI since forever. People really need to remember this when next time Unity tries to trick you to switch to their latest shining (but not working) system.

4 Likes

it is %100 correct

2 Likes

I see there is still people suffering for this bs design decision (which would be solved with a simple public boolean)
Now you can use the “TouchSimulation” component. So unity will treat the mouse as a Touch.
What ramifications does this have? i dont know, but probably something equally stupid, time will tell

1 Like

love to see some reasonable people here, uitk should not be the default, for many more years to come. they need a completely blockbuster feature-set to really turn this around, but why bother when ugui is already goat and you could just make the ui diagrams “transpile” into ugui somehow (like typescript transpiles to javascript). not sure this transpiling would be viable, but you could definitely build upon ugui to make it more declarative.
I agree sometimes it’s better to start from scatch, but you only have that luxury of everything else already works well, which is clearly not the case with Unity

Do we know that this will eventualy be fixed? Do they even want to fix this?

I dont know about “wanted behaviour on our side”, since everything works in old ugui. I cant be the only one using mouse in unity editor while developing, and only use touch when i test the app on mobile or windows touch screen?!

UI Toolkit has been life saviour for me, since i make a lot of “ui-heavy” apps, and it is soooo much easier to work with than ugui. But it is so annoying because of couple of small stupid things :cry:

edit:
oh, and also regarding scrollview. i made custom scrollview (with only visualelements as content, because buttons produce problems), and i noticed strange behaviour only when i build the app (for windows touch screen). scroll jumps around weirdly when scrolling. easy fix was just to remove new input system package, and everything worked :thinking:

1 Like