Mouse drag element inside ScrollRect throws PointerUp event

Inside ScrollRect I have UI elements with Monobehaviour scripts which implement OnPointerDown() and OnPointerUp() methods. OnPointerDown() locks scrolling and OnPointerUp() unlocks it.

OnPointerDown() works as intended. It’s called when I click the element.
But when I try to move the mouse while holding mouse button down it immediately throws OnPointerUp event and my OnPointerUp() method is called.

Is it supposed to be so? Shouldn’t OnPointerUp be thrown when I release the mouse button?

Unity 5.2.1p4

Yep I’m right. Without implementing the Drag function, it gets confused and fires the up.

using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
  
public class TestGridCell : MonoBehaviour, IPointerUpHandler, IPointerDownHandler, IDragHandler
{
    public void OnPointerDown(PointerEventData eventData)
    {
        Debug.Log("OnPointerDown was called for object " + gameObject.name);
    }
      
    public void OnPointerUp(PointerEventData eventData)
    {
        Debug.Log("OnPointerUp was called for object " + gameObject.name);
    }
      
    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("Dragging " + gameObject.name);
    }
}

I found a solution for my requirements:

  • Have elements in a scroll rect
  • Players can use the scroll rect normally
  • Players can tap on elements in the scroll rect
  • Players can long-tap on elements in the scroll rect

In essence, you want to implement the interfaces for IDragHandler, IBeginDragHandler, and IEndDragHandler and then pass their events through to the scroll rect (in addition to doing any custom stuff from your code).

Here’s a script I came up with after lots of trial and error, feel free to use / adapt it for your own use:

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

public class TapPanel : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler, IBeginDragHandler, IEndDragHandler
{
    [Header("Settings")]
    public float requiredHoldTime;
    public bool logging = true;

    public UnityEvent onTap;
    public UnityEvent onLongPress;

    private bool pointerDown;
    private float holdTime;
    private bool longPressInvoked = false;

    private ScrollRect scrollRect;
    private bool dragging;

    private void Awake()
    {
        scrollRect = GetComponentInParent<ScrollRect>();
    }

    public void OnDrag(PointerEventData eventData)
    {
        if (scrollRect != null)
        {
            scrollRect.OnDrag(eventData);
        }
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        if (logging) Debug.Log("[TAPPANEL] Begin drag detected.");
        if (scrollRect != null)
        {
            scrollRect.OnBeginDrag(eventData);
        }
        dragging = true;
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        if (logging) Debug.Log("[TAPPANEL] End drag detected.");
        if (scrollRect != null)
        {
            scrollRect.OnEndDrag(eventData);
        }
        dragging = false;
        Reset();
    }

    void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
    {
        if (logging) Debug.Log("[TAPPANEL] Pointer down detected.");
        pointerDown = true;
    }

    void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
    {
        if (logging) Debug.Log("[TAPPANEL] Pointer up detected.");

        if (longPressInvoked)
        {
            longPressInvoked = false;
            pointerDown = false;
            if (logging) Debug.Log("[TAPPANEL] Not invoking anything because long press was invoked previously.");
            return;
        }

        if(dragging)
        {
            if (logging) Debug.Log("[TAPPANEL] Not invoking anything because we were dragging.");
            return;
        }

        if (logging) Debug.Log("[TAPPANEL] Invoking Tap");
        if (onTap != null) onTap.Invoke();
        Reset();
    }

    void Reset()
    {
        pointerDown = false;
        holdTime = 0;
    }

    private void Update()
    {
        if(pointerDown)
        {
            holdTime += Time.deltaTime;

            if(holdTime >= requiredHoldTime && !dragging)
            {
                if (logging) Debug.Log("[TAPPANEL] Invoking Long Press");
                if (onLongPress != null) onLongPress.Invoke();
                longPressInvoked = true;
                Reset();
            }
        }
    }
}

This bug is still active.

This is still a bug in 2021.3.15f1, and implementing IDragHandler with an empty method fixes it.

I have a scroll rect which contains child-buttons that use the PointerUp/Down/Exit events.

The problem is, if I implement the drag handler on the buttons, then the scroll rect will not react on the drag event because the event is intercepted by the buttons.

If I dont implement the drag handler on the buttons, then the scroll rect reacts on the drag event but the buttons fire a pointer up event as soon as i scroll.

Bit of a necro here I know, but there is a really easy to deal with the above with regards to working with the scroll rect without implementing loads of interfaces. All we have to say is the position we touched onPointerDown the same as onPointerUp, if so then we are not scrolling so run the rest of the method.
If you wanted a bit of leway to still click the button you could do a distance check.

Vector2 touchStartPos;
  
public void OnPointerUp(PointerEventData eventData)
{
    if(eventData.position == touchStartPos)
    //Fired on finger release without scrolling
}
  
public void OnPointerDown(PointerEventData eventData)
{
    touchStartPos = eventData.position;
}

Just wanted to add this here. This is not a Unity Bug. You just need to use execute event in hierarchy that Unity already provides. I faced a similar issue of it firing on pointer up when I clicked and dragged off the scroll rect. Solution is to add on drag, on begin drag, and on end drag. These interface methods need to also be passed to the hierarchy childs using execute events.

    public abstract class UIObject : Atom, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler, IBeginDragHandler, IEndDragHandler, IDragHandler
    {
        // Actions:
        public Action JustHovered;
        public Action JustDeparted;
        public Action JustPressed;
        public Action JustReleased;
        public Action JustReleasedAndHovering;
        public Action OnEnable;
        public Action OnDisable;

        // References:
        protected Graphic _raycastObject;
        [HideInInspector] public UIWidget parent;

        // States:
        public bool IsHovering => _isHovering;
        protected bool _isHovering;
        protected bool _isDragging;

        protected override void BeginPlay()
        {
            base.BeginPlay();
            _raycastObject = GetComponent<Graphic>();
        }

        public virtual void OnPointerEnter(PointerEventData eventData)
        {
            if (!_isDragging)
            {
                _isHovering = true;
                JustHovered?.Invoke();
            }
        }

        public virtual void OnPointerExit(PointerEventData eventData)
        {
            _isHovering = false;
            JustDeparted?.Invoke();
        }

        public virtual void OnPointerDown(PointerEventData eventData)
        {
            if (_isHovering)
            {
                _isDragging = false; // Reset dragging status
                JustPressed?.Invoke();
            }
        }

        public virtual void OnPointerUp(PointerEventData eventData)
        {
            JustReleased?.Invoke();
            if (!_isDragging && _isHovering) JustReleasedAndHovering?.Invoke();
        }

        public virtual void OnDrag(PointerEventData eventData)
        {
            ExecuteEvents.ExecuteHierarchy(transform.parent.gameObject, eventData, ExecuteEvents.dragHandler);
        }

        public virtual void OnBeginDrag(PointerEventData eventData)
        {
            _isDragging = true;
            ExecuteEvents.ExecuteHierarchy(transform.parent.gameObject, eventData, ExecuteEvents.beginDragHandler);
        }

        public virtual void OnEndDrag(PointerEventData eventData)
        {
            _isDragging = false;
            ExecuteEvents.ExecuteHierarchy(transform.parent.gameObject, eventData, ExecuteEvents.endDragHandler);
        }

        public virtual void Enable()
        {
            OnEnable?.Invoke();
            _raycastObject.raycastTarget = true;
        }

        public virtual void Disable()
        {
            OnDisable?.Invoke();
            _raycastObject.raycastTarget = false;
        }
    }