Можно ли написать обыкновенное мобильное приложение на Qt Quick? Не игру, а именно традиционное приложение? Если полгода назад у меня были серьезные сомнения в осуществимости этого предприятия, то теперь сомнений не осталось — можно! Конечно, на этом пути поджидало (и поджидает) множество проблем, большинство из которых описано тут. На данный момент накопилось уже приличное количество наработок, надеюсь эта статья положит начало циклу по систематизации и документированию опыта. Начнем с чего-нибудь простого и нужного, а именно с виджета выбора цифрового значения, по английски именуемого Picker. Такой используется в Android, когда нужно ввести дату, время, или какое-нибудь специфическое значение.
Под капотом
Логично, что для того, чтобы повторить, нужно сначала препарировать оригинальный виджет и понять из каких частей состоит. Итак, что мы имеем?
1) В основе лежит прокручиваемый список (отмечен синим), выбранный элемент которого находится в центре видимой части. А значит в качестве основы будем использовать стандартный ListView. Для того, чтобы реализовать выбор центрального элемента, нам необходимо:
- Отслеживать окончание движения;
- Находить элемент попадающий в геометрический центр по вертикали;
- При необходимости анимировано докручивать его из половинчатой позиции;
- Делать центральный индекс текущим;
- Генерировать сигнал об изменении элемента;
Получившийся код
Следует отметить, что в оригинале списки часто циклические, однако получившийся qml-клон пока позволяет использовать только обычные.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) } }

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 } } }
Выбор времени
Итак, сам виджет у нас теперь есть, осталось привести пример использования. Полноценный диалог выбора даты — тема для отдельной статьи (но желающие вполне могут посмотреть его уже сегодня вот тут). Потому потренируемся на

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 }
Исходники проекта целиком.
