Swap elements in a GridView

2019-05-30 18:54发布

I'm writing a QML program that is essentially a 4x4 GridView filled with numbered rectangles. I would like to be able to:

  1. Swap two elements from the grid, dragging and dropping

  2. Allow swap only for directly adjacent elements

My current problem is that as soon as I drag an element on top of another, the whole grid adjusts positions filling the gap where the element originally was. Is there any way to avoid the auto adjust behavior for that type of grid?

I'm aware that the piece of code below might be the one responsible for this behavior, I just can't figure out how to change it the propper way.

DropArea {
            anchors { fill: parent; margins: 15 }
            onEntered: {visualModel.items.move(drag.source.visualIndex, delegateRoot.visualIndex)}
        }

Full code:

import QtQuick 2.0
import QtQml.Models 2.1

GridView {
    id: root
    width: 320; height: 480
    cellWidth: 80; cellHeight: 80

    displaced: Transition {
        NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }//Animação anima a transicao dos tiles
    }

    model: DelegateModel {
        id: visualModel
        model: ListModel {
            id: colorModel
            ListElement { color: "lightsteelblue" ; text: "1" }
            ListElement { color: "lightsteelblue" ; text: "2" }
            ListElement { color: "lightsteelblue" ; text: "3" }
            ListElement { color: "lightsteelblue" ; text: "4" }
            ListElement { color: "lightsteelblue" ; text: "5" }
            ListElement { color: "lightsteelblue" ; text: "6" }
            ListElement { color: "lightsteelblue" ; text: "7" }
            ListElement { color: "lightsteelblue" ; text: "8" }
            ListElement { color: "lightsteelblue" ; text: "9" }
            ListElement { color: "lightsteelblue" ; text: "10" }
            ListElement { color: "lightsteelblue" ; text: "11" }
            ListElement { color: "lightsteelblue" ; text: "12" }
            ListElement { color: "lightsteelblue" ; text: "13" }
            ListElement { color: "lightsteelblue" ; text: "14" }
            ListElement { color: "lightsteelblue" ; text: "15" }
            ListElement { color: "transparent"  }
        }

        delegate: MouseArea {
            id: delegateRoot

            property int visualIndex: DelegateModel.itemsIndex

            width: 80; height: 80
            drag.target: icon

            Rectangle {
                id: icon
                Text {
                   text: model.text
                   font.pointSize: 30
                   anchors.centerIn: parent
                }
                width: 72; height: 72
                anchors {
                    horizontalCenter: parent.horizontalCenter;
                    verticalCenter: parent.verticalCenter
                }
                color: model.color
                radius: 3

                Drag.active: delegateRoot.drag.active
                Drag.source: delegateRoot
                Drag.hotSpot.x: 36
                Drag.hotSpot.y: 36

                states: [
                    State {
                        when: icon.Drag.active
                        ParentChange {
                            target: icon
                            parent: root
                        }

                        AnchorChanges {
                            target: icon;
                            anchors.horizontalCenter: undefined;
                            anchors.verticalCenter: undefined
                        }
                    }
                ]
            }

            DropArea {
                anchors { fill: parent; margins: 15 }
                onEntered: {visualModel.items.move(drag.source.visualIndex, delegateRoot.visualIndex)}
            }
        }
    }
}

2条回答
beautiful°
2楼-- · 2019-05-30 19:34

I tried something but still few bugs here and there. Hope it helps.

import QtQuick 2.0
import QtQml.Models 2.1

GridView {
id: root
width: 320; height: 480
cellWidth: 80; cellHeight: 80

displaced: Transition {
    NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }//Animação anima a transicao dos tiles
}

model: DelegateModel {
    id: visualModel
    model: ListModel {
        id: colorModel
        ListElement { color: "lightsteelblue" ; text: "1" }
        ListElement { color: "lightsteelblue" ; text: "2" }
        ListElement { color: "lightsteelblue" ; text: "3" }
        ListElement { color: "lightsteelblue" ; text: "4" }
        ListElement { color: "lightsteelblue" ; text: "5" }
        ListElement { color: "lightsteelblue" ; text: "6" }
        ListElement { color: "lightsteelblue" ; text: "7" }
        ListElement { color: "lightsteelblue" ; text: "8" }
        ListElement { color: "lightsteelblue" ; text: "9" }
        ListElement { color: "lightsteelblue" ; text: "10" }
        ListElement { color: "lightsteelblue" ; text: "11" }
        ListElement { color: "lightsteelblue" ; text: "12" }
        ListElement { color: "lightsteelblue" ; text: "13" }
        ListElement { color: "lightsteelblue" ; text: "14" }
        ListElement { color: "lightsteelblue" ; text: "15" }
        ListElement { color: "transparent" ; text:"" }
    }

    delegate: MouseArea {
        id: delegateRoot
        property bool held: false
        property int visualIndex: DelegateModel.itemsIndex

        width: 80; height: 80
        drag.target: held ? icon : undefined
        drag.axis: Drag.XAndYAxis
        drag.minimumX: delegateRoot.x-75
        drag.minimumY: delegateRoot.y-75
        drag.maximumX: delegateRoot.x + 85
        drag.maximumY: delegateRoot.y + 85

        onPressed: {
                            held = true
                            icon.opacity = 0.5
                        }
                        onReleased: {
                            if (held === true) {
                                held = false
                                icon.opacity = 1
                                icon.Drag.drop()
                            } else {
                                //action on release
                            }
                        }

        Rectangle {
            id: icon
            Text {
               text: model.text
               font.pointSize: 30
               anchors.centerIn: parent
            }
            width: 72; height: 72
            anchors {
                horizontalCenter: parent.horizontalCenter;
                verticalCenter: parent.verticalCenter
            }
            color: model.color
            radius: 3

            Drag.active: delegateRoot.drag.active
            Drag.source: delegateRoot
            Drag.hotSpot.x: 36
            Drag.hotSpot.y: 36
            states: [
                State {
                    when: icon.Drag.active
                    ParentChange {
                        target: icon
                        parent: root
                    }

                    AnchorChanges {
                        target: icon;
                        anchors.horizontalCenter: undefined;
                        anchors.verticalCenter: undefined
                    }
                }
            ]
        }

        DropArea {
            anchors {
                                   fill: parent
                                   margins: 15
                    }
            onDropped: {
                var sourceNumber = colorModel.get(drag.source.visualIndex).text;
                var targetNumber = colorModel.get(delegateRoot.visualIndex).text;
                var sourceColor = colorModel.get(drag.source.visualIndex).color;
                var targetColor = colorModel.get(delegateRoot.visualIndex).color;
                colorModel.setProperty(drag.source.visualIndex, "text", targetNumber);
                colorModel.setProperty(delegateRoot.visualIndex, "text", sourceNumber);
                colorModel.setProperty(drag.source.visualIndex, "color", targetColor);
                colorModel.setProperty(delegateRoot.visualIndex, "color", sourceColor);
            }
        }
    }
}
}
查看更多
We Are One
3楼-- · 2019-05-30 19:49

Here is an implementation of DropArea that specialises for swapping adjacent elements in a 4x4 grid. See below for an explanation.

DropArea {
    id: dropArea

    anchors { fill: parent; margins: 15 }
    onEntered: {
        //  store as local variables
        var from = drag.source.visualIndex;
        var to = delegateRoot.visualIndex;

        console.log(from, "-->", to);

        //  `isAdjacent` is a function implemented below
        if (isAdjacent(from, to))
            console.warn("Yes, adjacent.");
        else {
            console.warn("No, not adjacent.");

            //  jump the gun, we don't care if they're not adjacent
            return;
        }

        //  normal move
        visualModel.items.move(from, to);
        // visualModel.items.move(drag.source.visualIndex, delegateRoot.visualIndex);   //  this is the same as the line above


        //  if `from`/`to` are horizontally adjacent (left/right)
        //  then the move is already valid

        if (from % 4 < 3 && from + 1 === to)    //  check `to` is right of `from`
            return;
        if (from % 4 > 0 && from - 1 === to)    //  check `to` is left of `from`
            return;


        //  move for vertically adjacent
        if (from < 12 && from + 4 === to)   //  check `to` is below `from`
            visualModel.items.move(to - 1, from);   // CRUCIAL MOVE

        if (from >= 4 && from - 4 === to)   //  check `to` is above `from`
            visualModel.items.move(to + 1, from);   // CRUCIAL MOVE

    }

    function isAdjacent(from, to) {
        if (from % 4 < 3 && from + 1 === to)    //  check `to` is right of `from`
            return true;
        if (from % 4 > 0 && from - 1 === to)    //  check `to` is left of `from`
            return true;
        if (from < 12 && from + 4 === to)   //  check `to` is below `from`
            return true;
        if (from >= 4 && from - 4 === to)   //  check `to` is above from
            return true;

        return false;
    }
}

The actual thought process got rather mathy. But here it is.

How do you check adjacency?

You could probably do a google search and easily find something. But I'll explain the conditions one by one.

//  check `to` is right of `from`
from % 4 < 3         // first make sure that `from` is not on the last column
from + 1 === to      // then check that `to` is on the next tile

//  check `to` is left of `from`
from % 4 > 0         // first make sure that `from` is not on the first column
from - 1 === to      // then check that `to` is on the previous tile

//  check `to` is below `from`
from < 12            // first make sure that `from` is not on the last row
from + 4 === to      // then check that `to` is four tiles to the right
                     // with the grid's wraparound, this will check if `to` is
                     // below `from`

//  check `to` is above from
from >= 4            // first make sure that `from` is not on the first row
from - 4 === to      // then check that `to` is four tiles to the left
                     // with the grid's wraparound, this will check if `to` is
                     // above `from`

How does one derive the Crucial Moves?

I started off by drawing on paper what would happen for a 2x2 grid. For horizontally adjacent elements, there's no problem in the swapping. The only problem occurs in swapping vertically adjacent elements.

Let X(i) -> Y(j) denote an object with display X at index i moving to index j, where index j was originally occupied by an object with display Y. The index of X becomes j, making it X(j) and object Y becomes displaced.

Consider if we want to swap B(1) with D(3) in a 2x2 grid.

+------+------+                   +------+------+
| A(0) | B(1) |    ←  ↖           | A(0) | D(1) |
+------+------+         >   -->   +------+------+
| C(2) | D(3) |    ←  ↙           | C(2) | B(3) |
+------+------+                   +------+------+

>>> User drags B(1) to D(3).

>>> var from = 1;
>>> var to = 3;

Just executing the command
>>> visualModel.items.move(from, to);

will give

B(1) -> D(3) -> C(2) -> (1)

i.e.
Object B goes to index 3. Displaces object D.
Object D goes to index 2. Displaces object C.
Object C goes to index 1, which is empty, since B was already moved.

And the result of the grid is

+------+------+
| A(0) | C(1) |
+------+------+
| D(2) | B(3) |
+------+------+

To achieve the desired result, we need to swap C(1) and D(2).

>>> visualModel.items.move(1, 2);

C(1) -> D(2) -> (1)

The result is shown below.

+------+------+
| A(0) | D(1) |
+------+------+
| C(2) | B(3) |
+------+------+

Now consider swapping in a 3x3 grid.

Consider swapping E(4) with H(7).

+------+------+------+                +------+------+------+
| A(0) | B(1) | C(2) |                | A(0) | B(1) | C(2) |
+------+------+------+                +------+------+------+
| D(3) | E(4) | F(5) |      -->       | D(3) | H(4) | F(5) |
+------+------+------+                +------+------+------+
| G(6) | H(7) | I(8) |                | G(6) | E(7) | I(8) |
+------+------+------+                +------+------+------+

>>> User drags E(4) to H(7)
>>> var from = 4;
>>> var to = 7;
>>> visualModel.items.move(from, to);

E(4) -> H(7) -> G(6) -> F(5) -> (4)

This results in

+------+------+------+
| A(0) | B(1) | C(2) |
+------+------+------+
| D(3) | F(4) | G(5) |
+------+------+------+
| H(6) | E(7) | I(8) |
+------+------+------+

To get our desired result, to get H(6) up to (4),
we need to simulate the user dragging H(6) to (4).

>>> visualModel.items.move(6, 4);

H(6) -> F(4) -> G(5) -> (6)

This achieves our desired result and gives us

+------+------+------+
| A(0) | B(1) | C(2) |
+------+------+------+
| D(3) | H(4) | F(5) |
+------+------+------+
| G(6) | E(7) | I(8) |
+------+------+------+

The crucial move here was with that second move command.
>>> visualModel.items.move(6, 4);

We can generalise that...

Anytime we move an item down an adjacent tile from from to to, all tiles between from + 1 and to will shift to the left in order to fill up the gap.

The item we want to swap becomes displaced to to - 1. Thus, we move to - 1 to from. Thus, we get visualModel.items.move(to - 1, from); for shifting tiles where to is below from.


We've tried dragging with `from` < `to`.
I.e., we dragged from an upper row to a lower row.

But what if we were to drag from a lower row to an upper row?
I.e. `to` < `from`.

The grid and desired result is the same.

+------+------+------+                +------+------+------+
| A(0) | B(1) | C(2) |                | A(0) | B(1) | C(2) |
+------+------+------+                +------+------+------+
| D(3) | E(4) | F(5) |      -->       | D(3) | H(4) | F(5) |
+------+------+------+                +------+------+------+
| G(6) | H(7) | I(8) |                | G(6) | E(7) | I(8) |
+------+------+------+                +------+------+------+

But...

>>> User drags H(7) to E(4)

Note: previously, it was "User drags E(4) to H(7)".

Thus,
>>> var from = 7;
>>> var to = 4;
>>> visualModel.items.move(from, to);

H(7) -> E(4) -> F(5) -> G(6) -> (7)

The grid is then

+------+------+------+
| A(0) | B(1) | C(2) |
+------+------+------+
| D(3) | H(4) | E(5) |
+------+------+------+
| F(6) | G(7) | I(8) |
+------+------+------+

This time, simulating (6) moving up to (4), will gives us an incorrect grid.
We want to move E(5) down to (7).

>>> visualModel.items.move(5, 7);

E(5) -> G(7) -> F(6) -> (5)

This gives us

+------+------+------+
| A(0) | B(1) | C(2) |
+------+------+------+
| D(3) | H(4) | F(5) |
+------+------+------+
| G(6) | E(7) | I(8) |
+------+------+------+

Anytime we move an item up an adjacent tile from from to to, all tiles between to + 1 to from will shift to the right in order to fill up the gap.

The item we want to swap becomes displaced to to + 1. Thus, we move to - 1 to from. Thus, we get visualModel.items.move(to + 1, from); for shifting tiles where to is above from.


What if my grid has a variable width and height?

This has been left as an exercise for the reader.

Just kidding, all you need to do is to change the condition checking.

if (from % width < width - 1 && from + 1 === to)    //  check `to` is right of `from`
    // ...
if (from % width > 0 && from - 1 === to)    //  check `to` is left of `from`
    // ...
if (from < (width * height - width) && from + width === to) //  check `to` is below `from`
    // ...
if (from >= width && from - width === to)   //  check `to` is above from
    // ...

And this should be able to work with any integer width and height.


Notes

The animation for a vertical swap is not as smooth or transitional as a horizontal swap.


Tedious answer.

查看更多
登录 后发表回答