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