2

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);
    }
}
ATHellboy
  • 704
  • 3
  • 15
  • 30
  • UGH. Dragging and dropping UI elements is a *huge* pain in the ass with the new UI. It's the one thing that isn't easy. I don't have a project on this computer that uses it right now, but essentially you create a zero-size scroll view for *each* draggable item with the "clamp" mode set to "unbound." These scroll views are then embedded inside the parent container (which can be *another* scroll view) at the anchor point. The per-item scroll view's contents object's transform is the one that's used for positioning. IIRC. If you'd like, take a look at https://github.com/Draco18s/IdleArtificer – Draco18s no longer trusts SE Jun 13 '17 at 13:50
  • Hey, I've checked the link and download the project, dig in it but didn't find anything useful and related to drag and drop in scroll view, I think I need to clarify something, about drag and drop in scroll view, I mean drag item from scroll view to the somewhere else out of scroll view. About tip that you said, I don't think so it solves my problem. – ATHellboy Jun 13 '17 at 18:38
  • Sorry. I ran out of characters. You'll need to download it. Open the main scene. On the left of the "game visible" area I keep copies of the prefabs. Look for the one with an arrow on/next to it. That one, when instanced during the game, is draggable. You'll want to examine the transform hierarchy (including the RectTransform pivot and anchors). They're instantiated by CraftingManager.cs, line 210 while 219 controls where it gets placed on the screen. – Draco18s no longer trusts SE Jun 13 '17 at 18:51
  • Hey man, I've used BuildingGridItem prefab in your project. I added bunch of this prefab to the scroll view and test drag and drop and scrolling but the problem is when mouse pointer is on the BuildingGridItem, scrolling scroll view doesn't work same as my problem. – ATHellboy Jun 13 '17 at 20:08
  • You can either drag an item around *or* you scan scroll the parent scroll view. You can't do both at the same time, it wouldn't make sense. – Draco18s no longer trusts SE Jun 13 '17 at 20:25
  • Yeah but the space of my scroll view is very low to just select those spaces and scroll it, So i think first hold the draggable item after that can drag it and if you don't hold the item, you can scroll the scroll view, my codes that I've sent in my first post show that, but the problem is I can't control OnDrag function after holding time is ended. You know what i mean ? – ATHellboy Jun 13 '17 at 21:06
  • At this point, you're getting into very complex behavior that isn't naively supported. I.e. you need to figure it out for yourself. – Draco18s no longer trusts SE Jun 13 '17 at 21:36
  • Yup, So I try to ask others which know how to handle this problem, Because I don't think, It wasn't done before by someone. – ATHellboy Jun 14 '17 at 05:19

5 Answers5

7

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.

ATHellboy
  • 704
  • 3
  • 15
  • 30
  • 1
    This answer is super useful! One thing that could be improved is preserving/restoring your childIndex, so that the element doesn't get thrown to the bottom of the scrollRect. – Foxor Apr 21 '18 at 14:11
7

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
{
    // Don't forget to set this to TRUE or expose it to the Inspector else it will always be false and the script will not work
    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;
        }
    }
}
wilbil741
  • 115
  • 1
  • 2
  • Using ExecuteEvents is a brilliant answer! Also makes it easy to selectively turn scroll drag on and off (I wanted to drag items out of a scroll rect into my scene). Note that the [legacy documentation](https://docs.unity3d.com/2018.2/Documentation/ScriptReference/EventSystems.ExecuteEvents.html) seems to be more helpful than the [latest documentation](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/api/UnityEngine.EventSystems.ExecuteEvents.html). – idbrii Aug 20 '20 at 15:24
  • This solution works great! But I have to say, for whoever sees this, in its current form, it will not work. Because in OnPointerDown it checks if the object is draggable and if not it blocks the rest of the script. But DRAGGABLE is a property and false by default. So either you expose it to the editor or set its value from another script. Once that's done, you're set and it works great! – Alex Catana May 21 '22 at 17:49
1

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;
    }
}
1

Here my solution, thank all.

I have to use a fake instance to keep the object over even if you move it down the list. I post a vertical and a horizontal solution. I hope it help

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

public class DragController : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
    private RectTransform currentTransform;
    private GameObject instancePrefab;
    private GameObject mainContent;
    private Vector3 initialePosition;
    
    private int totalChild;

    void Awake()
    {
        currentTransform = this.GetComponent<RectTransform>();
        mainContent = currentTransform.parent.gameObject;
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        initialePosition = currentTransform.position;
        totalChild = mainContent.transform.childCount;

        if (instancePrefab == null)
        {
            instancePrefab = Instantiate(this.gameObject, mainContent.transform.parent.transform);
            instancePrefab.GetComponent<Image>().enabled = false;
        }
    }

    public void OnDrag(PointerEventData eventData)
    {
        currentTransform.position = new Vector3(eventData.position.x, currentTransform.position.y, currentTransform.position.z);

        if (instancePrefab != null)
        {
            instancePrefab.GetComponent<Image>().enabled = true;
            currentTransform.GetComponent<Image>().enabled = false;

            instancePrefab.transform.position = currentTransform.position;
        }

        for (int i = 0; i < totalChild; i++)
        {
            if (i != currentTransform.GetSiblingIndex())
            {
                Transform otherTransform = mainContent.transform.GetChild(i);
                int distance = (int)Vector3.Distance(currentTransform.position, otherTransform.position);

                if (distance <= 20)
                {
                    Vector3 otherTransformOldPosition = otherTransform.position;
                    // Vertical
                    /*otherTransform.position = new Vector3(otherTransform.position.x, initialePosition.y,
                        otherTransform.position.z);
                    currentTransform.position = new Vector3(currentTransform.position.x, otherTransformOldPosition.y,
                        currentTransform.position.z);*/
                    // Horizontal 
                    otherTransform.position = new Vector3(initialePosition.x, otherTransform.position.y,
                        otherTransform.position.z);
                    currentTransform.position = new Vector3(otherTransformOldPosition.x, currentTransform.position.y,
                        currentTransform.position.z);

                    currentTransform.SetSiblingIndex(otherTransform.GetSiblingIndex());
                    initialePosition = currentTransform.position;
                }
            }
        }
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        currentTransform.position = initialePosition;
        instancePrefab.GetComponent<Image>().enabled = false;
        currentTransform.GetComponent<Image>().enabled = true;
        Destroy(instancePrefab);
        instancePrefab = null;
        
    }
}
ChucK
  • 11
  • 1
0

You can just manually call ScrollRect events, because they are public. Smthing like this:

public void OnDrag(PointerEventData eventData)
{
    _scrollRect.OnDrag(eventData);
}