Why does my rotatable wheel control sometimes spin in the wrong direction? - ios

I am making a rotatable wheel control. You can drag anywhere on it to spin the control by it's central point.
It's not behaving correctly. It usually works well, but for some portion of a rotation, it starts rotating in the opposite direction for a brief period, and then it starts rotating with your finger again. EDIT: Upon further testing, I realized that you don't need to rotate it 360 degrees for it to rotate in the wrong direction.
A possibly unrelated problem is that the control seems to move slower then your finger. If a solution to the original problem doesn't solve this problem, I'll ask a separate question.
I posted an example project. If you would prefer to reproduce the project yourself, here are the steps with code:
Create a new single view application.
Create a new UIView subclass (in Objective-C) called NNReadyIndicator
Copy and paste this code in `NNReadyIndicator.m
#import "NNReadyIndicator.h"
#interface NNReadyIndicator ()
#property CGAffineTransform startTransform;
#property double originalAngle;
#end
#implementation NNReadyIndicator
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
#pragma mark tracking
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint touchPoint = [touch locationInView:self];
[self setOriginalAngle:[NNReadyIndicator calculateAngleWithX1:touchPoint.x x2:self.center.x y1:touchPoint.y y2:self.center.y]];
[self setStartTransform:self.transform];
NSLog(#"BEGIN TRACKING | originalAngle: %f", self.originalAngle);
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint touchPoint = [touch locationInView:self];
double angle = [NNReadyIndicator calculateAngleWithX1:touchPoint.x x2:self.center.x y1:touchPoint.y y2:self.center.y];
CGFloat angleDifference = (CGFloat)(self.originalAngle - angle);
[self setTransform: CGAffineTransformRotate(self.startTransform, -angleDifference)];
NSLog(#"CONTINUE TRACKING | angle: %f angleDifference: %f", angle, angleDifference);
return YES;
}
#pragma mark -- Calculate Angle
+ (double)calculateAngleWithX1:(double)x1 x2:(double)x2 y1:(double)y1 y2:(double)y2 {
double distanceX = [NNReadyIndicator calculateDistanceWithPoint:x1 point:x2];
double distanceY = [NNReadyIndicator calculateDistanceWithPoint:y1 point:y2];
return [self calculateAngleWithDistanceX:distanceX distanceY:distanceY];
}
+ (double)calculateDistanceWithPoint:(double)pointA point:(double)pointB {
double distance = pointA - pointB;
if (distance < 0) {
distance = -distance;
}
return distance;
}
+ (double)calculateAngleWithDistanceX:(double)distanceX distanceY:(double)distanceY {
return atan2(distanceY, distanceX);
}
#end
EDIT: Added logging
Why is the control not consistently rotating in the same direction (and at the same speed?) as a finger that is dragging it?

You need to flip the angle if it is above or below 360 degrees. Also, to make it easier to understand, you will have to convert it into degrees and then back. Based off of this answer: https://stackoverflow.com/a/19617316/2564682
#define DEGREES_TO_RADIANS(angle) ((angle) / 180.0 * M_PI)
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint touchPoint = [touch locationInView:self];
double angle = [NNReadyIndicator calculateAngleWithX1:touchPoint.x x2:self.center.x y1:touchPoint.y y2:self.center.y];
double angleInDegrees = angle * 180 / M_PI;
// Flip angle if above or below 360 degrees
if (angleInDegrees > 180) {
angleInDegrees -= 360;
} else if (angleInDegrees < -180) {
angleInDegrees += 360;
}
CGFloat angleDifference = (CGFloat)(self.originalAngle - angleInDegrees);
[self.layer setTransform: CATransform3DRotate(self.layer.transform, (float)DEGREES_TO_RADIANS(-angleDifference), .0, .0, 1.0)];
NSLog(#"CONTINUE TRACKING | angle: %f angleDifference: %f", angleInRadians, angleDifference);
return YES;
}

Related

How to continue to drawRect: when finger on screen

I have the current code:
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
self.objectPoint = [[touches anyObject] locationInView:self];
float x, y;
if (self.objectPoint.x > self.objectPoint.x) {
x = self.objectPoint.x + 1;
}
else x = self.objectPoint.x - 1;
if (self.fingerPoint.y > self.objectPoint.y) {
y = self.objectPoint.y + 1;
}
else y = self.minionPoint.y - 1;
self.objectPoint = CGPointMake(x, y);
[self setNeedsDisplay];
}
My problem is that I want to keep the object follow your finger until you take your finger off the screen. It will only follow if my finger is moving. touchesEnded only works when I take my finger off the screen, so that's not what I want either. How can I enable something that would solve my problem?
If you want to touch a part of the screen and you want to move the drawn object in that direction as long as you're holding your finger down, there are a couple of approaches.
On approach is the use of some form of timer, something that will repeatedly call a method while the user is holding their finger down on the screen (because, as you noted, you only get updates to touchesMoved when you move). While NSTimer is the most common timer that you'd encounter, in this case you'd want to use a specialized timer called a display link, a CADisplayLink, that fires off when screen updates can be performed. So, you would:
In touchesBegan, capture where the user touched on the screen and start the CADisplayLink;
In touchesMoved, you'd update the user's touch location (but only called if they moved their finger);
In touchesEnded, you'd presumably stop the display link; and
In your CADisplayLink handler, you'd update the location (and you'd need to know the speed with which you want it to move).
So, that would look like:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
self.velocity = 100.0; // 100 points per second
self.touchLocation = [[touches anyObject] locationInView:self];
[self startDisplayLink];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
self.touchLocation = [[touches anyObject] locationInView:self];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self stopDisplayLink];
}
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.lastTimestamp = CACurrentMediaTime(); // initialize the `lastTimestamp`
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
// figure out the time elapsed, and reset the `lastTimestamp`
CFTimeInterval currentTimestamp = CACurrentMediaTime();
CFTimeInterval elapsed = currentTimestamp - self.lastTimestamp;
self.lastTimestamp = currentTimestamp;
// figure out distance to touch and distance we'd move on basis of velocity and elapsed time
CGFloat distanceToTouch = hypotf(self.touchLocation.y - self.objectPoint.y, self.touchLocation.x - self.objectPoint.x);
CGFloat distanceWillMove = self.velocity * elapsed;
// this does the calculation of the angle between the touch location and
// the current `self.objectPoint`, and then updates `self.objectPoint` on
// the basis of (a) the angle; and (b) the desired velocity.
if (distanceToTouch == 0.0) // if we're already at touchLocation, then just quit
return;
if (distanceToTouch < distanceWillMove) { // if the distance to move is less than the target, just move to touchLocation
self.objectPoint = self.touchLocation;
} else { // otherwise, calculate where we're going to move to
CGFloat angle = atan2f(self.touchLocation.y - self.objectPoint.y, self.touchLocation.x - self.objectPoint.x);
self.objectPoint = CGPointMake(self.objectPoint.x + cosf(angle) * distanceWillMove,
self.objectPoint.y + sinf(angle) * distanceWillMove);
}
[self setNeedsDisplay];
}
and to use that, you'd need a few properties defined:
#property (nonatomic) CGFloat velocity;
#property (nonatomic) CGPoint touchLocation;
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFTimeInterval lastTimestamp;
If you want to drag it with your finger, you want to:
In touchesBegan, save the starting locationInView as well as the "original location" of the object being dragged;
In touchesMoved, get the new locationInView, calculate the delta (the "translation") between that and the original locationInView, add that to the saved "original location" of the view, and use that to update the view.
That way, the object will track 100% with your finger as you're dragging it across the screen.
For example, you might:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
self.touchBeganLocation = [[touches anyObject] locationInView:self];
self.originalObjectPoint = self.objectPoint;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint location = [[touches anyObject] locationInView:self];
CGPoint translation = CGPointMake(location.x - self.touchBeganLocation.x, location.y - self.touchBeganLocation.y);
self.objectPoint = CGPointMake(self.originalObjectPoint.x + translation.x, self.originalObjectPoint.y + translation.y);
[self setNeedsDisplay];
}
Probably needless to say, you need properties to keep track of these two new CGPoint values:
#property (nonatomic) CGPoint originalObjectPoint;
#property (nonatomic) CGPoint touchBeganLocation;
Frankly, I might use gesture recognizer, but that's an example of dragging with touchesBegan and touchesMoved.

CGAffineTransformRotate : Detect rotation direction

What I am trying to do :
I have an Image of a wheel and a scroll view. User can drag the wheel to rotate it in either direction.
By detecting the direction of rotation. I have to scroll the images placed over scroll view.
What I am doing is :
Following this tutorial
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint touchPoint = [touch locationInView:self];
startTouch = touchPoint; // StartTouch is static varialbel
float dx = touchPoint.x - container.center.x;
float dy = touchPoint.y - container.center.y;
deltaAngle = atan2(dy,dx);
startTransform = container.transform;
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch*)touch withEvent:(UIEvent*)event
{
CGPoint pt = [touch locationInView:self];
float dx = pt.x - container.center.x;
float dy = pt.y - container.center.y;
float ang = atan2(dy,dx);
float angleDifference = deltaAngle - ang;
container.transform = CGAffineTransformRotate(startTransform, -angleDifference);
return YES;
}
Main Problem
I am not able to detect the Rotation Direction correctly. I am using the angleDifference varibale from method continueTrackingWithTouch to detect it.
if(angleDifference > 0){
// Positive value move in right direction.
}
else{
// Negative value move in left direction.
}
This is working ok for small drags of less than 360 dgrees, but after that it's not working properly.
Can anyone suggest me correct approach of Detecting the rotation Direction.
Thanks

CGAffineTransformMakeRotation goes the other way after 180 degrees (-3.14)

So,
i am trying to do a very simple disc rotation (2d), according to the user touch on it, just like a DJ or something.
It is working, but there is a problem, after certain amount of rotation, it starts going backwards, this amount is after 180 degrees or as i saw in while logging the angle, -3.14 (pi).
I was wondering, how can i achieve a infinite loop, i mean, the user can keep rotating and rotating to any side, just sliding his finger?
Also a second question is, is there any way to speed up the rotation?
Here is my code right now:
#import <UIKit/UIKit.h>
#interface Draggable : UIImageView {
CGPoint firstLoc;
UILabel * fred;
double angle;
}
#property (assign) CGPoint firstLoc;
#property (retain) UILabel * fred;
#end
#implementation Draggable
#synthesize fred, firstLoc;
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
angle = 0;
if (self) {
// Initialization code
}
return self;
}
-(void)handleObject:(NSSet *)touches
withEvent:(UIEvent *)event
isLast:(BOOL)lst
{
UITouch *touch =[[[event allTouches] allObjects] lastObject];
CGPoint curLoc = [touch locationInView:self];
float fromAngle = atan2( firstLoc.y-self.center.y,
firstLoc.x-self.center.x );
float toAngle = atan2( curLoc.y-(self.center.y+10),
curLoc.x-(self.center.x+10));
float newAngle = angle + (toAngle - fromAngle);
NSLog(#"%f",newAngle);
CGAffineTransform cgaRotate = CGAffineTransformMakeRotation(newAngle);
self.transform = cgaRotate;
if (lst)
angle = newAngle;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch =[[[event allTouches] allObjects] lastObject];
firstLoc = [touch locationInView:self];
};
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self handleObject:touches withEvent:event isLast:NO];
};
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self handleObject:touches withEvent:event isLast:YES];
}
#end
And in the ViewController:
UIImage *tmpImage = [UIImage imageNamed:#"theDisc.png"];
CGRect cellRectangle;
cellRectangle = CGRectMake(-1,self.view.frame.size.height,tmpImage.size.width ,tmpImage.size.height );
dragger = [[Draggable alloc] initWithFrame:cellRectangle];
[dragger setImage:tmpImage];
[dragger setUserInteractionEnabled:YES];
dragger.layer.anchorPoint = CGPointMake(.5,.5);
[self.view addSubview:dragger];
I am open to new/cleaner/more correct ways of doing this too.
Thanks in advance.
Flip the angle if it's below -180 or above 180 degrees. Consider the following touchesMoved implementation:
#implementation RotateView
#define DEGREES_TO_RADIANS(angle) ((angle) / 180.0 * M_PI)
CGFloat angleBetweenLinesInDegrees(CGPoint beginLineA, CGPoint endLineA, CGPoint beginLineB, CGPoint endLineB)
{
CGFloat a = endLineA.x - beginLineA.x;
CGFloat b = endLineA.y - beginLineA.y;
CGFloat c = endLineB.x - beginLineB.x;
CGFloat d = endLineB.y - beginLineB.y;
CGFloat atanA = atan2(a, b);
CGFloat atanB = atan2(c, d);
// convert radians to degrees
return (atanA - atanB) * 180 / M_PI;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint curPoint = [[touches anyObject] locationInView:self];
CGPoint prevPoint = [[touches anyObject] previousLocationInView:self];
// calculate rotation angle between two points
CGFloat angle = angleBetweenLinesInDegrees(self.center, prevPoint, self.center, curPoint);
// Flip
if (angle > 180) {
angle -= 360;
} else if (angle < -180) {
angle += 360;
}
self.layer.transform = CATransform3DRotate(self.layer.transform, DEGREES_TO_RADIANS(angle), .0, .0, 1.0);
}
#end
When dragging around the outer bounds of the view, it will rotate it continuously like a spinning wheel. Hope it helps.
You have some problems here:
1-)
CGPoint curLoc = [touch locationInView:self];
and
firstLoc = [touch locationInView:self];
You are transforming your view, and then asking for the location of a touch in it. You cannot get the correct location of a touch in a rotated view.
Make them something not transformed. (for example self.superview after putting it in a container)
2-)
cellRectangle = CGRectMake(-1,self.view.frame.size.height,tmpImage.size.width ,tmpImage.size.height );
You are placing your Draggable instance out of the screen by passing self.view.frame.size.height as the CGRect's y parameter.

iOS Setting Movement Tolerance for Tap Gesture Recognizer

Is there a way to ensure that any tap that includes more than some amount of movement is discarded? As it is, what counts as a tap can involve a lot of sliding of the finger. I would like to process a "tap and move" differently by using touchesBegan:, touchesMoved:, etc..
Probably not the answer you are looking for. But I've worked around this by instead doing it myself in the regular touches sequence. For this to work, you would also want to have self.multipleTouchEnabled = NO
#interface myView(){
CGPoint _touchStartPoint;
}
#end
#implementation myView
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
_touchStartPoint = [[touches anyObject] locationInView:self];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
[self checkDistance: [[touches anyObject] locationInView:self]];
}
-(void)checkDistance:(CGPoint)p{
static CGFloat dX;
dX = p.x - _touchStartPoint.x;
static CGFloat dY;
dY = p.y - _touchStartPoint.y;
static CGFloat dist;
dist = sqrt(dX*dX + dY*dY);
/* movement of less than 10 pixels */
if(dist < 10){
[self tap];
}
}
-(void)tap{
/* do something with your tap*/
}
#end

UIImageView rotation animation in the touched direction

I have one UIImageView having an image of an arrow. When user taps on the UIView this arrow should point to the direction of the tap maintaing its position it should just change the transform. I have implemented following code. But it not working as expected. I have added a screenshot. In this screenshot when i touch the point upper left the arrow direction should be as shown.But it is not happening so.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch=[[event allTouches]anyObject];
touchedPoint= [touch locationInView:touch.view];
imageViews.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(rangle11));
previousTouchedPoint = touchedPoint ;
}
- (CGFloat) pointPairToBearingDegrees:(CGPoint)startingPoint secondPoint:(CGPoint) endingPoint
{
CGPoint originPoint = CGPointMake(endingPoint.x - startingPoint.x, endingPoint.y - startingPoint.y); // get origin point to origin by subtracting end from start
float bearingRadians = atan2f(originPoint.y, originPoint.x); // get bearing in radians
float bearingDegrees = bearingRadians * (180.0 / M_PI); // convert to degrees
bearingDegrees = (bearingDegrees > 0.0 ? bearingDegrees : (360.0 + bearingDegrees)); // correct discontinuity
return bearingDegrees;
}
I assume you wanted an arrow image to point to where ever you touch, I tried and this is what i could come up with. I put an image view with an arrow pointing upwards (haven't tried starting from any other position, log gives correct angles) and on touching on different locations it rotates and points to touched location. Hope it helps ( tried some old math :-) )
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch=[[event allTouches]anyObject];
touchedPoint= [touch locationInView:touch.view];
CGFloat angle = [self getAngle:touchedPoint];
imageView.transform = CGAffineTransformMakeRotation(angle);
}
-(CGFloat) getAngle: (CGPoint) touchedPoints
{
CGFloat x1 = imageView.center.x;
CGFloat y1 = imageView.center.y;
CGFloat x2 = touchedPoints.x;
CGFloat y2 = touchedPoints.y;
CGFloat x3 = x1;
CGFloat y3 = y2;
CGFloat oppSide = sqrtf(((x2-x3)*(x2-x3)) + ((y2-y3)*(y2-y3)));
CGFloat adjSide = sqrtf(((x1-x3)*(x1-x3)) + ((y1-y3)*(y1-y3)));
CGFloat angle = atanf(oppSide/adjSide);
// Quadrant Identifiaction
if(x2 < imageView.center.x)
{
angle = 0-angle;
}
if(y2 > imageView.center.y)
{
angle = M_PI/2 + (M_PI/2 -angle);
}
NSLog(#"Angle is %2f",angle*180/M_PI);
return angle;
}
-anoop4real
Given what you told me, I think the problem is that you are not resetting your transform in touchesBegan. Try changing it to something like this and see if it works better:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch=[[event allTouches]anyObject];
touchedPoint= [touch locationInView:touch.view];
imageViews.transform = CGAffineTransformIdentity;
imageViews.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(rangle11));
previousTouchedPoint = touchedPoint ;
}
Do you need the line to "remove the discontinuity"? Seems atan2f() returns values between +π to -π. Won't those work directly with CATransform3DMakeRotation()?
What you need is that the arrow points to the last tapped point. To simplify and test, I have used a tap gesture (but it's similar to a touchBegan:withEvent:).
In the viewDidLoad method, I register the gesture :
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped:)];
[self.view addGestureRecognizer:tapGesture];
[tapGesture release];
The method called on each tap :
- (void)tapped:(UITapGestureRecognizer *)gesture
{
CGPoint imageCenter = mFlecheImageView.center;
CGPoint tapPoint = [gesture locationInView:self.view];
double deltaY = tapPoint.y - imageCenter.y;
double deltaX = tapPoint.x - imageCenter.x;
double angleInRadians = atan2(deltaY, deltaX) + M_PI_2;
mFlecheImageView.transform = CGAffineTransformMakeRotation(angleInRadians);
}
One key is the + M_PI_2 because UIKit coordinates have the origin at the top left corner (while in trigonometric, we use a bottom left corner).

Resources