Этот пост участвует в конкурсе „Умные телефоны за умные посты“.
Drag'n'Drop является неоспоримо важным элементом взаимодействия пользователя и графического окружения. К сожалению, в QML нет встроенного механизма Drag'n'Drop для View. Поэтому, я написал небольшой пример на основе GridView с 16 изображениями.
Этот пример Drag'n'Drop-а не претендует на совершенство (есть несколько других реализаций, которые визуально возможно более совершенны), а больше преследует цель показать, что QML является очень гибким и простым средством разработки.
Для начала небольшое видео, а под катом 5 простых шагов для получения подобного результата.
Начнем с создания GridView и небольшой модели. В качестве тестовых данных я взял стандартные изображения из Nokia N8.
Сделаем наш грид размером 4х4 изображения.
После запуска в qmlviewer мы увидим примерно следующую картину

Теперь нам нужно добавить в этот грид возможность перетаскивания картинок.
Для упрощения задачи отключим возможность прокрутки GridView. Можно обойтись и без этого (используя долгое нажатие вместо обычного нажатия для D'n'D и манипулируя свойством interactive (которое отвечает за то, будет ли прокручиваться GridView) в соответствующих коллбеках MouseArea), но это усложнит пример.
Также добавим элемент MouseArea размером во весь GridView, который и будет отлавливать нажатие мыши при начале драга, а также перемещать элемент на нужную позицию в модели при отпускании мыши. Плюс к этому, в GridView поместим еще дополнительный элемент dndContainer, о котором поговорим позже
Последним штрихом добавим в наш GridView свойство для хранения текущего перемещаемого элемента (а точнее, его индекса в модели).
В делегат добавим состояние inDrag, которое будет активироваться, когда данный элемент является перемещаемым. Вот тут нам и понадобится dndContainer. К нему мы будем цеплять (а если быть более точным, то менять родителя на этот контейнер) наш перемещаемый элемент. Кроме смены родителя, мы также отвязываем якоря у элемента (чтобы он мог перемещаться) и выставляем x и y соответственно координатам мышки (при чем, благодаря биндингу, положение отвязанной картинки будет меняться с перемещением курсора мыши). Когда состояние станет неактивным, все эти изменения откатятся.
Запустив, увидим примерно вот такую картину. Теперь, мы в нашем гриде можем спокойно перемещать элементы.

Ок, мы научились делать то, ради чего была написана эта статья (все просто, не правда ли?). Но перемещаемый элемент почти не заметен, надо его немного выделить на фоне остальных. Добавим белую рамку вокруг перетаскиваемой картинки.
Вставим вот такой код в наш itemImage.
Плюс к рамочке неплохо бы как-то помечать место, откуда был взят перетаскиваемый элемент. Добавим белый кружок в центре этого места. Этот код помещается рядом с нашим itemImage
Ну и добавим их отображение при начале перетаскивания в состояние inDrag делегата.
Теперь наш пример выглядит уже вот так.

Ну, основа нашего грида с Drag'n'Drop-ом готова. Добавим свистелок. А если более конкретно, то добавим анимацию.
Для начала в наш многострадальный itemImage добавим два состояния:
Таким образом, мы слегка затеняем все элементы, кроме перетаскиваемого, что дает нам большую видимость последнего на фоне остальных.
Также добавим анимированные изменения для прозрачности, ширины и высоты картинки.
В состояние inDrag добавим еще изменение высоты и ширины картинки и переход из этого состояния в любое другое (то есть переход из активного drag'n'drop в обычный режим). В этом переходе сделаем анимацию масштаба.
Также добавим анимации изменения прозрачности к рамке вокруг перетаскиваемого элемента и к кружку на пустом месте.
В итоге получилась уже вот такая картинка.

А это заснято в промежуточный момент, когда отрабатывает анимация.

И, в качестве завершения, добавим индикатор позиции, куда будет перемещен элемент. Отобразим его в виде вертикальной полоски слева от этого элемента.
Родителем этого элемента будет наш GridView, все действия с ним будут происходить там же.
Для начала добавим в GridView три новых свойства: индекс целевого элемента (possibleDropIndex) и текущие координаты мыши (xCoordinateInPossibleDrop и yCoordinateInPossibleDrop).
Плюс добавим сам элемент индикатора. Это обычная картинка 6x1 пикселей с градиентом, растиражированная по вертикали. У индикатора есть два состояния: невидим (по умолчанию) и shown. Во втором состоянии элемент индикатора помещается в промежуток между двумя картинками, слева от цели. Положение элемента рассчитывается на основе двух последних свойств, а не по индексу в модели, тем самым мы не зависим от текущего количества столбцов в таблице.
Также добавим еще один обработчик в MouseArea. Тут нам понадобится свойство с индексом места дропа, чтобы не обновлять каждый раз координаты мышки, а менять их только при смене целевого элемента.
В итоге, получим вот такое приложение. Такое же, как на видео в начале поста :)

Ну и да, скачать полные исходники (с отдельными .qml файлами для каждого шага) можно здесь
Drag'n'Drop является неоспоримо важным элементом взаимодействия пользователя и графического окружения. К сожалению, в QML нет встроенного механизма Drag'n'Drop для View. Поэтому, я написал небольшой пример на основе GridView с 16 изображениями.
Этот пример Drag'n'Drop-а не претендует на совершенство (есть несколько других реализаций, которые визуально возможно более совершенны), а больше преследует цель показать, что QML является очень гибким и простым средством разработки.
Для начала небольшое видео, а под катом 5 простых шагов для получения подобного результата.
Шаг 1. Создаем GridView
Начнем с создания GridView и небольшой модели. В качестве тестовых данных я взял стандартные изображения из Nokia N8.
Сделаем наш грид размером 4х4 изображения.
Rectangle {
width: 420
height: 420
color: "#000000"
Component {
id: dndDelegate
Item {
id: wrapper
width: dndGrid.cellWidth
height: dndGrid.cellHeight
Image {
id: itemImage
source: imagePath
anchors.centerIn: parent
width: 90
height: 90
smooth: true
fillMode: Image.PreserveAspectFit
}
}
}
ListModel {
id: dndModel
ListElement { imagePath: "images/1.jpg" }
//Еще 15 картинок
}
GridView {
id: dndGrid
anchors.fill: parent
anchors.margins: 10
cellWidth: 100
cellHeight: 100
model: dndModel
delegate: dndDelegate
}
}
После запуска в qmlviewer мы увидим примерно следующую картину

Шаг 2. Добавляем Drag'nDrop
Теперь нам нужно добавить в этот грид возможность перетаскивания картинок.
Для упрощения задачи отключим возможность прокрутки GridView. Можно обойтись и без этого (используя долгое нажатие вместо обычного нажатия для D'n'D и манипулируя свойством interactive (которое отвечает за то, будет ли прокручиваться GridView) в соответствующих коллбеках MouseArea), но это усложнит пример.
Также добавим элемент MouseArea размером во весь GridView, который и будет отлавливать нажатие мыши при начале драга, а также перемещать элемент на нужную позицию в модели при отпускании мыши. Плюс к этому, в GridView поместим еще дополнительный элемент dndContainer, о котором поговорим позже
Последним штрихом добавим в наш GridView свойство для хранения текущего перемещаемого элемента (а точнее, его индекса в модели).
property int draggedItemIndex: -1
interactive: false
Item {
id: dndContainer
anchors.fill: parent
}
MouseArea {
id: coords
anchors.fill: parent
onReleased: {
if (dndGrid.draggedItemIndex != -1) {
var draggedIndex = dndGrid.draggedItemIndex
dndGrid.draggedItemIndex = -1
dndModel.move(draggedIndex,dndGrid.indexAt(mouseX, mouseY),1)
}
}
onPressed: {
dndGrid.draggedItemIndex = dndGrid.indexAt(mouseX, mouseY)
}
}
В делегат добавим состояние inDrag, которое будет активироваться, когда данный элемент является перемещаемым. Вот тут нам и понадобится dndContainer. К нему мы будем цеплять (а если быть более точным, то менять родителя на этот контейнер) наш перемещаемый элемент. Кроме смены родителя, мы также отвязываем якоря у элемента (чтобы он мог перемещаться) и выставляем x и y соответственно координатам мышки (при чем, благодаря биндингу, положение отвязанной картинки будет меняться с перемещением курсора мыши). Когда состояние станет неактивным, все эти изменения откатятся.
states: [
State {
name: "inDrag"
when: index == dndGrid.draggedItemIndex
PropertyChanges { target: itemImage; parent: dndContainer }
PropertyChanges { target: itemImage; anchors.centerIn: undefined }
PropertyChanges { target: itemImage; x: coords.mouseX - itemImage.width / 2 }
PropertyChanges { target: itemImage; y: coords.mouseY - itemImage.height / 2 }
}
]
Запустив, увидим примерно вот такую картину. Теперь, мы в нашем гриде можем спокойно перемещать элементы.

Шаг 3. Визуализируем перемещаемый элемент
Ок, мы научились делать то, ради чего была написана эта статья (все просто, не правда ли?). Но перемещаемый элемент почти не заметен, надо его немного выделить на фоне остальных. Добавим белую рамку вокруг перетаскиваемой картинки.
Вставим вот такой код в наш itemImage.
Rectangle {
id: imageBorder
anchors.fill: parent
radius: 5
color: "transparent"
border.color: "#ffffff"
border.width: 6
opacity: 0
}
Плюс к рамочке неплохо бы как-то помечать место, откуда был взят перетаскиваемый элемент. Добавим белый кружок в центре этого места. Этот код помещается рядом с нашим itemImage
Rectangle {
id: circlePlaceholder
width: 30; height: 30; radius: 30
smooth: true
anchors.centerIn: parent
color: "#cecece"
opacity: 0
}
Ну и добавим их отображение при начале перетаскивания в состояние inDrag делегата.
State {
name: "inDrag"
when: index == dndGrid.draggedItemIndex
PropertyChanges { target: circlePlaceholder; opacity: 1 }
PropertyChanges { target: imageBorder; opacity: 1 }
PropertyChanges { target: itemImage; parent: dndContainer }
PropertyChanges { target: itemImage; anchors.centerIn: undefined }
PropertyChanges { target: itemImage; x: coords.mouseX - itemImage.width / 2 }
PropertyChanges { target: itemImage; y: coords.mouseY - itemImage.height / 2 }
}
Теперь наш пример выглядит уже вот так.

Шаг 4. Добавляем анимацию
Ну, основа нашего грида с Drag'n'Drop-ом готова. Добавим свистелок. А если более конкретно, то добавим анимацию.
Для начала в наш многострадальный itemImage добавим два состояния:
- greyedOut — затеняет картинку. Используется когда drag'n'drop активен, но не для данного элемента
- inactive — используется либо когда drag'n'drop неактивен, либо для перетаскиваемого элемента
Таким образом, мы слегка затеняем все элементы, кроме перетаскиваемого, что дает нам большую видимость последнего на фоне остальных.
Также добавим анимированные изменения для прозрачности, ширины и высоты картинки.
state: "inactive"
states: [
State {
name: "greyedOut"
when: (dndGrid.draggedItemIndex != -1) && (dndGrid.draggedItemIndex != index)
PropertyChanges { target: itemImage; opacity: 0.8}
},
State {
name: "inactive"
when: (dndGrid.draggedItemIndex == -1) || (dndGrid.draggedItemIndex == index)
PropertyChanges { target: itemImage; opacity: 1.0}
}
]
Behavior on width { NumberAnimation { duration: 300; easing.type: Easing.InOutQuad } }
Behavior on height { NumberAnimation { duration: 300; easing.type: Easing.InOutQuad } }
Behavior on opacity {NumberAnimation { duration: 300; easing.type: Easing.InOutQuad } }
В состояние inDrag добавим еще изменение высоты и ширины картинки и переход из этого состояния в любое другое (то есть переход из активного drag'n'drop в обычный режим). В этом переходе сделаем анимацию масштаба.
states: [
State {
name: "inDrag"
when: index == dndGrid.draggedItemIndex
PropertyChanges { target: circlePlaceholder; opacity: 1 }
PropertyChanges { target: imageBorder; opacity: 1 }
PropertyChanges { target: itemImage; parent: dndContainer }
PropertyChanges { target: itemImage; width: 80 }
PropertyChanges { target: itemImage; height: 80 }
PropertyChanges { target: itemImage; anchors.centerIn: undefined }
PropertyChanges { target: itemImage; x: coords.mouseX - itemImage.width / 2 }
PropertyChanges { target: itemImage; y: coords.mouseY - itemImage.height / 2 }
}
]
transitions: [
Transition {
from: "inDrag"
to: "*"
PropertyAnimation {
target: itemImage
properties: "scale, opacity"
easing.overshoot: 1.5
easing.type: "OutBack"
from: 0.0
to: 1.0
duration: 750
}
}
]
Также добавим анимации изменения прозрачности к рамке вокруг перетаскиваемого элемента и к кружку на пустом месте.
Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutQuad } }
В итоге получилась уже вот такая картинка.

А это заснято в промежуточный момент, когда отрабатывает анимация.

Шаг 5. Последний штрих
И, в качестве завершения, добавим индикатор позиции, куда будет перемещен элемент. Отобразим его в виде вертикальной полоски слева от этого элемента.
Родителем этого элемента будет наш GridView, все действия с ним будут происходить там же.
Для начала добавим в GridView три новых свойства: индекс целевого элемента (possibleDropIndex) и текущие координаты мыши (xCoordinateInPossibleDrop и yCoordinateInPossibleDrop).
Плюс добавим сам элемент индикатора. Это обычная картинка 6x1 пикселей с градиентом, растиражированная по вертикали. У индикатора есть два состояния: невидим (по умолчанию) и shown. Во втором состоянии элемент индикатора помещается в промежуток между двумя картинками, слева от цели. Положение элемента рассчитывается на основе двух последних свойств, а не по индексу в модели, тем самым мы не зависим от текущего количества столбцов в таблице.
property int possibleDropIndex: -1
property int xCoordinateInPossibleDrop: -1
property int yCoordinateInPossibleDrop: -1
Item {
id: dropPosIndicator
visible: false
height: dndGrid.cellHeight
width: 10
Image {
visible: parent.visible
anchors.centerIn: parent
height: parent.height-10
source: "drop-indicator.png"
}
states: [
State {
name: "shown"
when: dndGrid.possibleDropIndex != -1
PropertyChanges {
target: dropPosIndicator
visible: true
x: Math.floor(dndGrid.xCoordinateInPossibleDrop/dndGrid.cellWidth) *
dndGrid.cellWidth - 5
y: Math.floor(dndGrid.yCoordinateInPossibleDrop/dndGrid.cellHeight) *
dndGrid.cellHeight
}
}
]
}
Также добавим еще один обработчик в MouseArea. Тут нам понадобится свойство с индексом места дропа, чтобы не обновлять каждый раз координаты мышки, а менять их только при смене целевого элемента.
onPositionChanged: {
var newPos = dndGrid.indexAt(mouseX, mouseY)
if (newPos != dndGrid.possibleDropIndex) {
dndGrid.possibleDropIndex = newPos
dndGrid.xCoordinateInPossibleDrop = mouseX
dndGrid.yCoordinateInPossibleDrop = mouseY
}
}
В итоге, получим вот такое приложение. Такое же, как на видео в начале поста :)

Ну и да, скачать полные исходники (с отдельными .qml файлами для каждого шага) можно здесь