使用PinchGesture,如何放大到用户实际“捏”的位置?

19

我在我的应用程序中的UIImageView上实现了UIPinchGestureRecognizer,但是无论我在图像的哪个位置缩放,它似乎都会缩放到相同的点。有谁知道如何使其缩放到用户实际“捏合”的位置?请参见下面的代码。

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;
}
5个回答

39

缩放变换不会改变原点(0,0)。因此,如果要将视图围绕特定点进行缩放,必须先将该点平移到原点,然后应用缩放,最后再平移回来。

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

首先,我们得到被捏住的视图。

    UIView *pinchView = pinch.view;
为了计算捏合点的中心位置,我们需要获取视图边界的中点,因此我们也要获取边界:

要计算捏合点的中心位置,我们需要获得视图边界的中点,因此我们还要获取边界:

    CGRect bounds = pinchView.bounds;

中心点基于捏合触点的重心,我们通过以下方式获得:

    CGPoint pinchCenter = [pinch locationInView:pinchView];

但实际上我们需要相对于视图中心的捏合偏移量,因为视图的变换默认是相对于视图中心的。 (您可以通过更改视图的layer.anchorPoint来更改此设置。)

    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;
}

演示:

收缩比例

请注意,在模拟器中,您可以按住选项(alt)进行收缩手势。同时按住Shift键(同时按下选项键),可以将两个触摸点移动到一起。

以下是全部代码,可供复制粘贴使用:

- (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;
}

这对我似乎不起作用(尽管它看起来应该)?我实现了上面的内容,但我仍然只能将图像缩放到一个点。 - Brittany
1
你确定将手势识别器连接到了新方法而不是旧方法吗?你是否在新方法中放置了NSLog或断点以确保它被调用了? - rob mayoff
能否应用非等比缩放?我的意思是分别应用x和y的变化? - Siarhei
使用UIPinchGestureRecognizer直接实现是不可能的,因为该识别器仅报告单个“scale”因子。您需要查看手势中的各个触摸点,在每次更新时跟踪它们的移动,并自己计算各个X和Y比例因子。 - rob mayoff
嘿@robmayoff,如果我在缩放过程中移动图片,然后再次缩放,x和y会被破坏吗?还有一个问题,如何限制最小缩放边界? - Boris
显示剩余2条评论

22

Swift实现rob的答案:

@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
        }


    }
}

3

罗布·马约夫(@robmayoff)的回答真是令人鼓舞,但我实际上是通过改变锚点而不是使用转换平移来解决这个问题的。它实际上具有相同的结果。

我还没有在SO上看到这个解决方案(但可能会出现...),并认为这是一个很好的地方。

变换是相对于视图层锚点工作的。但如果视图已经被转换了,你不能只把位置点设置为捏合中心点。因此,这里有一个方法来设置层的锚点,并正确地转换位置点:

// 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;
}

这里是handlePinch方法,它使用setAnchorPoint方法将图层的锚点设置为捏合触摸点的中心点:

- (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];
    }
}

1
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;
}

2021年4月...这个可行(其他的不行)- 为滚动视图进行了修改。简短明了。谢谢! - user3741598

0
你可以简单地添加一个scrollView,然后在scrollView中像下面这样添加一个imageView:
![ImageView on top of scrollView][1]

图片上传存在问题,请在链接处查看图片。

接下来,您可以创建scrollView和imageView的IBOutlet属性,并分别连接它们。然后,在viewDidAppear方法中添加以下几行代码:

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

还需添加以下方法:

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

所以现在您将获得缩放效果和您设置的最大/最小缩放限制。实现目标所需的代码更少。

我本来会这样做的,但是我不喜欢在放大的图像上看到滚动条的想法 :/ - Brittany
能够看到滚动条的想法是让用户知道他可以移动以查看图像(因为在缩放的情况下,他将看到完整图像的部分)。 - nikhil84
1
您可以通过取消“显示水平/垂直指示器”选项来从界面构建中禁用滚动条。甚至可以通过编程方式完成。 - Abdurrahman Mubeen Ali

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接