Pull to refresh

Создаем TimePicker аналогичный стандартному в Harmattan

Qt *
Tutorial
Этот пост участвует в конкурсе „Умные телефоны за умные посты


Слева на картинке вы можете увидеть, как выглядит TimePicker (компонент настройки времени) в стандартных приложениях MeeGo Harmattan от Nokia. А справа TimePicker из MeeGo Qt Components (Extras), который предлагается разработчикам для использования. Различия на лицо.

Определенно, становится ясно, что существует некая несправедливость, потому что используемый Nokia компонент недоступен разработчикам сторонних приложений, несмотря на то, что он более красив и функционален. Честно говоря, на мой взгляд, это лучший вариант TimePicker'а, который я видел.

Итак, ниже я покажу, как реализовать такой компонент самому, и вы убедитесь, что все относительно просто.

QML


Что из себя представляет наш TimePicker? По сути это лишь, три картинки, две метки и одна активная область, для управления. Давайте просто нарисуем это в QML.
Item {
    id: timePicker

    width: 400
    height: 400

    property int hours: 0
    property int minutes: 0

    property alias backgroundImage: bg.source
    property alias hourDotImage: hourDot.source
    property alias minutesDotImage: minuteDot.source

    Image {
        id: bg
        anchors.fill: parent


        property int centerX: 200
        property int centerY: 200

        property int minuteRadius: 152
        property int hourRadius: 65

        property int minuteGradDelta: 6
        property int hourGradDelta: 30

        property int diameter: 73

        Image {
            id: hourDot

            x: centerX
            y: centerY - bg.hourRadius

            width: bg.diameter
            height: bg.diameter

            Text {
                font.pixelSize: 40
                anchors.centerIn: parent

                text: (timePicker.hours < 10 ? "0" : "") + timePicker.hours
            }
        }

        Image {
            id: minuteDot

            x: centerX
            y: centerY - bg.minuteRadius

            width: bg.diameter
            height: bg.diameter

            Text {
                font.pixelSize: 40
                anchors.centerIn: parent
                color: "#CCCCCC"
                text: (timePicker.minutes < 10 ? "0" : "") + timePicker.minutes
            }
        }
    }

    MouseArea {
        id: mouseArea
        anchors.fill: parent
    }
}

Получается примерно так, если сейчас использовать этот компонент, передав ему в качестве картинок, такие же кружочки как в оригинальном TimePicker, то мы увидим компонент, не реагирующий на наши действия, с временем установленным в 00:00.

Теперь надо сделать так, чтобы местоположение кружков с часами и минутами менялось в зависимости от значения переменных:
    property int hours: 0
    property int minutes: 0

Для этого добавляем компонентам hourDot и minutesDot следующие строки соответственно:
            x: (bg.centerX - bg.diameter / 2) + bg.hourRadius * Math.cos(timePicker.hours * bg.hourGradDelta * (3.14 / 180) - (90 * (3.14 / 180)))
            y: (bg.centerY - bg.diameter / 2) + bg.hourRadius * Math.sin(timePicker.hours * bg.hourGradDelta * (3.14 / 180) - (90 * (3.14 / 180)))

и
            x: (bg.centerX - bg.diameter / 2) + bg.minuteRadius * Math.cos(timePicker.minutes * bg.minuteGradDelta * (3.14 / 180) - (90 * (3.14 / 180)))
            y: (bg.centerY - bg.diameter / 2) + bg.minuteRadius * Math.sin(timePicker.minutes * bg.minuteGradDelta * (3.14 / 180) - (90 * (3.14 / 180)))

Ничего сверхъестественного — просто нахождение местоположения точки на окружности.

После того как мы все нарисовали, перейдем к главной части компонента а именно обработке действий пользователя и реакции на них.

Обработка


Наша MouseArea будет работать одна и сразу для обоих окружностей, а все потому, что круглых MouseArea не существует, а нам как то надо обрабатывать положение пальца именно в круге и в кольце. Таким образом мы перекладываем эту задачу на себя, и пишем такой простой метод для MouseArea:
function chooseHandler(mouseX, mouseY) {
            if (bg.hourRadius + bg.diameter / 2 > Math.sqrt(Math.pow(bg.centerX - mouseX, 2) + Math.pow(bg.centerY - mouseY, 2)))
                return 0
            else if (bg.minuteRadius + bg.diameter / 2 > Math.sqrt(Math.pow(bg.centerX - mouseX, 2) + Math.pow(bg.centerY - mouseY, 2)))
                return 1
            return -1
        }

Этот метод возвращает 0 если мы попали в малый круг (часы), единицу — если в кольцо (минуты) и -1 если никуда не попали — чтобы не обрабатывать углы квадрата.

Для того чтобы обработать жесты пользователя на потребуются три события MouseArea:
        onPressed: {
            currentHandler = chooseHandler(mouseX, mouseY)
            previousAlpha = findAlpha(mouseX, mouseY)
        }

Вызываем метод описанный выше и определяем с чем работаем и получаем угол точки нажатия относительно «12 часов».

        onReleased: {
            currentHandler = -1
            previousAlpha = -1
        }

Просто сбрасываем все.

        onPositionChanged: {
            var newAlpha = 0;
            if (currentHandler < 0)
                return

            newAlpha = findAlpha(mouseX, mouseY)

            if (currentHandler > 0) {
                timePicker.minutes = getNewTime(timePicker.minutes, newAlpha, bg.minuteGradDelta, 1)
            }
            else
                timePicker.hours = getNewTime(timePicker.hours, newAlpha, bg.hourGradDelta, 2)
        }

Происходит, когда палец пользователя меняет свою позицию.
Получаем новый угол все также относительно «12 часов», и вызываем метод getNewTime (рассмотрим его ниже) с определенными параметрами — в зависимости от того с часами или минутами мы сейчас работаем.

Теперь посмотрим на метод findAlpha, он прост, и так же не выходит за рамки школьной геометрии:
        function findAlpha(x, y) {
            var alpha = (Math.atan((y - bg.centerY)/(x - bg.centerX)) * 180) / 3.14 + 90
            if (x < bg.centerX)
                alpha += 180

            return alpha
        }

Вычисляем угол в радианах и переводим его в градусы и добавляем дополнительную проверку, для того чтобы работать со всеми 360 градусами (а не только с первыми 180).

getNewTime


Этот метод является ядром вычислений и имеет следующие параметры:
  • source — исходное значение счетчика;
  • alpha — текущий угол;
  • resolution — размер одного сектора в градусах (6 — для минут, 30 — для секунд) ;
  • boundFactor — сколько раз нужно крутануться по окружности для сброса счетчика — 1 для минут, 2 — для часов, т. к. у нас 24 часа но в окружности все 12 делений.

Собственно метод:
function getNewTime(source, alpha, resolution, boundFactor) {
            var delta = alpha - previousAlpha

            if (Math.abs(delta) < resolution)
                return source

            if (Math.abs(delta) > 180) {
                delta = delta - sign(delta) * 360
            }

            var result = source * resolution

            var resdel = Math.round(result + delta)
            if (Math.round(result + delta) > 359 * boundFactor)
                result += delta - 360 * (source * resolution > 359 ? boundFactor : 1)
            else if (Math.round(result + delta) < 0 * boundFactor)
                result += delta + 360 * (source * resolution > 359 ? boundFactor : boundFactor)
            else
                result += delta

            previousAlpha = alpha
            return result / resolution
        }

Сначала мы вычисляем разницу между текущим и предыдущим сохраненным положением пальца, если разница меньше размера одного сектора, то просто выходим, ничего не меняя.
Следующая проверка обрабатывает случай, когда проход пальца через 0 градусов (12 часов) возвращает слишком большую дельту, таким образом она корректирует ее.
В переменную result записывается исходное положение точки в градусах, после этого к нему прибавляется дельта.
Но не все так просто, группа условий служит для корректировки пограничных случаев, если бы этих проверок не было, мы бы могли увидеть на TimePicker'е некорректные значения типа 25:68.
После этого мы запоминаем новое положение пальца и возвращаем результат в правильных единицах.

Заключение


Собственно, на этом все. Слева итоговый screenshot с девайса. Приводимые алгоритмы не являются верхом совершенства, это просто рабочее решение. Если вы сможете сделать лучше — милости прошу, сообщите мне об этом.
Код компонента доступен на Gitorius

P. S. Первое (неправильное) решение этой задачи было еще проще — я просто смотрел в какой четверти окружности находится палец, и, в зависимости от одного из четырех направлений его движения, прибавлял или вычитал единицу. Возможно, для каких-то целей такой подход тоже мог бы иметь смысл, но я ставил задачу максимально соответствовать существующему решению от Nokia.
Tags: n9_contestQtQMLQt QuickMeeGoHarmattanN9
Hubs: Qt
Total votes 54: ↑42 and ↓12 +30
Comments 4
Comments Comments 4

Popular right now