Use UIPinchGestureRecognizer to scale view in direction of pinch [closed]

限于喜欢 提交于 2019-12-03 21:06:15

I created a custom version of a UIPinchGestureRecognizer. 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.

Here's a solution in Swift:

extension UIPinchGestureRecognizer {
    func scale(view: UIView) -> (x: CGFloat, y: CGFloat)? {
        if numberOfTouches() > 1 {
            let touch1 = self.locationOfTouch(0, inView: view)
            let touch2 = self.locationOfTouch(1, inView: view)
            let deltaX = abs(touch1.x - touch2.x)
            let deltaY = abs(touch1.y - touch2.y)
            let sum = deltaX + deltaY
            if sum > 0 {
                let scale = self.scale
                return (1.0 + (scale - 1.0) * (deltaX / sum), 1.0 + (scale - 1.0) * (deltaY / sum))
            }
        }
        return nil
    }
}

This alternative solution determines the direction of scaling based on bearing angle rather than slope. I find it a bit easier to adjust the different zones using angle measurements.

@objc func viewPinched(sender: UIPinchGestureRecognizer) {
    //  Scale the view either vertically, horizontally, or diagonally based on the axis of the initial pinch
    let locationOne = sender.location(ofTouch: 0, in: sender.view)
    let locationTwo = sender.location(ofTouch: 1, in: sender.view)
    let diffX = locationOne.x - locationTwo.x
    let diffY = locationOne.y - locationTwo.y

    //  Break the plane into 3 equal segments
    //  Inverse tangent will return between π/2 and -π/2. Absolute value can be used to only consider 0 to π/2 - don't forget to handle divide by 0 case
    //  Breaking π/2 into three equal pieces, we get regions of 0 to π/6, π/6 to 2π/6, and 2π/6 to π/2 (note 2π/6 = π/3)
    //  Radian reminder - π/2 is 90 degreees :)
    let bearingAngle = diffY == 0 ? CGFloat.pi / 2.0 : abs(atan(diffX/diffY))

    if sender.state == .began {
        //  Determine type of pan based on bearing angle formed by the two touch points.
        //  Only do this when the pan begins - don't change type as the user rotates their fingers. Require a new gesture to change pan type
        if bearingAngle < CGFloat.pi / 6.0 {
            panType = .vertical
        } else if bearingAngle < CGFloat.pi / 3.0 {
            panType = .diagonal
        } else if bearingAngle <= CGFloat.pi / 2.0 {
            panType = .horizontal
        }
    }

    //  Scale the view based on the pan type
    switch panType {
    case .diagonal: transform = CGAffineTransform(scaleX: sender.scale, y: sender.scale)
    case .horizontal: transform = CGAffineTransform(scaleX: sender.scale, y: 1.0)
    case .vertical: transform = CGAffineTransform(scaleX: 1.0, y: sender.scale)
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!