I would like to know how we can use UIPinchGestureRecognizer
to scale UIView
in single (x or y) directions alone. Say, if the user moves his two fi
I created a custom version of a UIPinchGestureRecognizer to do exactly what you're looking for. It uses the slope of line between the two fingers to determine the direction of the scale. It does 3 types: Vertical; Horizontal; and Combined(diagonal). Please see my notes at the bottom.
-(void) scaleTheView:(UIPinchGestureRecognizer *)pinchRecognizer
{
if ([pinchRecognizer state] == UIGestureRecognizerStateBegan || [pinchRecognizer state] == UIGestureRecognizerStateChanged) {
if ([pinchRecognizer numberOfTouches] > 1) {
UIView *theView = [pinchRecognizer view];
CGPoint locationOne = [pinchRecognizer locationOfTouch:0 inView:theView];
CGPoint locationTwo = [pinchRecognizer locationOfTouch:1 inView:theView];
NSLog(@"touch ONE = %f, %f", locationOne.x, locationOne.y);
NSLog(@"touch TWO = %f, %f", locationTwo.x, locationTwo.y);
[scalableView setBackgroundColor:[UIColor redColor]];
if (locationOne.x == locationTwo.x) {
// perfect vertical line
// not likely, but to avoid dividing by 0 in the slope equation
theSlope = 1000.0;
}else if (locationOne.y == locationTwo.y) {
// perfect horz line
// not likely, but to avoid any problems in the slope equation
theSlope = 0.0;
}else {
theSlope = (locationTwo.y - locationOne.y)/(locationTwo.x - locationOne.x);
}
double abSlope = ABS(theSlope);
if (abSlope < 0.5) {
// Horizontal pinch - scale in the X
[arrows setImage:[UIImage imageNamed:@"HorzArrows.png"]];
arrows.hidden = FALSE;
// tranform.a = X-axis
NSLog(@"transform.A = %f", scalableView.transform.a);
// tranform.d = Y-axis
NSLog(@"transform.D = %f", scalableView.transform.d);
// if hit scale limit along X-axis then stop scale and show Blocked image
if (((pinchRecognizer.scale > 1.0) && (scalableView.transform.a >= 2.0)) || ((pinchRecognizer.scale < 1.0) && (scalableView.transform.a <= 0.1))) {
blocked.hidden = FALSE;
arrows.hidden = TRUE;
} else {
// scale along X-axis
scalableView.transform = CGAffineTransformScale(scalableView.transform, pinchRecognizer.scale, 1.0);
pinchRecognizer.scale = 1.0;
blocked.hidden = TRUE;
arrows.hidden = FALSE;
}
}else if (abSlope > 1.7) {
// Vertical pinch - scale in the Y
[arrows setImage:[UIImage imageNamed:@"VerticalArrows.png"]];
arrows.hidden = FALSE;
NSLog(@"transform.A = %f", scalableView.transform.a);
NSLog(@"transform.D = %f", scalableView.transform.d);
// if hit scale limit along Y-axis then don't scale and show Blocked image
if (((pinchRecognizer.scale > 1.0) && (scalableView.transform.d >= 2.0)) || ((pinchRecognizer.scale < 1.0) && (scalableView.transform.d <= 0.1))) {
blocked.hidden = FALSE;
arrows.hidden = TRUE;
} else {
// scale along Y-axis
scalableView.transform = CGAffineTransformScale(scalableView.transform, 1.0, pinchRecognizer.scale);
pinchRecognizer.scale = 1.0;
blocked.hidden = TRUE;
arrows.hidden = FALSE;
}
} else {
// Diagonal pinch - scale in both directions
[arrows setImage:[UIImage imageNamed:@"CrossArrows.png"]];
blocked.hidden = TRUE;
arrows.hidden = FALSE;
NSLog(@"transform.A = %f", scalableView.transform.a);
NSLog(@"transform.D = %f", scalableView.transform.d);
// if we have hit any limit don't allow scaling
if ((((pinchRecognizer.scale > 1.0) && (scalableView.transform.a >= 2.0)) || ((pinchRecognizer.scale < 1.0) && (scalableView.transform.a <= 0.1))) || (((pinchRecognizer.scale > 1.0) && (scalableView.transform.d >= 2.0)) || ((pinchRecognizer.scale < 1.0) && (scalableView.transform.d <= 0.1)))) {
blocked.hidden = FALSE;
arrows.hidden = TRUE;
} else {
// scale in both directions
scalableView.transform = CGAffineTransformScale(scalableView.transform, pinchRecognizer.scale, pinchRecognizer.scale);
pinchRecognizer.scale = 1.0;
blocked.hidden = TRUE;
arrows.hidden = FALSE;
}
} // else for diagonal pinch
} // if numberOfTouches
} // StateBegan if
if ([pinchRecognizer state] == UIGestureRecognizerStateEnded || [pinchRecognizer state] == UIGestureRecognizerStateCancelled) {
NSLog(@"StateEnded StateCancelled");
[scalableView setBackgroundColor:[UIColor whiteColor]];
arrows.hidden = TRUE;
blocked.hidden = TRUE;
}
}
Remember to add the protocol to the view controller header file:
@interface WhiteViewController : UIViewController <UIGestureRecognizerDelegate>
{
IBOutlet UIView *scalableView;
IBOutlet UIView *mainView;
IBOutlet UIImageView *arrows;
IBOutlet UIImageView *blocked;
}
@property (strong, nonatomic) IBOutlet UIView *scalableView;
@property (strong, nonatomic) IBOutlet UIView *mainView;
@property (strong, nonatomic)IBOutlet UIImageView *arrows;
@property (strong, nonatomic)IBOutlet UIImageView *blocked;
-(void) scaleTheView:(UIPinchGestureRecognizer *)pinchRecognizer;
@end
And add the recognizer in the viewDidLoad:
- (void)viewDidLoad
{ UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(scaleTheView:)];
[pinchGesture setDelegate:self];
[mainView addGestureRecognizer:pinchGesture];
arrows.hidden = TRUE;
blocked.hidden = TRUE;
[scalableView setBackgroundColor:[UIColor whiteColor]];}
This is set up to use the main view to capture the pinch; and manipulate a second view. This way you can still scale it as the view gets small. You can change it to react directly to the scalable view.
LIMITS: I arbitrarily chose the starting size of my view so a scale limit of 2.0 would equal full screen. My lower scale is set at 0.1.
USER INTERACTION: I mess around with a lot of user interaction things like changing the view's background color and adding/changing arrows over the view to show direction. It's important to give them feedback during the scaling process, especially when changing directions like this codes allows.
BUG: There is a bug in Apple's UIPinchGestureRecognizer. It registers UIGestureRecognizerStateBegan with the touch of 2 fingers as you would expect. But once it is in StateBegan or StateChanged you can lift one finger and the state remains. It doesn't move to StateEnded or StateCancelled until BOTH fingers are lifted. This created a bug in my code and many headaches! The if numberOfTouches > 1 fixes it.
FUTURE: You can change the slope settings to scale in just one direction, or just 2. If you add the arrows images, you can see them change as you rotate your fingers.
If I understand correctly your question, you are aiming at having non-proportional scaling along the horizontal and vertical axis. In this case, the thing comes down to providing your affine transform:
piece.transform = CGAffineTransformScale([piece transform], hScale, vScale);
with different scaling factors.
One way to calculate them is the following:
define an ivar in your class to store the lastTouchPosition
;
in your gesture handler you will do something like this:
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan){
lastTouchPosition = [gestureRecognize locationInView:yourViewHere];
hScale = 1;
vScale = 1;
} else if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged){
CGPoint currentTouchLocation = [gestureRecognize locationInView:yourViewHere];
CGPoint deltaMove = CGPointDistance(currentTouchLocation, lastTouchPosition);
float distance = sqrt(deltaMove.x*deltaMove.x + deltaMove.y*deltaMove.y);
hScale -= abs(deltaMove.x)/distance * (1-gestureRecognizer.scale);
vScale -= abs(deltaMove.y)/distance * (1-gestureRecognizer.scale);
piece.transform = CGAffineTransformScale([piece transform], hScale, vScale);
[gestureRecognizer setScale:1];
lastTouchPosition = currentTouchLocation;
}
An alternative way of doing is:
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan){
lastTouchPosition = [gestureRecognize locationInView:yourViewHere];
} else if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged){
CGPoint currentTouchLocation = [gestureRecognize locationInView:yourViewHere];
CGPoint deltaMove = CGPointDistance(currentTouchLocation, lastTouchPosition);
float distance = sqrt(deltaMove.x*deltaMove.x + deltaMove.y*deltaMove.y);
float hScale = 1 - abs(deltaMove.x)/distance * (1-gestureRecognizer.scale);
float vScale = 1 - abs(deltaMove.y)/distance * (1-gestureRecognizer.scale);
piece.transform = CGAffineTransformScale([piece transform], hScale, vScale);
lastTouchPosition = currentTouchLocation;
}
This will not store at each iteration the current hFloat
and vFloat
and instead rely on the fact that the gestureRecognizer
will accumulate the overall scale change. It does an "absolute" calculate, while the first implementation does a "relative" one.
Note that you need also define CGPointDistance
to calculate the distance between the two touches and choose which view are your going to use to calculate the distance (yourViewHere
).
EDIT:
CGPoint CGPointDistance(CGPoint point1, CGPoint point2)
{
return = CGPointMake(point2.x - point1.x, point2.y - point1.y);
};
EDIT2: about the formula to calculate the scaling
The idea is calculating the variation in the scale factor and apply it to the two directions (x and y) according to their relative variations.
the scale factor delta is: 1-gestureRecognizer.scale
;
the delta is the multiplied by a factor so that it is somehow proportional to the displacement along the horizontal or vertical axis; when the displacement is zero, the delta scale is also zero; when the displacement is equal along the two axis, the scale delta is also equal along the two axis; I decided to divide by the distance
, but there are other possibilities (like dividing by the sum of the two deltaMoves or by the max of the deltaMoves, etc).
the adjusted delta is finally subtracted from the current scale of the element.
Today I am facing the same problem and I found a simple and short way to do this
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer
{
recognizer.view.transform = CGAffineTransformScale(recognizer.view.transform, recognizer.scale,1);
recognizer.scale = 1;
}