Create a TimePicker similar to the standard in Harmattan

  • Tutorial
This post was in the " Smart phones for smart positions ,"


the left in the picture you can see what it looks like TimePicker (time adjustment component) in standard applications MeeGo Harmattan on the Nokia . And on the right is the TimePicker from MeeGo Qt Components (Extras) , which is offered to developers for use. Differences on the face.

Definitely, it becomes clear that there is a certain injustice, because the component used by Nokia is not available to developers of third-party applications, despite the fact that it is more beautiful and functional. Honestly, in my opinion, this is the best TimePicker option I've seen.

So, below I will show how to implement such a component yourself, and you will see that everything is relatively simple.

QML


What is our TimePicker ? In fact, it is only three pictures, two tags and one active area for management. Let's just draw this in 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
    }
}

It turns out something like this, if you use this component now, passing it as pictures, the same circles as in the original TimePicker , then we will see a component that does not respond to our actions, with the time set at 00:00.

Now we need to make the location of the circles with hours and minutes change depending on the value of the variables:
    property int hours: 0
    property int minutes: 0

To do this, add the following lines to the hourDot and minutesDot components, respectively:
            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)))

and
            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)))

Nothing supernatural - just finding the location of a point on a circle.

After we have drawn everything, we will move on to the main part of the component, namely the processing of user actions and reactions to them.

Treatment


Our MouseArea will work alone at once for both circles, and all because there are no round MouseArea , but somehow we need to handle the position of the finger in the circle and in the ring. Thus, we shift this task to ourselves, and write such a simple method for 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
        }

This method returns 0 if we hit the small circle (hours), one if the ring (minutes), and -1 if we didn’t hit anywhere so as not to process the corners of the square.

In order to process user gestures, three MouseArea events are required :
        onPressed: {
            currentHandler = chooseHandler(mouseX, mouseY)
            previousAlpha = findAlpha(mouseX, mouseY)
        }

We call the method described above and determine what we are working with and get the angle of the pressure point relative to "12 hours".

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

Just reset everything.

        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)
        }

Occurs when the user's finger changes its position.
We get a new angle, everything is also relatively “12 hours”, and we call the getNewTime method (we will consider it below) with certain parameters - depending on whether we are working with hours or minutes now.

Now let's look at the findAlpha method , it is simple, and also does not go beyond school geometry:
        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
        }

We calculate the angle in radians and translate it into degrees and add an additional check in order to work with all 360 degrees (and not just the first 180).

getNewTime


This method is the core of the calculation and has the following parameters:
  • source - the initial value of the counter;
  • alpha is the current angle;
  • resolution - the size of one sector in degrees (6 - for minutes, 30 - for seconds);
  • boundFactor - how many times you need to spin around the circle to reset the counter - 1 for minutes, 2 - for hours, because we have 24 hours but all 12 divisions in a circle.

Actually the method:
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
        }

First, we calculate the difference between the current and previous saved position of the finger, if the difference is less than the size of one sector, then we simply exit without changing anything.
The next check handles the case when a finger’s passage through 0 degrees (12 hours) returns a delta that is too large, so it corrects it.
The result variable is written into the initial position of the point in degrees, after which the delta is added to it.
But not everything is so simple, a group of conditions is to adjust borderline cases, if these checks were not, we could see on TimePicker'e incorrect values such as 25:68.
After that, we remember the new position of the finger and return the result in the correct units.

Conclusion


Actually, that’s all. On the left is the final screenshot from the device. The given algorithms are not the top of perfection, it is just a working solution. If you can do better - please, let me know.
The component code is available on Gitorius

P. S. The first ( wrong ) solution to this problem was even simpler - I just looked at which quarter of the circle the finger was, and, depending on one of the four directions of its movement, added or subtracted one. Perhaps for some purpose this approach could also make sense, but I set the task to match the existing Nokia solution as closely as possible.

Also popular now: