Drag and Drop In ScrollRect (ScrollView) in Unity3D

11,078

Solution 1

Answer:

I wrote some codes that handle drag and drop in scrollRect(scrollView) without using DragHandler interfaces.

DragHandler:

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

public class DragHandler : MonoBehaviour, IPointerExitHandler
{
    public static GameObject itemBeingDragged;

    public static bool isCustomerDragged;

    public Transform customerScrollRect;
    public Transform dragParent;

    public float holdTime;
    public float maxScrollVelocityInDrag;

    private Transform startParent;

    private ScrollRect scrollRect;

    private float timer;

    private bool isHolding;
    private bool canDrag;
    private bool isPointerOverGameObject;

    private CanvasGroup canvasGroup;

    private Vector3 startPos;

    public Transform StartParent
    {
        get { return startParent; }
    }

    public Vector3 StartPos
    {
        get { return startPos; }
    }

    void Awake()
    {
        canvasGroup = GetComponent<CanvasGroup>();
    }

    // Use this for initialization
    void Start()
    {
        timer = holdTime;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (EventSystem.current.currentSelectedGameObject == gameObject)
            {
                //Debug.Log("Mouse Button Down");
                scrollRect = customerScrollRect.GetComponent<ScrollRect>();
                isPointerOverGameObject = true;
                isHolding = true;
                StartCoroutine(Holding());
            }
        }

        if (Input.GetMouseButtonUp(0))
        {
            if (EventSystem.current.currentSelectedGameObject == gameObject)
            {
                //Debug.Log("Mouse Button Up");
                isHolding = false;

                if (canDrag)
                {
                    itemBeingDragged = null;
                    isCustomerDragged = false;
                    if (transform.parent == dragParent)
                    {
                        canvasGroup.blocksRaycasts = true;
                        transform.SetParent(startParent);
                        transform.localPosition = startPos;
                    }
                    canDrag = false;
                    timer = holdTime;
                }
            }
        }

        if (Input.GetMouseButton(0))
        {
            if (EventSystem.current.currentSelectedGameObject == gameObject)
            {
                if (canDrag)
                {
                    //Debug.Log("Mouse Button");
                    transform.position = Input.mousePosition;
                }
                else
                {
                    if (!isPointerOverGameObject)
                    {
                        isHolding = false;
                    }
                }
            }
        }
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        isPointerOverGameObject = false;
    }

    IEnumerator Holding()
    {
        while (timer > 0)
        {
            if (scrollRect.velocity.x >= maxScrollVelocityInDrag)
            {
                isHolding = false;
            }

            if (!isHolding)
            {
                timer = holdTime;
                yield break;
            }

            timer -= Time.deltaTime;
            //Debug.Log("Time : " + timer);
            yield return null;
        }

        isCustomerDragged = true;
        itemBeingDragged = gameObject;
        startPos = transform.localPosition;
        startParent = transform.parent;
        canDrag = true;
        canvasGroup.blocksRaycasts = false;
        transform.SetParent(dragParent);
    }

    public void Reset()
    {
        isHolding = false;
        canDrag = false;
        isPointerOverGameObject = false;
    }
}

Some explanation for this piece of code :

  1. Your draggable UI element need intractable option, for me, I used button.
  2. You need to attach this script to your draggable item.
  3. Also you need add Canvas Group component.
  4. customerScrollRect is a ScrollRect parent of your items.
  5. dragParent can be a empty GameObject which is used because of mask of view port.

Slot:

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

public class Slot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    bool isEntered;

    public void OnPointerEnter(PointerEventData eventData)
    {
        isEntered = true;
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        isEntered = false;
    }

    void Update()
    {
        if (Input.GetMouseButtonUp(0))
        {
            if (isEntered)
            {
                if (DragHandler.itemBeingDragged)
                {
                    GameObject draggedItem = DragHandler.itemBeingDragged;
                    DragHandler dragHandler = draggedItem.GetComponent<DragHandler>();
                    Vector3 childPos = draggedItem.transform.position;
                    //Debug.Log("On Pointer Enter");
                    draggedItem.transform.SetParent(dragHandler.StartParent);
                    draggedItem.transform.localPosition = dragHandler.StartPos;
                    draggedItem.transform.parent.SetParent(transform);
                    draggedItem.transform.parent.position = childPos;
                    isEntered = false;
                }
            }
        }
    }
}

Some explanation for this script:

1.Attach the script to the dropped item.

Solution 2

The easiest solution for this problem is actually to manually call the ScrollRect's events IF the user hasn't pressed long enough using ExecuteEvents.Execute. This solution has the least amount of additional code.

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

public class DragAndDropHandler : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerExitHandler, IDragHandler, IBeginDragHandler, IEndDragHandler
{
    public bool Draggable { get; set; }

    private bool draggingSlot;

    [SerializeField] private ScrollRect scrollRect;

    public void OnPointerDown(PointerEventData eventData)
    {
        if (!Draggable)
        {
            return;
        }

        StartCoroutine(StartTimer());
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        StopAllCoroutines();
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        StopAllCoroutines();
    }

    private IEnumerator StartTimer()
    {
        yield return new WaitForSeconds(0.5f);
        draggingSlot = true;
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        ExecuteEvents.Execute(scrollRect.gameObject, eventData, ExecuteEvents.beginDragHandler);
    }

    public void OnDrag(PointerEventData eventData)
    {
        if (draggingSlot)
        {
            //DO YOUR DRAGGING HERE
        } else
        {
            //OR DO THE SCROLLRECT'S
            ExecuteEvents.Execute(scrollRect.gameObject, eventData, ExecuteEvents.dragHandler);
        }
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        ExecuteEvents.Execute(scrollRect.gameObject, eventData, ExecuteEvents.endDragHandler);
        if (draggingSlot)
        {
            //END YOUR DRAGGING HERE
            draggingSlot = false;
        }
    }
}

Solution 3

I've managed to find an easier solution. Maybe it must be customized by special needs, but if you put this script on your items and make the setup, it should work.

It's not easy from a prefab, but I let a controller make this. I find all UIElementDragger inside a specified GameObject and add the needed GO instances programmatically.

But can use this code out of the box if you don't use prefabs.

using UnityEngine;
using UnityEngine.EventSystems;

public class UIElementDragger : MonoBehaviour, IPointerUpHandler, IPointerDownHandler
{
    /// <summary>
    /// Offset in pixels horizontally (positive to right, negative to left)
    /// </summary>
    [Range(40, 100)]
    public float offsetX = 40;

    /// <summary>
    /// Offset in pixels vertically (positive to right, negative to left)
    /// </summary>
    [Range(40, 100)]
    public float offsetY = 40;

    /// <summary>
    /// The Panel where the item will set as Child to during drag
    /// </summary>
    public Transform parentRect;

    /// <summary>
    /// The GameObject where the item is at start
    /// </summary>
    public Transform homeWrapper;

    /// <summary>
    /// The Object where the mouse must be when pointer is up, to put it in this panel
    /// </summary>
    public Transform targetRect;

    /// <summary>
    /// The GameObject where the item should live after dropping
    /// </summary>
    public Transform targetWrapper;

    private int siblingIndex;
    private bool dragging;

    private void Start()
    {
        siblingIndex = transform.GetSiblingIndex();
    }

    private void Update()
    {
        if (dragging)
        {
            transform.position = new Vector2(Input.mousePosition.x + offsetX, Input.mousePosition.y + offsetY);
        }
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        transform.parent = parentRect;
        dragging = true;
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        if (eventData.pointerCurrentRaycast.gameObject.transform.IsChildOf(targetRect))
        {
            transform.parent = targetWrapper;
        }
        else
        {
            transform.parent = homeWrapper;
            transform.SetSiblingIndex(siblingIndex);
        }

        dragging = false;
    }
}
Share:
11,078
ATHellboy
Author by

ATHellboy

Hi! I'm Alireza, I'm a game programmer with more than 6 years' experience. I’m always eager to learn new things, leverage my skills and take on challenges, and I always like to put some extra time in polishing my creations. I strive to write clean code based on architecture and design patterns. I'm serious about brainstorming sessions, teamwork and following deadlines. What makes me passionate about making games are penchant of playing video games, combination of technology and creativity and creating moments of joy for people and myself.

Updated on June 04, 2022

Comments

  • ATHellboy
    ATHellboy about 2 years

    I want to implement drag and drop on contents of scroll view.

    The problem is when you try drag items in scroll view you can't scroll the view.

    First, I've tried to implement drag and drop by IDragHandler, IBeginDragHandler, IEndDragHandle and IDropHandler interfaces. In a first sight, It worked pretty good but the problem was you can't scroll the ScrollRect.

    I think the problem is because of overriding, When I use event triggers that the same as scroll rect like drag, the parent one don't work properly.

    So after that, I've thought by myself and implement it by IPointerDown, IPointerUp interfaces and specific time for holding drag-gable UI in ScrollRect and if you don't hold it in specific time the scrolling work well.

    But the problem is by enabling DragHandler script that I wrote before the OnDrag, OnBeginDrag and OnEndDrag functions doesn't work when time of holding ended.

    First I want to know there is any way to call these functions ?

    Second is there any way to implement drag and drop UI without using drag interfaces ?

    DragHandler :

    using System;
    using UnityEngine;
    using UnityEngine.EventSystems;
    
    public class DragHandler : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler
    {
        public static GameObject itemBeingDragged;
    
        private Vector3 startPos;
    
        private Transform startParent;
    
        DragHandler dragHandler;
    
        public void Awake()
        {
            dragHandler = GetComponent<DragHandler>();
        }
    
        public void OnBeginDrag(PointerEventData eventData)
        {
            Debug.Log("Begin");
            itemBeingDragged = gameObject;
            startPos = transform.position;
            startParent = transform.parent;
        }
    
        public void OnDrag(PointerEventData eventData)
        {
            Debug.Log("Drag");
            transform.position = Input.mousePosition;
        }
    
        public void OnEndDrag(PointerEventData eventData)
        {
            Debug.Log("End");
            itemBeingDragged = null;
            if (transform.parent == startParent)
            {
                dragHandler.enabled = false;
                transform.SetParent(startParent);
                transform.position = startPos;
            }
        }
    }
    

    ScrollRectController:

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.EventSystems;
    using UnityEngine.UI;
    
    public class ScrollRectController : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
    {
        public float holdTime;
        public float maxVelocity;
    
        private Transform scrollRectParent;
    
        private DragHandler dragHandler;
    
        private ScrollRect scrollRect;
    
        private float timer;
    
        private bool isHolding;
    
        void Awake()
        {
            scrollRectParent = GameObject.FindGameObjectWithTag("rec_dlg").transform;
            dragHandler = GetComponent<DragHandler>();
            dragHandler.enabled = false;
        }
    
        // Use this for initialization
        void Start()
        {
            timer = holdTime;
        }
    
        // Update is called once per frame
        void Update()
        {
    
        }
    
        public void OnPointerDown(PointerEventData eventData)
        {
            Debug.Log("Down");
            scrollRect = scrollRectParent.GetComponent<ScrollRect>();
            isHolding = true;
            StartCoroutine(Holding());
        }
    
        public void OnPointerUp(PointerEventData eventData)
        {
            Debug.Log("Up");
            isHolding = false;
        }
    
        IEnumerator Holding()
        {
            while (timer > 0)
            {
                //if (scrollRect.velocity.x >= maxVelocity)
                //{
                //    isHolding = false;
                //}
    
                if (!isHolding)
                {
                    timer = holdTime;
                    yield break;
                }
    
                timer -= Time.deltaTime;
                Debug.Log(timer);
                yield return null;
            }
    
            dragHandler.enabled = true;
            //dragHandler.OnBeginDrag();
        }
    }
    

    Slot:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.EventSystems;
    
    public class Slot : MonoBehaviour, IDropHandler
    {
        public void OnDrop(PointerEventData eventData)
        {
            DragHandler.itemBeingDragged.transform.SetParent(transform);
        }
    }