This post participates in the competition "
Smart phones for smart posts "


On the left in the picture you can see how
TimePicker looks
like (time setting component) in
Nokia’s standard
MeeGo Harmattan applications. And on the right is the
TimePicker from
MeeGo Qt Components (Extras) , which is offered to developers for use. The differences on the face.
Certainly, it becomes clear that there is some 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 version of
TimePicker'a , which I saw.
')
So, below I will show how to implement such a component myself, and you will see that everything is relatively simple.
QML
What is our
TimePicker ? In fact, these are only three pictures, two labels and one active area for management. Let's just draw it 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 now use this component, 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 the 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 components
hourDot and
minutesDot , 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 extraordinary - just finding the location of a point on a circle.
After we have drawn everything, let's move on to the main part of the component, namely the processing of user actions and reactions to them.
Treatment
Our
MouseArea will work one and at once for both circles, and all because the round
MouseArea does not exist, and we somehow 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 a small circle (hours), a unit — if the ring (minutes) and –1 if it didn’t get anywhere — so as not to process the corners of the square.
In order to handle user gestures, three
MouseArea events will be
required :
onPressed: { currentHandler = chooseHandler(mouseX, mouseY) previousAlpha = findAlpha(mouseX, mouseY) }
Call the method described above and determine what we are working with and we 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 (consider it below) with certain parameters - depending on whether we are currently working with hours or minutes.
Now let's look at the
findAlpha method, it is simple, and also does not go beyond the scope of 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 with the first 180).
getNewTime
This method is the core of calculations and has the following parameters:
- source - the initial value of the counter;
- alpha is the current angle;
- resolution is 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 in circumference all 12 divisions.
Actually 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 the previous saved finger position, if the difference is smaller than the size of one sector, then we simply exit without changing anything.
The following check handles the case when the passage of a finger through 0 degrees (12 hours) returns too large a delta, so it corrects it.
The
result variable is recorded in the initial position of the point in degrees, then the delta is added to it.
But not everything is so simple, a group of conditions serves to correct borderline cases, if these checks were not available, we could see incorrect values ​​like 25:68 on the TimePicker.
After that, we memorize the new position of the finger and return the result in the correct units.
Conclusion

Actually, that's all. On the left, the final screenshot from the device. The algorithms are not the height of perfection, it is just a working solution. If you can do better - welcome, tell me about it.
Component code
is available on GitoriusPS The first (
wrong ) solution of this problem was even simpler - I just looked at which quarter of the circle the finger was in, and, depending on one of the four directions of its movement, I added or subtracted a unit. Perhaps for some purposes this approach could also make sense, but I set the task to correspond to the existing solution from Nokia as much as possible.