Suppose you want to create a screen that looks something like this.
How would you do it? You may think that the labels on the right side can be put inside a UITableView. But there is a problem: each label belongs to a certain sector on the wheel, meaning that each label should be aligned vertically to a corresponding number on the wheel. So, as you rotate the wheel, the vertical distances between the labels will not stay the same. I’m not sure if you can dynamically change the row height in UITableView, but even if you could, this approach doesn’t seem to be right.
When I had to implement such a screen, I basically created my own version of a table view, with reusable cells for the labels on the right side as well as for the sectors of the wheel. I’m not going to go into detail about how I did it. The process was pretty straightforward. Actually there was no table view. It was just a bunch of reusable views which I called cells by analogy with UITableView.
What I want to tell you about, though, is how I implemented the scrolling. Because I had some problems with it.
There are two parts to scrolling. First part is when your finger is on the screen. The second part is the inertial movement of views after you lift your finger off the screen.
The first part you implement in the touchesMoved:withEvent: method. It may look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ if (touches.count != 1) return; UITouch *touch = [touches anyObject]; CGPoint touchPosition = [touch locationInView:self.view]; CGPoint previousPosition = [touch previousLocationInView:self.view]; //center is the center of the wheel CGFloat radius = sqrtf(powf((previousTouchPosition.x - self.center.x), 2) + powf((previousTouchPosition.y - self.center.y), 2)); CGFloat angularDelta = (touchPosition.y - previousTouchPosition.y)/radius; // use angularDelta to rotate the circle } |
What happens here is easy to understand.
In the second part, at the moment when you lift your finger off the screen, you need to calculate the initial angular velocity with which the wheel will start its rotation. That velocity will then decrease with time. The problem I had was about calculating that initial velocity.
The solution was easy but I was thinking too hard. I tried to implement strange use cases. I thought, what if you put your finger on the screen, then move, then instead of lifting it you wait for some time, and then with quick movement you swipe and lift your finger? Now, I thought, the velocity in this case should be calculated based only on the last swipe. And the first movement should be ignored.
What I did was I saved the position and time of the last touchesMoved event. Then I used it in touchesEnded:withEvent to calculate velocity. So, to calculate the initial velocity of inertial movement of the wheel I used only the last microscopic movement of the finger. And it sort of worked. But not always. In most cases that last little movement would take about 0.01 seconds. But in some cases it would take much less than that, something like 0.00001 seconds. The movement itself almost always was no less then 2 pixels. That would result in a very high speed. Imagine that you don’t do a swipe, and just gently lift your finger off the screen, expecting the wheel to stay still, but it starts rotating!
I tried to artificially limit the velocity, but it resulted in stalling of the wheel from time to time. Finally, as an experiment I tried to use the whole movement of the finger, not just its tiny last part. And it worked! Then I compared the behaviour of my screen to that of a normal UITableView, and as far as I could see it was exactly the same. So, that lead me to believe that’s exactly how the scrolling is implemented in UITableView. Here is the code that I used (not exactly):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ if (touches.count != 1) return; UITouch *touch = [touches anyObject]; //save position and time of a touch down event in the properties self.touchDownPosition = touchPosition; self.touchDownTime = [NSDate date]; //you need to stop the inertial motion when the screen is touched self.angleVelocity = 0; } -(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ UITouch *touch = [touches anyObject]; //center is the center of the wheel CGFloat radius = sqrtf(powf((self.touchDownPosition.x - self.center.x), 2) + powf((self.touchDownPosition.y - self.center.y), 2)); CGPoint touchPosition = [touch locationInView:self.view]; CGFloat angularDelta = (touchPosition.y - self.touchDownPosition.y)/radius; NSTimeInterval secs = [[NSDate date] timeIntervalSinceDate:self.touchDownTime]; self.angleVelocity = angularDelta/secs; // use self.angleVelocity to rotate the circle } |
Update (September 6, 2016)
OK, it turned out that it wasn’t as simple as I described it in this post. I had to make one little adjustment.
Imagine, that you move your finger on the screen and then stop, wait a second, and then just lift your finger off the screen. In this case the wheel must not move. But in the simplistic model that I have described above you may produce an undesirable movement of the wheel.
How to fix this? You need to analyse the last part of your touch to learn if there was a significant enough movement of the finger in that last part. If there was no movement or it was less than a certain amount of points, you just set the angular velocity of the wheel to zero.