📜 ⬆️ ⬇️

Create a TimePicker similar to the standard in Harmattan

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:

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 Gitorius

PS 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.

Source: https://habr.com/ru/post/134397/


All Articles