Multitouch/Singletouch/Tap handling

August 8, 2009 by Skylar
Filed under: iPhone Development 

In response to a recent iPTF thread in which no one was seemingly able to help the original poster, I made this quick little application. Hopefully you should be able to learn.

  • Single Tap on the beach ball – The beach ball will let you know you tapped on it by pulsing. User feedback is ultra important.
  • Drag your finger around the screen to move the beach ball wherever.
  • Using two fingers, spin the beach ball to rotate it on screen.
  • Double tap anywhere on the screen to reset the beach ball’s rotation and location.

The code is clear, so you should have no problem following it. All my touch events are handled by the view controller. If you still aren’t using view controllers, shame on you. The MVC method makes life quite a bit easier, and is HIGHLY recommended by Apple. Also, without using view controllers, you won’t be able to use neat things such as Navigation and TabBar controllers (among others).

TEST_SPINVIEW_APPViewController.h

#import <UIKit/UIKit.h> #define degreesToRadians(x) (M_PI * x / 180.0) @interface TEST_SPINVIEW_APPViewController : UIViewController {         UIImageView             *beachBall;         CGPoint touch1;         CGPoint touch2; } @end

I’ll be the first to admit, I hate working with radians. To make my life easier, I define a function that will convert radians to degrees. I recommend using this in any instance where you need to feed another function radians. Simply put, it will look something like this: degreesToRadians(45); That will convert a 45 degree angle into a radian. Doesn’t that make life so much simpler.

In the view controller, you’ll see how I convert radians into degrees. Technically, I don’t have to do this. I could leave things as radians only, and feed the radian into the rotation method directly, but degrees are much simpler to debug with. Ultimately, it’s your call which method you decide to choose. In an instance where performance isn’t an issues, i’d say do what you want. If every cycle counts, then do not convert to degrees, you’re just doing needless math.

TEST_SPINVIEW_APPViewController.m

#import "TEST_SPINVIEW_APPViewController.h" @implementation TEST_SPINVIEW_APPViewController - (void)viewDidLoad {     [super viewDidLoad];                 [self.view setMultipleTouchEnabled:YES];         self.view.backgroundColor = [UIColor whiteColor];                 beachBall = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ball.png"]];         beachBall.center = self.view.center;                 [self.view addSubview:beachBall]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {     return NO; } - (void)didReceiveMemoryWarning {     [super didReceiveMemoryWarning]; } #pragma mark Touch Methods - (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {         [super touchesBegan:touches withEvent:event];                 NSArray *allTouches = [touches allObjects];         UITouch *touch = [touches anyObject];                 int count = [allTouches count];                 if (count == 1) {                 if ([touch tapCount] < 2) {                                 if (CGRectContainsPoint([beachBall frame], [[allTouches objectAtIndex:0] locationInView:self.view])) {                                         [UIView beginAnimations:@"TouchDownAnimation" context:NULL];                                         [UIView setAnimationBeginsFromCurrentState:YES];                                         [UIView setAnimationDelegate:self];                                         [UIView setAnimationDidStopSelector:@selector(finishedTouchDownAnimation:finished:context:)];                                         [UIView setAnimationCurve:UIViewAnimationCurveLinear];                                         [UIView setAnimationDuration:0.25];                                         CGAffineTransform transform = CGAffineTransformMakeScale(1.1, 1.1);                                         beachBall.transform = transform;                                         beachBall.alpha = 0.85;                                         [UIView commitAnimations];                                 }                 }         }                 if (count > 1) {                 touch1 = [[allTouches objectAtIndex:0] locationInView:self.view];                 touch2 = [[allTouches objectAtIndex:1] locationInView:self.view];         }         } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {         [super touchesMoved:touches withEvent:event];                 CGPoint currentTouch1;         CGPoint currentTouch2;                 NSArray *allTouches = [touches allObjects];         int count = [allTouches count];                 if (count == 1) {                 if (CGRectContainsPoint([beachBall frame], [[allTouches objectAtIndex:0] locationInView:self.view])) {                         beachBall.center = [[allTouches objectAtIndex:0] locationInView:self.view];                         return;                 }         }         if (count > 1) {                 if ((CGRectContainsPoint([beachBall frame], [[allTouches objectAtIndex:0] locationInView:self.view])) ||                         (CGRectContainsPoint([beachBall frame], [[allTouches objectAtIndex:1] locationInView:self.view]))) {                                 currentTouch1 = [[allTouches objectAtIndex:0] locationInView:self.view];                         currentTouch2 = [[allTouches objectAtIndex:1] locationInView:self.view];                                                 CGFloat previousAngle = atan2(touch2.y - touch1.y, touch2.x - touch1.x) * 180 / M_PI;                         CGFloat currentAngle = atan2(currentTouch2.y - currentTouch1.y,currentTouch2.x - currentTouch1.x) * 180 / M_PI;                                         CGFloat angleToRotate = currentAngle - previousAngle;                                         CGAffineTransform transform = CGAffineTransformRotate(beachBall.transform, degreesToRadians(angleToRotate));                                         beachBall.transform = transform;                         touch1 = currentTouch1;                         touch2 = currentTouch2;                 }                         } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {         [super touchesEnded:touches withEvent:event];                 UITouch *touch = [touches anyObject];                 if ([touch tapCount] == 2) {                 [UIView beginAnimations:nil context:NULL];                 [UIView setAnimationBeginsFromCurrentState:YES];                 [UIView setAnimationCurve:UIViewAnimationCurveLinear];                 [UIView setAnimationDuration:0.20];                 beachBall.center = self.view.center;                 beachBall.transform = CGAffineTransformIdentity;                 [UIView commitAnimations];                                 return;         } } #pragma mark Animation Selectors - (void)finishedTouchDownAnimation:(NSString*)animationID finished:(BOOL)finished context:(void *)context {         [UIView beginAnimations:nil context:NULL];         [UIView setAnimationBeginsFromCurrentState:YES];         [UIView setAnimationCurve:UIViewAnimationCurveLinear];         [UIView setAnimationDuration:0.25];         CGAffineTransform transform = CGAffineTransformMakeScale(1.0, 1.0);         beachBall.transform = transform;         beachBall.alpha = 1.0;         [UIView commitAnimations]; } - (void)dealloc {     [beachball release];     [super dealloc]; } @end

The first thing you want to do is set up a few things. Where you do this is entirely up to you, and what suits your application best. I prefer to viewDidLoad method. Go ahead and enable multi touch gestures and add the image view containing the beach ball image to your view. And to be sure, we don’t want the application auto rotating on us.

Really, the only methods we want to override are the touch methods. In the touchesBegan method, go ahead and count how many times the user has touched and tapped the screen.

If the touch count is only one, and the user has tapped the screen only once, check to see if the user touched the beach ball. If so, we’ll start an animation block. Although not required in most animation blocks, we want to set our view controller as the animation’s delegate and set an animationDidStop selector. Doing so will allow us to call our own method when thee animation stops.

Regardless of whether or not the animation finishes, we’ll still know when it stops. This is important. Setting a timer could cause the the follow up method to perform before the animation is completed, ie if the animation lags for one of any number of reasons. Waiting for completion of the animation is also a fail, because an animation might not finish animating completely, for any number of reasons. In any case, we will know.

In this same animation block, we will expand the image to 110% of its original size and set it’s alpha to 85%. Scrolling to the bottom of this file, you will find the animationDidStop selector. Notice that we don’t really need to know when this animation is stopped. Feel free to leave out the setAnimationDelegate and setAnimationDidStopSelector messages. Make sure to reset the original values of our beach ball.

If the user touches a second finger (or a third, fourth, or fifteenth finger) to the screen, we want to register the points of two fingers. Use the first and second finger to touch the screen for simplicity’s sake.

In the touchesMoved, things get interested. First, we want to know how many fingers the user is moving across the screen. If only one finger is being used, let’s go ahead and move our beach ball to where the finger is.

If more than one finger are being used, and at least one finger is on the beach ball itself, let’s start the rotation method. Get the angle of where the fingers were last by using a built in arctangent function. atan2() is what we want. Is different than atan() in that it takes two inputs. The arctangent will return the angle (in radians) between a coordinate and the x axis. [And you thought you'd never use trigonometry outside of school!] Using the same atan2 function, get the angle of the where the fingers currently are.

The next optional step I take is to convert both angles into degrees from their radian value. That can be calculated like so: degrees = radians * (180 / pi). Remember that a radian is the measure of an arc that extends along a circle’s circumference for the distance of the circle’s radius.

Finally, and this step is often missed, get the difference between the angles. If the fingers started out at 89 degrees, and we moved to 94 degrees, we only moved our fingers five degrees. Accordingly, we only want to move the beach ball five degrees. Create a rotational transform out of the difference, and apply it to the beach ball. Make sure to reset the original touch points to the current touch points, else your touchesMoved will never show any more change than the initial rotation.

In your touchesEnded method, all we are really doing is looking for the end of a double tap. If detected, reset the center of the beach ball to the center of the screen, and reset its transform to its identity transform.

And you are now done. If you actually were to release an application based off of this code, then please add in a flag for the touchesMoved that lifts upon touchesEnded that controls whether to move the beach ball if one finger is stopped/lifted. In a real life application, you wouldn’t want that. Until both fingers are lifted, keep on rotating the ball. Setting a simple boolean will fix that.

Source Code: http://www.touchrepo.com/SampleCode/TEST_SPINVIEW_APP.zip

More comments at: ipodtouchfans.com

Comments

3 Comments on Multitouch/Singletouch/Tap handling

  1. Web developer on Tue, 18th Aug 2009 1:06 am
  2. That was an inspiring post,

    Anyway, thanks for the post

  3. Mike on Sat, 3rd Oct 2009 7:45 pm
  4. Hey Skylar, thanks for the great tutorial, I’ve used a portion of it (namely much of the TouchesMoved method) in an app I’m working on and I had a question. When I test the app on the device I run into the problem, that I believe you mentioned in the last paragraph, about telling the device when to stop looking for single touches and only work with the rotation code until both fingers have left the screen. I’ve attempted to set a BOOL as you mention but I seem to be making things more complicated than they are, and breaking the entire rotation method in the process. If you have a minute it would be awesome if you could show the couple of lines of code I’m probably butchering, or even just hit me up on my email addy! Any help is greatly appreciated! Thanks man, and keep up the great work!

    -Mike

  5. H. Larry on Thu, 8th Oct 2009 1:18 pm
  6. Yes, I’m having the same issue that Mike is having. Do you mean that I should set up a simple (BOOL)thisVariable = YES; and then link that to the TouchesEnded method? If you could clear that part up it would be neat since it hasn’t worked that way for me.

Tell me what you're thinking...
and oh, if you want a pic to show with your comment, go get a gravatar!





Powered by WP Hashcash