iOS同时进行双指捏合缩放和旋转的操作

31

以下是我的代码:

viewDidLoad方法:

UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(pinch:)];
[self.canvas addGestureRecognizer:pinch];
pinch.delegate = self;

UIRotationGestureRecognizer *twoFingersRotate = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(pinchRotate:)];
[[self canvas] addGestureRecognizer:twoFingersRotate];

twoFingersRotate.delegate = self;

捏和旋转的代码:

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

-(void)pinchRotate:(UIRotationGestureRecognizer*)rotate
{
    SMImage *selectedImage = [DataCenter sharedDataCenter].selectedImage;

    switch (rotate.state) 
    {
        case UIGestureRecognizerStateBegan:
        {
            selectedImage.referenceTransform = selectedImage.transform;
            break;
        }
        case UIGestureRecognizerStateChanged:
        {
            selectedImage.transform = CGAffineTransformRotate(selectedImage.referenceTransform, ([rotate rotation] * 55) * M_PI/180);
            break;
        }

        default:
            break;
    }
}

-(void)pinch:(UIPinchGestureRecognizer*)pinch
{
    SMImage *selectedImage = [DataCenter sharedDataCenter].selectedImage;
    [self itemSelected];

    switch (pinch.state) 
    {
        case UIGestureRecognizerStateBegan:
        {
            selectedImage.referenceTransform = selectedImage.transform;
            break;
        }
        case UIGestureRecognizerStateChanged:
        {
            CGAffineTransform transform = CGAffineTransformScale(selectedImage.referenceTransform, pinch.scale, pinch.scale);
            selectedImage.transform = transform;
            break;
        }

        default:
            break;
    }
}
我的旋转手势和缩放手势单独使用时都很好用,但是它们不能同时使用。其中一个手势总是有效,另一个则无效。当我实现shouldRecognizeSimultaneouslyWithGestureRecognizer方法时,两个手势似乎会相互抵消,导致结果不佳。我错过了什么吗?(是的,我已经实现了)
4个回答

36

每次调用pinch:时,您只需根据捏合识别器的比例计算变换。每次调用pinchRotate:时,您只需根据旋转识别器的旋转计算变换。您永远不会将比例和旋转组合成一个变换。

这是一种方法。给自己一个新的实例变量_activeRecognizers

NSMutableSet *_activeRecognizers;

viewDidLoad 中进行初始化:

_activeRecognizers = [NSMutableSet set];

将一个方法用作两个识别器的操作:

- (IBAction)handleGesture:(UIGestureRecognizer *)recognizer
{
    SMImage *selectedImage = [DataCenter sharedDataCenter].selectedImage;

    switch (recognizer.state) {
        case UIGestureRecognizerStateBegan:
            if (_activeRecognizers.count == 0)
                selectedImage.referenceTransform = selectedImage.transform;
            [_activeRecognizers addObject:recognizer];
            break;

        case UIGestureRecognizerStateEnded:
            selectedImage.referenceTransform = [self applyRecognizer:recognizer toTransform:selectedImage.referenceTransform];
            [_activeRecognizers removeObject:recognizer];
            break;

        case UIGestureRecognizerStateChanged: {
            CGAffineTransform transform = selectedImage.referenceTransform;
            for (UIGestureRecognizer *recognizer in _activeRecognizers)
                transform = [self applyRecognizer:recognizer toTransform:transform];
            selectedImage.transform = transform;
            break;
        }

        default:
            break;
    }
}
您将需要这个帮助方法:
- (CGAffineTransform)applyRecognizer:(UIGestureRecognizer *)recognizer toTransform:(CGAffineTransform)transform
{
    if ([recognizer respondsToSelector:@selector(rotation)])
        return CGAffineTransformRotate(transform, [(UIRotationGestureRecognizer *)recognizer rotation]);
    else if ([recognizer respondsToSelector:@selector(scale)]) {
        CGFloat scale = [(UIPinchGestureRecognizer *)recognizer scale];
        return CGAffineTransformScale(transform, scale, scale);
    }
    else
        return transform;
}

如果你只想允许旋转和缩放,那么这个方法是可行的。(我甚至已经测试过了!)

如果你想添加平移功能,使用另一个动作方法并调整selectedImage.center。试图使用selectedImage.transform同时进行旋转、缩放和平移会更加复杂。


你能给我一个代码示例,展示如何同时应用两者吗?我的UIRotationGestureRecognizer没有“scale”属性,而我的UIPinchGestureRecognizer没有“rotation”属性。我也不确定我应该使用哪个Affine来同时实现两者。 - spentak
工作得非常好。我现在拥有这个知识,感觉好多了,谢谢! - spentak
很高兴能够帮忙。顺便说一下,我改变了有关如何进行平移的最后一部分。此外,您可以将手势识别器放入XIB中,然后就不必在viewDidLoad中使用代码创建它们了。只需将识别器拖到视图上,然后使用从识别器到文件所有者的控制拖动将识别器的委托和操作输出连接起来。 - rob mayoff
handleGesture 函数来自哪里? - Leang Socheat
SMImage是什么?它是第三方的吗? - Mansi Dobariya
这种方法在Swift 5和iOS 13+上仍然有效吗?我使用它,但必须将其转换为Swift并且无法同时使两种变换类型起作用。也就是说,如果我从捏合(调整大小)开始,然后尝试旋转 - 旋转始于原始比例(图像返回到scale = 1),而不是当前比例。我只有一个图像视图,它填充整个安全区域,类似于照片应用程序中的详细照片视图。 - Rosen Dimov

13

使用Pan、Rotate和Pinch的Swift 3

// MARK: - Gesturies

    func transformUsingRecognizer(_ recognizer: UIGestureRecognizer, transform: CGAffineTransform) -> CGAffineTransform {

        if let rotateRecognizer = recognizer as? UIRotationGestureRecognizer {
            return transform.rotated(by: rotateRecognizer.rotation)
        }

        if let pinchRecognizer = recognizer as? UIPinchGestureRecognizer {
            let scale = pinchRecognizer.scale
            return transform.scaledBy(x: scale, y: scale)
        }

        if let panRecognizer = recognizer as? UIPanGestureRecognizer {
            let deltaX = panRecognizer.translation(in: imageView).x
            let deltaY = panRecognizer.translation(in: imageView).y
            return transform.translatedBy(x: deltaX, y: deltaY)
        }

        return transform
    }

    var initialTransform: CGAffineTransform?

    var gestures = Set<UIGestureRecognizer>(minimumCapacity: 3)

    @IBAction func processTransform(_ sender: Any) {

        let gesture = sender as! UIGestureRecognizer

        switch gesture.state {

        case .began:
            if gestures.count == 0 {
                initialTransform = imageView.transform
            }
            gestures.insert(gesture)

        case .changed:
            if var initial = initialTransform {
                gestures.forEach({ (gesture) in
                    initial = transformUsingRecognizer(gesture, transform: initial)
                })
                imageView.transform = initial
            }

        case .ended:
            gestures.remove(gesture)

        default:
            break
        }
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {

        return true
    }

你测试过了吗? - Roi Mulia
@RoiMulia 当然可以。目前在实际应用中使用它(尚未在生产环境中)。有什么问题吗? - Mike Glukhov
嗨,Mike,抱歉让你感到紧张。其实没什么问题,我只是想问一下是否已经测试过以确保它可以安全使用(以防万一我错过了某些结束场景)。 - Roi Mulia
我可以确认,这个不起作用。如果视图被旋转,平移手势在Y方向上无法正常工作。 - Deepak Sharma
几年晚了,但对我来说这完美地解决了问题。为我省了很多麻烦。 - Abdul Hadi Bharara

8
为了实现这个功能,您需要实现手势委托 shouldRecognizeSimultaneouslyWithGestureRecognizer 并设置您想要同时识别的手势。
// ensure that the pinch and rotate gesture recognizers on a particular view can all recognize simultaneously
// prevent other gesture recognizers from recognizing simultaneously
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    // if the gesture recognizers's view isn't one of our views, don't allow simultaneous recognition
    if (gestureRecognizer.view != firstView && gestureRecognizer.view != secondView)
        return NO;

    // if the gesture recognizers are on different views, don't allow simultaneous recognition
    if (gestureRecognizer.view != otherGestureRecognizer.view)
        return NO;

    // if either of the gesture recognizers is the long press, don't allow simultaneous recognition
    if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]] || [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]])
        return NO;

    return YES;
}

这段代码需要修改以适应您想要同时使用手势识别器的视图。上述代码就是您所需的。

他已经有一个shouldRecognizeSimultaneouslyWithGestureRecognizer:,它总是返回YES。 - rob mayoff

0

这个示例不使用手势识别器,而是直接计算变换矩阵。它还正确处理了一到两个手指的过渡。

class PincherView: UIView {

    override var bounds :CGRect {
        willSet(newBounds) {
            oldBounds = self.bounds
        } didSet {
            self.imageLayer.position = self.bounds
            self._adjustScaleForBoundsChange()
        }
    }
    var oldBounds :CGRect

    var touch₁  :UITouch?
    var touch₂  :UITouch?
    var p₁      :CGPoint?  // point 1 in image coordiate system
    var p₂      :CGPoint?  // point 2 in image coordinate system
    var p₁ʹ     :CGPoint?  // point 1 in view coordinate system
    var p₂ʹ     :CGPoint?  // point 2 in view coordinate system

    var image   :UIImage? {
        didSet {self._reset()}
    }
    var imageLayer :CALayer
    var imageTransform :CGAffineTransform {
        didSet {
            self.backTransform = self.imageTransform.inverted()
            self.imageLayer.transform = CATransform3DMakeAffineTransform(self.imageTransform)
        }
    }
    var backTransform  :CGAffineTransform
    var solutionMatrix :HXMatrix?

    required init?(coder aDecoder: NSCoder) {
        self.oldBounds = CGRect.zero
        let layer = CALayer();
        self.imageLayer = layer
        self.imageTransform = CGAffineTransform.identity
        self.backTransform = CGAffineTransform.identity

        super.init(coder: aDecoder)

        self.oldBounds = self.bounds
        self.isMultipleTouchEnabled = true
        self.layer.addSublayer(layer)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            let= touch.location(in: self).applying(self._backNormalizeTransform())
            let p = pʹ.applying(self.backTransform)
            if self.touch₁ == nil {
                self.touch₁ = touch
                self.p₁ʹ =self.p₁ = p
            } else if self.touch₂ == nil {
                self.touch₂ = touch
                self.p₂ʹ =self.p₂ = p
            }
        }
        self.solutionMatrix = self._computeSolutionMatrix()
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            let= touch.location(in: self).applying(self._backNormalizeTransform())
            if self.touch₁ == touch {
                self.p₁ʹ = pʹ
            } else if self.touch₂ == touch {
                self.p₂ʹ = pʹ
            }
        }

        CATransaction.begin()
        CATransaction.setValue(true, forKey:kCATransactionDisableActions)
        // Whether you're using 1 finger or 2 fingers
        if let q₁ʹ = self.p₁ʹ, let q₂ʹ = self.p₂ʹ {
            self.imageTransform = self._computeTransform(q₁ʹ, q₂ʹ)
        } else if let q₁ʹ = (self.p₁ʹ != nil ? self.p₁ʹ : self.p₂ʹ) {
            self.imageTransform = self._computeTransform(q₁ʹ, CGPoint(x:q₁ʹ.x + 10, y:q₁ʹ.y + 10))
        }
        CATransaction.commit()
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            if self.touch₁ == touch {
                self.touch₁ = nil
                self.p₁ = nil
                self.p₁ʹ = nil
            } else if self.touch₂ == touch {
                self.touch₂ = nil
                self.p₂ = nil
                self.p₂ʹ = nil
            }
        }
        self.solutionMatrix = self._computeSolutionMatrix()
    }

    //MARK: Private Methods

    private func _reset() {
        guard
            let image = self.image,
            let cgimage = image.cgImage else {
                return
        }

        let r = CGRect(x:0, y:0, width:cgimage.width, height:cgimage.height)
        imageLayer.contents = cgimage;
        imageLayer.bounds = r
        imageLayer.position = self.bounds
        self.imageTransform = self._initialTransform()
    }

    private func _normalizeTransform() -> CGAffineTransform {
        let center = self.bounds
        return CGAffineTransform(translationX: center.x, y: center.y)
    }

    private func _backNormalizeTransform() -> CGAffineTransform {
        return self._normalizeTransform().inverted();
    }

    private func _initialTransform() -> CGAffineTransform {
        guard let image = self.image, let cgimage = image.cgImage else {
            return CGAffineTransform.identity;
        }
        let r = CGRect(x:0, y:0, width:cgimage.width, height:cgimage.height)
        let s = r.scaleIn(rect: self.bounds)
        return CGAffineTransform(scaleX: s, y: s)
    }

    private func _adjustScaleForBoundsChange() {
        guard let image = self.image, let cgimage = image.cgImage else {
            return
        }
        let r = CGRect(x:0, y:0, width:cgimage.width, height:cgimage.height)
        let oldIdeal = r.scaleAndCenterIn(rect: self.oldBounds)
        let newIdeal = r.scaleAndCenterIn(rect: self.bounds)
        let s = newIdeal.height / oldIdeal.height
        self.imageTransform = self.imageTransform.scaledBy(x: s, y: s)
    }

    private func _computeSolutionMatrix() -> HXMatrix? {
        if let q₁ = self.p₁, let q₂ = self.p₂ {
            return _computeSolutionMatrix(q₁, q₂)
        } else if let q₁ = self.p₁, let q₁ʹ = self.p₁ʹ {
            let q₂ = CGPoint(x: q₁ʹ.x + 10, y: q₁ʹ.y + 10).applying(self.backTransform)
            return _computeSolutionMatrix(q₁, q₂)
        } else if let q₂ = self.p₂, let q₂ʹ = self.p₂ʹ {
            let q₁ = CGPoint(x: q₂ʹ.x + 10, y: q₂ʹ.y + 10).applying(self.backTransform)
            return _computeSolutionMatrix(q₂, q₁)
        }
        return nil
    }

    private func _computeSolutionMatrix(_ q₁:CGPoint, _ q₂:CGPoint) -> HXMatrix {
        let x₁ = Double(q₁.x)
        let y₁ = Double(q₁.y)
        let x₂ = Double(q₂.x)
        let y₂ = Double(q₂.y)
        let A = HXMatrix(rows: 4, columns: 4, values:[
            x₁, -y₁, 1, 0,
            y₁,  x₁, 0, 1,
            x₂, -y₂, 1, 0,
            y₂,  x₂, 0, 1
        ])
        return A.inverse()
    }

    private func _computeTransform(_ q₁ʹ:CGPoint, _ q₂ʹ:CGPoint) -> CGAffineTransform {
        guard let solutionMatrix = self.solutionMatrix else {
            return CGAffineTransform.identity
        }

        let B = HXMatrix(rows: 4, columns: 1, values: [
            Double(q₁ʹ.x),
            Double(q₁ʹ.y),
            Double(q₂ʹ.x),
            Double(q₂ʹ.y)
            ])
        let C = solutionMatrix  B

        let  U = CGFloat(C[0,0])
        let  V = CGFloat(C[1,0])
        let tx = CGFloat(C[2,0])
        let ty = CGFloat(C[3,0])

        var  t :CGAffineTransform = CGAffineTransform.identity
        t.a  =  U; t.b  = V
        t.c  = -V; t.d  = U
        t.tx = tx; t.ty = ty

        return t
    }
}

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