Можно ли написать обыкновенное мобильное приложение на Qt Quick? Не игру, а именно традиционное приложение? Если полгода назад у меня были серьезные сомнения в осуществимости этого предприятия, то теперь сомнений не осталось — можно!
Конечно, на этом пути поджидало (и поджидает) множество проблем, большинство из которых описано тут. На данный момент накопилось уже приличное количество наработок, надеюсь эта статья положит начало циклу по систематизации и документированию опыта. Начнем с чего-нибудь простого и нужного, а именно с виджета выбора цифрового значения, по английски именуемого Picker. Такой используется в Android, когда нужно ввести дату, время, или какое-нибудь специфическое значение.
Под капотом
Логично, что для того, чтобы повторить, нужно сначала препарировать оригинальный виджет и понять из каких частей состоит. Итак, что мы имеем?
1) В основе лежит прокручиваемый список (отмечен синим), выбранный элемент которого находится в центре видимой части. А значит в качестве основы будем использовать стандартный ListView. Для того, чтобы реализовать выбор центрального элемента, нам необходимо:
- Отслеживать окончание движения;
- Находить элемент попадающий в геометрический центр по вертикали;
- При необходимости анимировано докручивать его из половинчатой позиции;
- Делать центральный индекс текущим;
- Генерировать сигнал об изменении элемента;
import QtQuick 2.0
Rectangle {
id: rootRect
property double itemHeight: 8*mm
property alias model: listView.model
signal indexChanged(int value)
function setValue(value) {
listView.currentIndex = value
listView.positionViewAtIndex(value, ListView.Center);
}
ListView {
id: listView
clip: true
anchors.fill: parent
contentHeight: itemHeight*3
delegate: Item {
property var isCurrent: ListView.isCurrentItem
id: item
height: itemHeight
width: listView.width
Rectangle {
anchors.fill: parent
Text {
text: model.text
font.pixelSize: 3*mm
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
onClicked: {
rootRect.gotoIndex(model.index)
}
}
}
}
onMovementEnded: {
var centralIndex = listView.indexAt(listView.contentX+1,listView.contentY+itemHeight+itemHeight/2)
gotoIndex(centralIndex)
indexChanged(currentIndex)
}
}
function gotoIndex(inIndex) {
var begPos = listView.contentY;
var destPos;
listView.positionViewAtIndex(inIndex, ListView.Center);
destPos = listView.contentY;
anim.from = begPos;
anim.to = destPos;
anim.running = true;
listView.currentIndex = inIndex
}
NumberAnimation {
id: anim;
target: listView;
property: "contentY";
easing {
type: Easing.OutInExpo;
overshoot: 50
}
}
function next() {
gotoIndex(listView.currentIndex+1)
}
function prev() {
gotoIndex(listView.currentIndex-1)
}
}
Следует отметить, что в оригинале списки часто циклические, однако получившийся qml-клон пока позволяет использовать только обычные.
2) Поверх списка лежат разделители (отмечены оранжевым). Необходимы для визуального выделения выбранного элемента. Реализуются тривиальными прямоугольниками нужного цвета, с заданным смещением (соответственно на высоту одного и двух элементов).
3) Для придания эффекта засветки верхнего и нижнего элементов (отмечены зеленым) используется изображение с градиентом из белого в прозрачный. Так же накладывается сверху, с позиционированием проблем тоже никаких.
import QtQuick 2.0
import "../Global"
Rectangle {
property alias model: pickerList.model
signal indexSelected(int value)
function setValue(value) {
pickerList.setValue(value)
}
width: 10*mm
height: 25*mm
ACPickerList {
id: pickerList
width: parent.width
height: parent.height
onIndexChanged: {
indexSelected(value)
}
}
Image {
id: upShadow
sourceSize.height: 10*mm
sourceSize.width: 10*mm
source: "qrc:/img/images/icons/pickerShadowUp.svg"
anchors {
top: parent.top
}
}
Image {
id: downShadow
sourceSize.height: 10*mm
sourceSize.width: 10*mm
source: "qrc:/img/images/icons/pickerShadowDown.svg"
anchors {
bottom: parent.bottom
}
}
Rectangle {
id: topSelector
width: parent.width
height: parseInt(0.3*mm)
color: ACGlobal.style.holoLightBlue
anchors {
top: parent.top
topMargin: pickerList.itemHeight
}
}
Rectangle {
id: bottomSelector
width: parent.width
height: parseInt(0.3*mm)
color: ACGlobal.style.holoLightBlue
anchors {
top: parent.top
topMargin: pickerList.itemHeight*2
}
}
}
Выбор времени
Итак, сам виджет у нас теперь есть, осталось привести пример использования. Полноценный диалог выбора даты — тема для отдельной статьи (но желающие вполне могут посмотреть его уже сегодня вот тут). Потому потренируемся на кошках чем-нибудь более простом, например создадим пикеры как заготовку для диалога выбора времени. Их нам нужно целых два, для часов и минут соответственно. По середине, между ними, должен быть разделитель ":". Для выполнения этой задачи нужно заполнить модель значениями часов и минут, то есть сгенерировать значения от 0 до 23 и от 0 до 59. Если значение меньше 10, нужно дополнять его впереди идущим нулём. Для того, чтобы можно было выбрать крайние элементы списка, необходимо добавить пустые заглушки в начале и конце модели.
Rectangle {
ACPicker {
id: hoursPicker
model:
ListModel {
id: hoursModel
Component.onCompleted: {
append({ value: -1, text: " " })
for(var i = 0; i <= 23; i++){
var norm = i.toString();
if( i < 10 ) norm = "0" + i
append({ value: i, text: norm })
}
append({ value: -1, text: " " })
}
}
anchors {
right: center.left
rightMargin: 1*mm
verticalCenter: parent.verticalCenter
}
}
Text {
id: center
text:":"
font.pixelSize: 3*mm
anchors.centerIn: parent
}
ACPicker {
id: minutesPicker
model:
ListModel {
id: minutesModel
Component.onCompleted: {
append({ value: -1, text: " " })
for(var i = 0; i <= 59; i++){
var norm = i.toString();
if( i < 10 ) norm = "0" + i
append({ value: i, text: norm })
}
append({ value: -1, text: " " })
}
}
anchors {
left: center.right
leftMargin: 1*mm
verticalCenter: parent.verticalCenter
}
}
anchors.fill: parent
}
Исходники проекта целиком.
Автор: Zifix