How Can I Coordinate the Behavior of Two UIGestureRecognizers? - ios

I have attached a pan and pinch gesture to a view. I want to coordinate them such that when the each complete one cycle of "UIGestureRecognizerStateChanged" state I want to take a coordinated action that aggregates the updated information from each. Specifically, my pan controls translation and my pinch controls scale. I want to do incremental matrix concatenate of both translation and scale in a single place rather then have each concatenate autonomously which results in an unpleasant stair-step type movement.
I have stared at the UIGestureRecognizerDelegate docs but see no way to make it do what I want.
Thanks,
Doug

The first thing to try is making sure the recognizers are working "simultaneously". In your delegate, you want to define the following:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return (gestureRecognizer.view == otherGestureRecognizer.view);
}
In my experience, that's enough to give an adequately smooth experience. However, if it's not, then what I'd do is to only update your transform from one of the recognizers, storing the state from the other in a property. Let's say that you've declared a CGFloat property called cachedScale, which you set to 1.0 in your initializer. Then in your pinch and pan handlers, you'd do the following:
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer
{
self.cachedScale *= recognizer.scale;
}
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer
{
CGAffineTransform transform = CGAffineTransformMakeScale(self.cachedScale, self.cachedScale);
self.cachedScale = 1.0;
CGPoint translation = [recognizer translationInView:recognizer.view.superview];
CGAffineTransformTranslate(transform, translation.x, translation.y);
// do something with your transform
}
If you're just trying to drag a view around, you may have more luck changing the view's center rather than applying a translation to its transform.

Related

Do I have to use a UIPanGestureRecognizer instead of a UISwipeGestureRecognizer if I want to track the movement?

I'm trying to implement a paging interaction for one of my views. I thought I would just use UISwipeGestureRecognizers. But through trial and error as well as examination of the documentation it appears that
A swipe is a discrete gesture, and thus the associated action message
is sent only once per gesture.
So while I could trigger the page, I wouldn't be able to hook up animation that occurred during the drag.
Is my only alternative to use a UIPanGestureRecognizer and reimplement the basic filtering/calculations of the swipe?
Update/Redux
In hindsight, what I really should have been asking is how to implement a "flick" gesture. If you're not going to roll your own subclass (may bite that off in a bit), you use a UIPanGestureRecognizer as #Lyndsey 's answer indicates. What I was looking for after that (in the comments) was how to do the flick part, where the momentum of the flick contributes to the decision of whether to carry the motion of the flick through or snap back to the original presentation.
UIScrollView has behavior like that and it's tempting to mine its implementation for details on how one decelerates the momentum in a way that would be consistent, but alas the decelerationRate supplied for UIScrollView is "per iteration" value (according to some). I beat my head on how to properly apply the default value of 0.998 to the end velocity of my pan.
In the end, I used code pulled from sites about "flick" computation and did something like this in my gesture handler:
...
else if (pan.state == UIGestureRecognizerStateEnded) {
CGFloat v = [pan velocityInView: self.view].x;
CGFloat a = -4000.0; // 4 pixels/millisecond, recommended on gavedev
CGFloat decelDisplacement = -(v * v) / (2 * a); // physics 101
// how far have we come plus how far will momentum carry us?
CGFloat totalDisplacement = ABS(translation) + decelDisplacement;
// if that is (or will be) half way across our view, finish the transition
if (totalDisplacement >= self.view.bounds.size.width / 2) {
// how much time would we need to carry remainder across view with current velocity and existing displacement? (capped)
CGFloat travelTime = MIN(0.4, (self.view.bounds.size.width - ABS(translation)) * 2 / ABS(v));
[UIView animateWithDuration: travelTime delay: 0.0 options: UIViewAnimationOptionCurveEaseOut animations:^{
// target/end animation positions
} completion:^(BOOL finished) {
if (finished) {
// any final state change
}
}];
}
else { // put everything back the way it was
...
}
}
Yes, use a UIPanGestureRecognizer if you want the specific speed, angle, changes, etc. of the "swipe" to trigger your animations. A UISwipeGestureRecognizer is indeed a single discrete gesture; similar to a UITapGestureRecognizer, it triggers a single action message upon recognition.
As in physics, the UIPanGestureRecognizer's "velocity" will indicate both the speed and direction of the pan gesture. Here are the docs for velocityInView: method which will help you calculate the horizontal and vertical components of the changing pan gesture in points per second.

Point inside a rotated CGRect

How would you properly determine if a point is inside a rotated CGRect/frame?
The frame is rotated with Core Graphics.
So far I've found an algorithm that calculates if a point is inside a triangle, but that's not quite what I need.
The frame being rotated is a regular UIView with a few subviews.
Let's imagine that you use transform property to rotate a view:
self.sampleView.transform = CGAffineTransformMakeRotation(M_PI_2 / 3.0);
If you then have a gesture recognizer, for example, you can see if the user tapped in that location using locationInView with the rotated view, and it automatically factors in the rotation for you:
- (void)handleTap:(UITapGestureRecognizer *)gesture
{
CGPoint location = [gesture locationInView:self.sampleView];
if (CGRectContainsPoint(self.sampleView.bounds, location))
NSLog(#"Yes");
else
NSLog(#"No");
}
Or you can use convertPoint:
- (void)handleTap:(UITapGestureRecognizer *)gesture
{
CGPoint locationInMainView = [gesture locationInView:self.view];
CGPoint locationInSampleView = [self.sampleView convertPoint:locationInMainView fromView:self.view];
if (CGRectContainsPoint(self.sampleView.bounds, locationInSampleView))
NSLog(#"Yes");
else
NSLog(#"No");
}
The convertPoint method obviously doesn't need to be used in a gesture recognizer, but rather it can be used in any context. But hopefully this illustrates the technique.
Use CGRectContainsPoint() to check whether a point is inside a rectangle or not.

Panning a UIImage with a Gesture Recognizer on a placeholder UIView

I need to crop an image to match a specific dimension. I have three layers in my view starting from the bottom:
I have the raw image in a UIImage. This image comes from the camera. (called cameraImage)
I have a UIView holding this image. When user clicks "crop", the UIView's bounds are used to crop the raw image inside it.
Above all of this I have a guide image which shows the user the dimensions they need to pan, rotate, and pinch their image to fit into.
I want to add the pan gesture to the top guide image and have it control the raw image at the bottom. So the guide image never moves but it is listening for the pan gesture.
I can't figure out how to reset the recognizer without it making my raw image jump back to zero. Maybe someone could help?
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
recognizer.view.center = CGPointMake(recognizer.view.center.x+translation.x, recognizer.view.center.y+ translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:recognizer.view];
}
The above code works great in my gesture is attached to the bottom image. The problem is when the user goes outside the view's bounds, the image stops panning and is basically stuck. You can't touch it anymore so it sits there. So I thought if i attached the gesture to the top it would solve this problem.
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
cameraImage.center = CGPointMake(recognizer.view.center.x+translation.x, recognizer.view.center.y+ translation.y);
}
This almost works. I set the cameraImage's center and removed the third line which resets the recognizer. If I don't remove it, the cameraImage jumps back into the same position every time I try and pan. It almost works because when you click the image again it doesn't start from the pixel you touched. It moves the image back to the original position and then lets you pan.
First option:
when the recognizer enter the UIGestureRecognizerStateEndedstate
if(recofnizer.state == UIGestureRecognizerStateEnded )
{
...
}
You store the translation at that point in time in an instance varibale (#property) of your class.
And then you always add the saved translation to the new translation.
In code this would look like this:
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint updatedTranslation = CGPointMake(translation.x+self.savedTranslation.x,translation.y+self.savedTranslation.y);
cameraImage.center = CGPointMake(recognizer.view.center.x+updatedTranslation.x, recognizer.view.center.y+ updatedTranslation.y);
if(recofnizer.state == UIGestureRecognizerStateEnded )
{
self.savedTranslation = updatedTranslation;
}
}
Dont forget to add #property (nonatomic, assign) CGPoint savedTranslation; to your interface.
Also make sure the savedTranslation variable is initialized in the init method of your class to self.savedTranslation = CGPointMake(0,0);
Second option:
You should think about doing all that what you want in an scrollview with the imageview as the viewForZooming of the scrollview. This allows very smooth interaction, like users are used to!
Above this scrollview you can then put your mask/guide, but make sure to disable the userInteraction of the mask/guide view to make your user touch the scrollview below!

Multiple Gesture Responders for a Single View

I have an image that I would like to set up to respond to several different gesture responders. So for example, if one part of the picture is touched I would like one selector to be called, and another selector for a different part of the picture.
I looked at the UIGestureRecognizer and UITapGestureRecognizer classes, but I couldn't find a way to specify the image zones to be associated with them. Is this at all possible in iOS? And if so what classes should I look into using?
The easiest solution is to lay invisible views over the image and put the gesture recognizers on them.
If that's not feasible you'll have to look at the locationInView in the gesture recognizer's tap handler and figure out what you want to do based on where the user tapped.
Use the locationInView: property to determine where your tap occurred and then conditionally invoke a method. You can do this by setting up some CGRects that correspond to your hit areas. Then use the CGRectContainsPoint() function to determine if the tap landed in one of the hit areas.
Your tap gesture recognizer action may look something like this:
- (void)tapGestureRecognized:(UIGestureRecognizer *)recognizer
{
// Specify some CGRects that will be hit areas
CGRect firstHitArea = CGRectMake(10.0f, 10.0f, 44.0f, 44.0f);
CGRect secondHitArea = CGRectMake(64.0f, 10.0f, 44.0f, 44.0f)
// Get the location of the touch in the view's coordinate space
CGPoint touchLocation = [recognizer locationInView:recognizer.view];
if (CGRectContainsPoint(firstHitArea, touchLocation))
{
[self firstMethod];
}
else if (CGRectContainsPoint(secondHitArea, touchLocation))
{
[self secondMethod];
}
}

IOS drag, flick, or fling a UIView

Was wondering how I would go about flicking or flinging a UIView, such as http://www.cardflick.co/ or https://squareup.com/cardcase demo videos.
I know how to drag items, but how do you give them gravity/inertia. Is this handled by IOS?
The kind of effects that you are describing as simulating a king of gravity/inertia can be produced by means of ease-out (start fast, end slow) and ease-in (start slow, end fast) timing functions.
Support for easing out and easing in is available in iOS, so I don't think you need any external library nor hard work (although, as you can imagine, your effect will need a lot of fine tuning).
This will animate the translation of an object to a given position with an ease-out effect:
[UIView animateWithDuration:2.0 delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^ {
self.image.center = finalPosition;
}
completion:NULL];
}
If you handle your gesture through a UIPanGestureRecognizer, the gesture recognizer will provide you with two important information to calculate the final position: velocity and translation, which represent respectively how fast and how much the object was moved.
You can install a pan gesture recognizer in your view (this would be the object you would like to animate, I guess) like this:
UIPanGestureRecognizer* panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanFrom:)];
[yourView addGestureRecognizer:panGestureRecognizer];
[panGestureRecognizer release];
And then handle the animation in your handler:
- (void)handlePanFrom:(UIPanGestureRecognizer*)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint velocity = [recognizer velocityInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateBegan) {
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
<track the movement>
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
<animate to final position>
}
}
iOS doesn't have any built-in API for gravity/inertia. Your best options are:
Simulate the effect with animations: if you are flicking an object toward a target (it doesn't need to bounce off things), you could probably get a pretty good result just by sending it to the target and tweaking the animation timing curve.
Use a physics library. Chipmunk is a high quality one and there is a lot of iOS support available. You set up objects, assign them weights, describe their shapes, and then the library code will give you updated (x,y) coordinates so you can update your objects on screen.
There's nothing built into iOS that does this for you, but you could easily implement it yourself. I'd start by creating a gesture recognizer that recognizes the "flick" gesture that you want to use, possibly something that looks for constant direction and increase in velocity. When you recognize such a gesture, you just have to animate the affected view's position appropriately.

Resources