Custom reorderable list implementation produces jittery/flickering during drag and drop

I’m trying to implement a custom reorderable list. My specific goal is to drag an item so that other items move out of the way (the same way the builtin Unity ReorderableList for Array works). I would prefer not to use a ghost element (keep the original static and show a half transparent preview).

I based my code on the TexureDragger example. My implementation has the correct result, but it jitters and produces a flickering motion. See this animated Gif:

7743645--973500--JitteryDragReorder.gif

I use target.transform.position to drag move the element and then reorder the hierarchy via PlaceBehind. Apparently, when the reordering of the hierarchy happens, there is one or two frames of time in which the layout engine calculates a different position for my element. Then, when I continue to move the mouse again, the position is fixed.

What is my mistake here? Is my approach incorrect of moving the transform position or is it simply a math mistake? Or is the issue that layout rebuilding takes longer than a single frame and is more or less async in between the MouseMove events?

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public class DragReorderTest : EditorWindow
{
    [MenuItem("Test/Drag Reorder")]
    public static void Open()
    {
        GetWindow<DragReorderTest>("Drag Reorder");
    }

    private void CreateGUI()
    {
        rootVisualElement.Add(CreateBox(Color.red));
        rootVisualElement.Add(CreateBox(Color.green));
        rootVisualElement.Add(CreateBox(Color.blue));
    }

    private static VisualElement CreateBox(Color color)
    {
        var box = new VisualElement();

        box.AddManipulator(new DragReorderManipulator());

        box.style.width = 50;
        box.style.height = 50;
        box.style.marginTop = 4;
        box.style.marginRight = 4;
        box.style.marginBottom = 4;
        box.style.marginLeft = 4;
        box.style.backgroundColor = color;

        return box;
    }

    /// <summary>
    /// This manipulator allows dragging a VisualElement and reorders
    /// its siblings when they are presented in a list of elements in a container.
    /// </summary>
    /// <example>
    /// Container
    ///  [a]
    ///  [b]
    ///  [c]
    ///
    /// Container
    ///  [b]
    ///  [c]
    ///  [a]
    /// </example>
    private class DragReorderManipulator : MouseManipulator
    {
        private bool dragging;
        private Vector2 startMousePosition;
        private Vector3 startTargetPosition;

        public DragReorderManipulator()
        {
            activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
            dragging = false;
        }

        protected override void RegisterCallbacksOnTarget()
        {
            this.target.AddToClassList("draggable");
            target.RegisterCallback<MouseDownEvent>(OnMouseDown);
            target.RegisterCallback<MouseMoveEvent>(OnMouseMove);
            target.RegisterCallback<MouseUpEvent>(OnMouseUp);
        }

        protected override void UnregisterCallbacksFromTarget()
        {
            this.target.RemoveFromClassList("draggable");
            target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
            target.UnregisterCallback<MouseMoveEvent>(OnMouseMove);
            target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
        }

        private void OnMouseDown(MouseDownEvent e)
        {
            if (dragging)
            {
                e.StopImmediatePropagation();
                return;
            }

            if (CanStartManipulation(e))
            {
                startMousePosition = e.localMousePosition;
                startTargetPosition = target.transform.position;

                dragging = true;
                target.CaptureMouse();
                e.StopPropagation();
            }
        }

        private void OnMouseMove(MouseMoveEvent e)
        {
            if (!dragging || !target.HasMouseCapture())
                return;

            e.StopPropagation();

            DragMoveTarget(e);
            ReorderHierarchy();
        }

        private void DragMoveTarget(MouseMoveEvent e)
        {
            // This code was inspired by the TextureDragger example.
            Vector2 diff = e.localMousePosition - startMousePosition;

            // But instead of setting the style I set the transform position for simplicity.
            target.transform.position += (Vector3)diff;

            // However, code like this doesn't work at all (it jitters wildly).
            // But I understand why it does that.
            //target.transform.position = e.localMousePosition;
        }

        private void ReorderHierarchy()
        {
            // Coordinate system: Top is zero, towards bottom is positive.

            // Find the element which is closest but below our current target.
            VisualElement closest = null;
            float closestOffset = float.NegativeInfinity;

            target.parent.Query(null, className: "draggable").ForEach(element =>
            {
                if (element == target)
                    return;

                float offset = target.worldBound.center.y - element.worldBound.center.y;

                if (offset < 0 && offset > closestOffset)
                {
                    // This means the element is below the target, but closer
                    // than the previously found one.
                    closest = element;
                    closestOffset = offset;
                }
            });

            if (closest != null)
            {
                // Place the target above the closest image.
                // BUG: This is where the flickering/jitter issue happens.
                // After the hierarchy was changed, it seems the layout
                // rebuilds and moves some elements to weird places until
                // the next MouseMoveEvent is called and the position is fixed again.
                target.PlaceBehind(closest);
            }
            else
            {
                // Append to the end of the list.
                target.parent.Add(target);
            }
        }

        private void OnMouseUp(MouseUpEvent e)
        {
            if (!dragging || !target.HasMouseCapture() || !CanStopManipulation(e))
                return;

            dragging = false;
            target.ReleaseMouse();
            e.StopPropagation();

            // Since this is a local position, this simply resets the drag offset
            // and puts the target back into the layouted position within its container.
            target.transform.position = startTargetPosition;
        }
    }
}

Thank you for any insights! :slight_smile:

The attached script is the editor window for testing, simply drop it into a project and open via the main menu Test > Drag Reorder.
Unity 2020.3.17f1

7743645–973506–DragReorderTest.cs (4.41 KB)

Hello, not fully working your method out, one guess would be that the moment the positions become changed the transform position from your target is relative to the position inside the parent. So when it moves up and becomes placed behind for the next frame the transform position offset is still the old one? I dont know if that makes sense :smile: