I am wondering what is the best way to implement rotation-based dragging movements in my iPhone application.
I have a UIView that I wish to rotate around its centre, when the users finger is touch the view and they move it. Think of it like a dial that needs to be adjusted with the finger.
The basic question comes down to:
1) Should I remember the initial angle and transform when touchesBegan is called, and then every time touchesMoved is called apply a new transform to the view based on the current position of the finger, e.g., something like:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self]; //current position of touch
if (([touch view] == self)
&& [Utility getDistance:currentPoint toPoint:self.middle] <= ROTATE_RADIUS //middle is centre of view
&& [Utility getDistance:currentPoint toPoint:self.middle] >= MOVE_RADIUS) { //will be rotation gesture
//remember state of view at beginning of touch
CGPoint top = CGPointMake(self.middle.x, 0);
self.initialTouch = currentPoint;
self.initialAngle = angleBetweenLines(self.middle, top, self.middle, currentPoint);
self.initialTransform = self.transform;
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self]; //current position of touch
if (([touch view] == self)
&& [Utility getDistance:currentPoint toPoint:self.middle] <= ROTATE_RADIUS
&& [Utility getDistance:currentPoint toPoint:self.middle] >= MOVE_RADIUS) { //a rotation gesture
//rotate tile
float newAngle = angleBetweenLines(self.middle, CGPointMake(self.middle.x, 0), self.middle, currentPoint); //touch angle
float angleDif = newAngle - self.initialAngle; //work out dif between angle at beginning of touch and now.
CGAffineTransform newTransform = CGAffineTransformRotate(self.initialTransform, angleDif); //create new transform
self.transform = newTransform; //apply transform.
}
OR
2) Should I simply remember the last known position/angle, and rotate the view based on the difference in angle between that and now, e.g.,:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self]; //current position of touch
if (([touch view] == self)
&& [Utility getDistance:currentPoint toPoint:self.middle] <= ROTATE_RADIUS
&& [Utility getDistance:currentPoint toPoint:self.middle] >= MOVE_RADIUS) { //will be rotation gesture
//remember state of view at beginning of touch
CGPoint top = CGPointMake(self.middle.x, 0);
self.lastTouch = currentPoint;
self.lastAngle = angleBetweenLines(self.middle, top, self.middle, currentPoint);
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self]; //current position of touch
if (([touch view] == self)
&& [Utility getDistance:currentPoint toPoint:middle] <= ROTATE_RADIUS
&& [Utility getDistance:currentPoint toPoint:middle] >= MOVE_RADIUS) { //a rotation gesture
//rotate tile
float newAngle = angleBetweenLines(self.middle, CGPointMake(self.middle.x, 0), self.middle, currentPoint); //touch angle
float angleDif = newAngle - self.lastAngle; //work out dif between angle at beginning of touch and now.
CGAffineTransform newTransform = CGAffineTransformRotate(self.transform, angleDif); //create new transform
self.transform = newTransform; //apply transform.
self.lastTouch = currentPoint;
self.lastAngle = newAngle;
}
The second option makes more sense to me, but it is not giving very pleasing results (jaggy updates and non-smooth rotations). Which way is best (if any), in terms of performance?
Cheers!
It is actually much simpler than what you have tried.
You need three data points:
The origin of your view.
The location of the current touch
The location of the previous touch
The Touch object passed to you actually contains the last touch location. So you don't need to keep track of it.
All you have to do is calculate the angle between two lines:
Origin to Current Touch
Origin to Previous Touch
Then convert that to radians and use that in your CGAffineTransformRotate(). Do that all in your touchesMoved handler.
Here is a function to calculate what you need just that:
static inline CGFloat angleBetweenLinesInRadians(CGPoint line1Start, CGPoint line1End, CGPoint line2Start, CGPoint line2End) {
CGFloat a = line1End.x - line1Start.x;
CGFloat b = line1End.y - line1Start.y;
CGFloat c = line2End.x - line2Start.x;
CGFloat d = line2End.y - line2Start.y;
CGFloat line1Slope = (line1End.y - line1Start.y) / (line1End.x - line1Start.x);
CGFloat line2Slope = (line2End.y - line2Start.y) / (line2End.x - line2Start.x);
CGFloat degs = acosf(((a*c) + (b*d)) / ((sqrt(a*a + b*b)) * (sqrt(c*c + d*d))));
return (line2Slope > line1Slope) ? degs : -degs;
}
Courtesy of Jeff LaMarche at:
http://iphonedevelopment.blogspot.com/2009/12/better-two-finger-rotate-gesture.html
Example:
UITouch *touch = [touches anyObject];
CGPoint origin = [view center];
CGFloat angle = angleBetweenLinesInRadians(origin, [touch previousLocationInView:self.superview.superview], origin, [touch locationInView:self.superview.superview]);
Have you considered using UIRotationGestureRecognizer? Seems like that has the logic already baked in, and might make things simpler.
Related
I have written a program that will rotate my imageView as i want (CW or ~CW).
And it is working fine.
but it is responding very slowly, I want it to respond quickly...
can you please help me out of it?
Thanks in advance...!
Following is my code------------------------
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch * touch = [[event allTouches]anyObject];
CGPoint prevLoc = [touch previousLocationInView:self];
CGPoint loc = [touch locationInView:self];
if (isRotating) {
float prevAngle = atan2(prevLoc.y-self.center.y,
prevLoc.x-self.center.x);
float newAngle = atan2( loc.y-self.center.y,
loc.x-self.center.x);
float finalAngle =angle+(newAngle-prevAngle);
//Apply new transform
CGAffineTransform rotateTransform = CGAffineTransformMakeRotation(Angle);
self.transform=rotateTransform;
angle = finalAngle;
}
}
I seem to be having an issue with calculating the angle between my sprite and a touch point. I'm trying to get my sprite to directly face the direction of the touch point whenever the user touches the screen. Here's my code:
-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
CGPoint tapPosition;
for (UITouch *touch in touches){
CGPoint location = [touch locationInView:[touch view]];
tapPosition = [self convertToNodeSpace:[[CCDirector sharedDirector] convertToGL:location]];
}
float angle = CC_RADIANS_TO_DEGREES(ccpAngle(fish.position, tapPosition));
[fish runAction:[CCRotateTo actionWithDuration:0.5 angle:angle]];
}
Any ideas? Thanks
Add this to the end of Nikhil's answer to avoid getting a negative angle when the touch location is to the bottom right of the sprite.
if (calculatedAngle < 0)
{
calculatedAngle+=360;
}
Try this out, first of all you don't need that for loop because you are just going to end up with the last touch location anyway. You may as well use [touches anyObject] as below.
Second, I am not sure what ccpAngle does off the top of my head but when macros like that don't work sometimes its easier to just do the maths yourself.
-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint tapPosition;
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:[touch view]];
tapPosition = [self convertToNodeSpace:[[CCDirector sharedDirector] convertToGL:location]];
float dY = fish.position.y - tapPosition.y;
float dX = fish.position.x - tapPosition.x;
float offset = dX<0 ? 90.0f : -90.0f;
float angle = CC_RADIANS_TO_DEGREES(atan2f(dY, dX)) + offset;
[fish runAction:[CCRotateTo actionWithDuration:0.5 angle:angle]];
}
You probably could replace dX and dY with a point from ccpDiff as well but it doesn't really matter. Also depending on where the head of your fish is you may need to adjust the angle offset but I will leave that up to you.
Let me know if this helps.
CCPoint pos1 = [fish position];
CCPoint pos2 = touchlocation;
float theta = atan((pos1.y-pos2.y)/(pos1.x-pos2.x)) * 180 * 7 /22;
float calculatedAngle;
if(pos1.y - pos2.y > 0)
{
if(pos1.x - pos2.x < 0)
{
calculatedAngle = (-90-theta);
}
else if(pos1.x - pos2.x > 0)
{
calculatedAngle = (90-theta);
}
}
else if(pos1.y - pos2.y < 0)
{
if(pos1.x - pos2.x < 0)
{
calculatedAngle = (270-theta);
}
else if(pos1.x - pos2.x > 0)
{
calculatedAngle = (90-theta);
}
}
Use this calculatedAngle in your Run Action.. hope this helps... :)
I have an object that the user has to drag around the screen. This is a regular UIImageView. Currently, the image can be dragged around but when you release it, it stops where your finger lifted the image. What I want is to create momentum, so I can give an impulse to the image and it scrolls until it bounces the screen edge.
I don't want nothing very complex like adding physics libraries and stuff like that. I want to keep it as minimum as possible.
How do I do that? Can you guys point me to some tutorial or in the right direction?
thanks.
Well one way of doing this without using any physics library is this:
First in your class create 4 instance variables like this:
CFTimeInterval startTime;
CGPoint startPoint;
CGPoint oldPoint;
BOOL imageViewTouched;
Than in your - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event method have this code:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouched] anyObject];
// Test to see if the touched view is your image view. If not just return.
if (touch.view != <#yourImageView#>) {
return;
}
startPoint = [touch locationInView:self.view];
oldPoint = startPoint;
startTime = CACurrentMediaTime();
imageViewTouched = YES;
}
For - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event do this:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// Fingers were moved on the screen but your image view was not touched in the beginning
if (!imageViewTouched) {
return;
}
UITouch *touch = [[event allTouches] anyObject];
CGPoint newPoint = [touch locationInView:self.view];
<#yourImageView#>.frame = CGRectOffset(<#yourImageView#>.frame, newPoint.x - oldPoint.x, newPoint.y - oldPoint.y);
oldPoint = newPoint;
}
Now for - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event try this:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// Fingers were removed from the screen but your image view was not touched in the beginning
if (!imageViewTouched) {
return;
}
imageViewTouched = NO;
UITouch *touch = [[event allTouches] anyObject];
CGPoint endPoint = [touch locationInView:self.view];
CFTimeInterval endTime = CACurrentMediaTime();
CFTimeInterval timeDifference = endTime - startTime;
// You may play with this value until you get your desired effect
CGFloat maxSpeed = 80;
CGFloat deltaX = 0;
CGFloat deltaY = 0;
if (timeDifference < 0.35) {
deltaX = (endPoint.x - startPoint.x) / (timeDifference * 10);
deltaY = (endPoint.y - startPoint.y) / (timeDifference * 10);
}
if (deltaX > maxSpeed) { deltaX = maxSpeed; }
else if (deltaX < -maxSpeed) { deltaX = -maxSpeed; }
else if (deltaX > -5 && deltaX < 5) { deltaX = 0; }
if (deltaY > maxSpeed) { deltaY = maxSpeed; }
else if (deltaY < -maxSpeed) { deltaY = -maxSpeed; }
else if (deltaY > -5 && deltaY < 5) { deltaY = 0; }
[UIView begginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5f];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
<#yourImageView#>.frame = CGRectOffset(<#yourImageView#>.frame, deltaX, deltaY);
[UIView commitAnimations];
}
Please let me know if this makes sense and/or works for you. Cheers!
I have a small image on a view. The view is the object process multi-touch actions. If a finger drag on the view, the image will translate its position. And if user use 2 fingers to make pinch gesture, the image will scale its size. And I do work as section code below:
//touch detect methods
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"Touch began");
//Devide into 2 cases: 1 touch and 2 touches.
if ([touches count] == 1) {
NSLog(#"Touch began cout = 1");
currentImageCenter = focusImage.center;
UITouch *touch = [[touches allObjects] objectAtIndex:0];
previousPoint = [touch locationInView:self];
isTwoFingerTouching = FALSE;
}
else if([touches count] == 2){
NSLog(#"Touch began cout = 2");
UITouch *touch = [[touches allObjects] objectAtIndex:0];
beginFirstPoint = [touch locationInView:self];
touch = [[touches allObjects] objectAtIndex:1];
beginSecondPoint = [touch locationInView:self];
isTwoFingerTouching = TRUE;
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if ([touches count] == 1 && isTwoFingerTouching == FALSE) {
UITouch *touch = [touches anyObject];
CGPoint currentPosition = [touch locationInView:self];
//Calculate distance
double deltaX = currentPosition.x - previousPoint.x;
double deltaY = currentPosition.y - previousPoint.y;
NSLog(#"Touch move detect 1 touch ");
focusImage.center = CGPointMake(currentImageCenter.x+deltaX, currentImageCenter.y+deltaY);
}
else if([touches count] == 2){
NSLog(#"Touch move detect 2 touches");
CGPoint currentFirstPoint;
CGPoint currentSecondPoint;
UITouch *touch = [[touches allObjects] objectAtIndex:0];
currentFirstPoint = [touch locationInView:self];
CGPoint previousFirstPoint = [touch previousLocationInView:self];
touch = [[touches allObjects] objectAtIndex:1];
currentSecondPoint = [touch locationInView:self];
CGPoint previousSecondPoint = [touch previousLocationInView:self];
//Compare previous points with current points.
//Pinch gesture
CGFloat beginDistance = distanceBetweenPoints(previousFirstPoint, previousSecondPoint);
CGFloat currentDistance = distanceBetweenPoints(currentFirstPoint, currentSecondPoint);
if (currentDistance > 0 && beginDistance > 0) {
double scale = currentDistance/beginDistance;
NSLog(#"%f", scale);
//Rotation
CGPoint vector1 = CGPointMake(previousFirstPoint.x - previousSecondPoint.x, previousFirstPoint.y - previousSecondPoint.y);
CGPoint vector2 = CGPointMake(currentFirstPoint.x - currentSecondPoint.x, currentFirstPoint.y - currentSecondPoint.y);
//[vector1, vector2].
double zValue = vector1.x*vector2.y - vector1.y*vector2.x;
CGFloat rotateAngle = angleBetweenLines(previousFirstPoint, previousSecondPoint, currentFirstPoint, currentSecondPoint);
//zValue < 0, vector1 rotate counter-clockwise, so the angle should be negative.
if (zValue < 0) {
rotateAngle = -rotateAngle;
}
//Don't allow to zoom out if the image is too small
if (scale > 1 || focusImage.frame.size.width > 30) {
CGAffineTransform previousTransform = focusImage.transform;
CGAffineTransform mixTransform = CGAffineTransformConcat(CGAffineTransformMakeScale(scale, scale), CGAffineTransformMakeRotation(rotateAngle));
focusImage.transform = CGAffineTransformConcat(previousTransform, mixTransform);
}
}
}
}
But the problem is: I can't identify whether 1 finger or 2 fingers on screen. When I touch 2 fingers, and I move (for rotate and scale) those fingers, [touches count] in touchesMoved: method still equal 1 occasionally. Any one experienced this, please tell me how to solve my problem?
Any reason why you are not using UIGestureRecognizers? It will save you a lot of work if you just use a UIPanGestureRecognizer and a UIPinchGestureRecognizer.
In any case, iOS has always behaved like that. Unless you touch with both fingers at the exact same millisecond, you will detect a one finger touch first then detect two finger touch. You need to employ some sort of a cancelling mechanism in your code if you still want to go the -touchesBegan..., -touchesMoved..., etc., method.
I want to move an image (cBlock) continuously with a gesture. The direction of the swipe gesture determines the direction of the movement. I am able to move, but not continuously. Although the finger is held down, the movement starts/stops. Can anyone see why?
I believe touchesMoved is called continuously, as the log statements appear to be firing off continuously. Also, this logic does not seem to account for the finger being held down while the direction becomes positive, then negative (ex. users pans right, then pans left without lifting finger.) Is there a proven solution for that case?
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
gestureStartPoint = [touch locationInView:gridView];
prevGesturePoint = gestureStartPoint;
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint currentPosition = [touch locationInView:gridView];
CGFloat deltaX = currentPosition.x - prevGesturePoint.x;
CGFloat deltaY = currentPosition.y - prevGesturePoint.y;
if(running == YES){
if(fromTop || fromBottom){
if((currentPosition.x - prevGesturePoint.x) > 0) {
NSLog(#"swipe right.");
[cBlock movXSinglePixel:10]; // move 10px
} else if((currentPosition.x - prevGesturePoint.x) < 0) {
NSLog(#"swipe left.");
[cBlock movXSinglePixel:-10]; //move -10px
}
}
}
[container setNeedsDisplay];
prevGesturePoint = currentPosition;
}
- (void)movXSinglePixel:(int)dir{
[UIView beginAnimations:#"center" context:nil];
[UIView setAnimationDuration:0.01];
[UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
if(tX + dir > 8 || tX + dir < 0){
// Attempt to move out of grid bounds.
return;
}
CGPoint center = [self center];
center.x += (dir * 1.0f);
[self setCenter:center];
tX += dir;
[UIView commitAnimations];
}