简体   繁体   中英

Using PinchGesture; how can I zoom in to where a user's fingers actually “pinch”?

I've implemented the UIPinchGestureRecognizer on a UIImageView in my app, however no matter where I pinch on the image, it seems to zoom into the same spot. Does anyone know how I can make it zoom in to where a user actually "pinches"? See code below.

ViewController.m

 - (IBAction)scaleImage:(UIPinchGestureRecognizer *)recognizer {

   recognizer.view.transform = CGAffineTransformScale(recognizer.view.transform, recognizer.scale, recognizer.scale);
   recognizer.scale = 1; 

 }

 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
{
    BOOL shouldReceiveTouch = YES;

    if (gestureRecognizer == tap) {
        shouldReceiveTouch = (touch.view == featureImage);
    }
    return shouldReceiveTouch;
}

A scale transform leaves the origin (0, 0) untouched. So to scale a view around a particular point, you must first translate that point to the origin, then apply the scale, then translate back.

- (IBAction)pinchGestureDidFire:(UIPinchGestureRecognizer *)pinch {

First, we get the view being pinched.

    UIView *pinchView = pinch.view;

To compute the center of the pinch, we'll need the midpoint of the view's bounds, so we get the bounds too:

    CGRect bounds = pinchView.bounds;

The center is based on the centroid of the pinch's touches, which we get this way:

    CGPoint pinchCenter = [pinch locationInView:pinchView];

But we actually need the pinch offset relative to the center of the view, because the view's transform is relative to the center of the view by default. (You can change this by changing the view's layer.anchorPoint .)

    pinchCenter.x -= CGRectGetMidX(bounds);
    pinchCenter.y -= CGRectGetMidY(bounds);

Now we can update the view's transform. First we get its current transform:

    CGAffineTransform transform = pinchView.transform;

Then we update it to translate the pinch center to the origin:

    transform = CGAffineTransformTranslate(transform, pinchCenter.x, pinchCenter.y);

Now we can apply the scale:

    CGFloat scale = pinch.scale;
    transform = CGAffineTransformScale(transform, scale, scale);

Then we translate the view back:

    transform = CGAffineTransformTranslate(transform, -pinchCenter.x, -pinchCenter.y);

Now we can update the view with the modified transform:

    pinchView.transform = transform;

Finally, we reset the gesture recognizer's scale, since we've applied the current scale:

    pinch.scale = 1.0;
}

Demo:

捏缩放

Note that in the simulator, you can hold option (alt) for a pinch gesture. Holding shift (while holding option) moves the two touches together.

Here's the code all together for copy/paste:

- (IBAction)pinchGestureDidFire:(UIPinchGestureRecognizer *)pinch {
    UIView *pinchView = pinch.view;
    CGRect bounds = pinchView.bounds;
    CGPoint pinchCenter = [pinch locationInView:pinchView];
    pinchCenter.x -= CGRectGetMidX(bounds);
    pinchCenter.y -= CGRectGetMidY(bounds);
    CGAffineTransform transform = pinchView.transform;
    transform = CGAffineTransformTranslate(transform, pinchCenter.x, pinchCenter.y);
    CGFloat scale = pinch.scale;
    transform = CGAffineTransformScale(transform, scale, scale);
    transform = CGAffineTransformTranslate(transform, -pinchCenter.x, -pinchCenter.y);
    pinchView.transform = transform;
    pinch.scale = 1.0;
}

Swift implementation of rob's answer:

@objc private func pinchHandler(gesture: UIPinchGestureRecognizer) {
    if let view = gesture.view {

        switch gesture.state {
        case .changed:
            let pinchCenter = CGPoint(x: gesture.location(in: view).x - view.bounds.midX,
                                      y: gesture.location(in: view).y - view.bounds.midY)
            let transform = view.transform.translatedBy(x: pinchCenter.x, y: pinchCenter.y)
                                            .scaledBy(x: gesture.scale, y: gesture.scale)
                                            .translatedBy(x: -pinchCenter.x, y: -pinchCenter.y)
            view.transform = transform
            gesture.scale = 1
        case .ended:
            // Nice animation to scale down when releasing the pinch.
            // OPTIONAL
            UIView.animate(withDuration: 0.2, animations: {
                view.transform = CGAffineTransform.identity
            })
        default:
            return
        }


    }
}

Rob Mayoff's (@robmayoff) answer is truly inspiring and I wish I had found it earlier. But I actually solved this by changing the anchor point rather than using a transform translation. Effectively it has the same result.

I have not seen this solution on SO (yet...) and figured this was a good place for it.

Transforms work relative to the view layer anchor point. But you can't just set the position point to the pinch center point if the view has been transformed. So here's a method to set the layer anchor point AND correctly convert the position point as well:

// change the anchor point for the view layer
- (void)setAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)view {
    // sanity check - x and y MUST be between 0 and 1
    if (anchorPoint.x < 0 || anchorPoint.x > 1 ||
        anchorPoint.y < 0 || anchorPoint.y > 1) {
        return;
    }

    CGPoint newPoint = CGPointMake(view.bounds.size.width * anchorPoint.x,
                                   view.bounds.size.height * anchorPoint.y);
    CGPoint oldPoint = CGPointMake(view.bounds.size.width * view.layer.anchorPoint.x,
                                   view.bounds.size.height * view.layer.anchorPoint.y);

    newPoint = CGPointApplyAffineTransform(newPoint, view.transform);
    oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform);

    CGPoint position = view.layer.position;

    position.x -= oldPoint.x;
    position.x += newPoint.x;

    position.y -= oldPoint.y;
    position.y += newPoint.y;

    view.layer.position = position;
    view.layer.anchorPoint = anchorPoint;
}

And here's the handlePinch method that uses the setAnchorPoint method to set the layer anchor point to the center point of the pinch touch points:

- (void)handlePinch:(UIPinchGestureRecognizer *)recognizer {
    // make these static so they can be used across gesture states
    static CGAffineTransform initialTransform;
    static CGPoint initialAnchor;

    if (recognizer.state == UIGestureRecognizerStateBegan) {
        // save these for later states
        initialTransform = recognizer.view.transform;
        initialAnchor = recognizer.view.layer.anchorPoint;

        // get the center point of the pinch
        CGPoint touch = [recognizer locationInView:recognizer.view];

        // anchor point is relative to the view bounds:  0 ... up to 1.0, for both x and y
        CGFloat anchorX = touch.x / recognizer.view.bounds.size.width;
        CGFloat anchorY = touch.y / recognizer.view.bounds.size.height;

        // set the layer anchor point AND position, to where the view was initially pinched
        [self setAnchorPoint:CGPointMake(anchorX,anchorY) forView:recognizer.view];

    } else if (recognizer.state == UIGestureRecognizerStateChanged) {
        // perform the pinch zoom
        recognizer.view.transform = CGAffineTransformScale(initialTransform,recognizer.scale,recognizer.scale);

    } else if (recognizer.state == UIGestureRecognizerStateEnded) {
        // reset the scale when it's done
        recognizer.scale = 1;

        // restore the original anchor point
        [self setAnchorPoint:initialAnchor forView:recognizer.view];
    }
}

You could simply add a scrollView and in the scrollView add a imageView as below,

![ImageView on top of scrollView][1]

Some issue with image upload so check image at this link

Then, you could create a IBOutlet property of scrollView and imageView ,and connect it respectively. Later, add these lines in your viewDidAppear method,

_scrollView.maximumZoomScale = 10.0;        //Maximum Zoom
_scrollView.minimumZoomScale = minimumScale;   //Minimum Zoom

Also add below method,

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
   return _imageView;
}

So now you would get the zoom in/out effects and maximum/minimum zoom limit that u set. Lesser code along with achieving ur goal.

ViewController.h

@property (strong, nonatomic) IBOutlet UIImageView *img;

ViewController.m

- (void)viewDidLoad

 {

    [super viewDidLoad];

    UIPinchGestureRecognizer *pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchDetected:)];

    [_img addGestureRecognizer:pinchRecognizer];

    // Do any additional setup after loading the view, typically from a nib.

}

- (void)pinchDetected:(UIPinchGestureRecognizer *)pinchRecognizer

{

    CGFloat scale = pinchRecognizer.scale;

    _img.transform = CGAffineTransformScale(self.img.transform, scale, scale);

    pinchRecognizer.scale = 1.0;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM