开发者

Can UIPinchGestureRecognizer and UIPanGestureRecognizer Be Merged?

I am struggling a bit trying to figure out if it is possible to create a single combined gesture recognizer that combines UIPinchGestureRecognizer with UIPanGestureRecognizer.

I am using pan for view translation and pinch for view scaling. I am doing incremental mat开发者_JAVA技巧rix concatenation to derive a resultant final transformation matrix that is applied to the view. This matrix has both scale and translation. Using separate gesture recognizers leads to a jittery movement/scaling. Not what I want. Thus, I want to handle concatenation of scale and translation once within a single gesture. Can someone please shed some light on how to do this?


6/14/14: Updated Sample Code for iOS 7+ with ARC.

The UIGestureRecognizers can work together and you just need to make sure you don't trash the current view's transform matrix. Use the CGAffineTransformScale method and related methods that take a transform as input, rather than creating it from scratch (unless you maintain the current rotation, scale, or translation yourself.

Download Xcode Project

  • Sample UIPinchGesture project on Github.

Note: iOS 7 behaves weird with UIView's in IB that have Pan/Pinch/Rotate gestures applied. iOS 8 fixes it, but my workaround is to add all views in code like this code example.

Demo Video

Can UIPinchGestureRecognizer and UIPanGestureRecognizer Be Merged?

  1. Add them to a view and conform to the UIGestureRecognizerDelegate protocol

    @interface ViewController () <UIGestureRecognizerDelegate>
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 150, 150)];
        blueView.backgroundColor = [UIColor blueColor];
        [self.view addSubview:blueView];
        [self addMovementGesturesToView:blueView];
    
        // UIImageView's and UILabel's don't have userInteractionEnabled by default!
        UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"BombDodge.png"]]; // Any image in Xcode project
        imageView.center = CGPointMake(100, 250);
        [imageView sizeToFit];
        [self.view addSubview:imageView];
        [self addMovementGesturesToView:imageView];
    
        // Note: Changing the font size would be crisper than zooming a font!
        UILabel *label = [[UILabel alloc] init];
        label.text = @"Hello Gestures!";
        label.font = [UIFont systemFontOfSize:30];
        label.textColor = [UIColor blackColor];
        [label sizeToFit];
        label.center = CGPointMake(100, 400);
        [self.view addSubview:label];
        [self addMovementGesturesToView:label];
    }
    
    - (void)addMovementGesturesToView:(UIView *)view {
        view.userInteractionEnabled = YES;  // Enable user interaction
    
        UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
        panGesture.delegate = self;
        [view addGestureRecognizer:panGesture];
    
        UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
        pinchGesture.delegate = self;
        [view addGestureRecognizer:pinchGesture];
    }
    
  2. Implement gesture methods

    - (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
        CGPoint translation = [panGesture translationInView:panGesture.view.superview];
    
        if (UIGestureRecognizerStateBegan == panGesture.state ||UIGestureRecognizerStateChanged == panGesture.state) {
            panGesture.view.center = CGPointMake(panGesture.view.center.x + translation.x,
                                                 panGesture.view.center.y + translation.y);
            // Reset translation, so we can get translation delta's (i.e. change in translation)
            [panGesture setTranslation:CGPointZero inView:self.view];
        }
        // Don't need any logic for ended/failed/canceled states
    }
    
    - (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinchGesture {
    
        if (UIGestureRecognizerStateBegan == pinchGesture.state ||
            UIGestureRecognizerStateChanged == pinchGesture.state) {
    
            // Use the x or y scale, they should be the same for typical zooming (non-skewing)
            float currentScale = [[pinchGesture.view.layer valueForKeyPath:@"transform.scale.x"] floatValue];
    
            // Variables to adjust the max/min values of zoom
            float minScale = 1.0;
            float maxScale = 2.0;
            float zoomSpeed = .5;
    
            float deltaScale = pinchGesture.scale;
    
            // You need to translate the zoom to 0 (origin) so that you
            // can multiply a speed factor and then translate back to "zoomSpace" around 1
            deltaScale = ((deltaScale - 1) * zoomSpeed) + 1;
    
            // Limit to min/max size (i.e maxScale = 2, current scale = 2, 2/2 = 1.0)
            //  A deltaScale is ~0.99 for decreasing or ~1.01 for increasing
            //  A deltaScale of 1.0 will maintain the zoom size
            deltaScale = MIN(deltaScale, maxScale / currentScale);
            deltaScale = MAX(deltaScale, minScale / currentScale);
    
            CGAffineTransform zoomTransform = CGAffineTransformScale(pinchGesture.view.transform, deltaScale, deltaScale);
            pinchGesture.view.transform = zoomTransform;
    
            // Reset to 1 for scale delta's
            //  Note: not 0, or we won't see a size: 0 * width = 0
            pinchGesture.scale = 1;
        }
    }
    
    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
        return YES; // Works for most use cases of pinch + zoom + pan
    }
    

Resources

  • Xcode Gesture Sample Project
  • Apple's Gestures Guide


If anyone is interested in a Swift implementation of this using Metal to do the rendering, I have a project available here.


Swift

Many thanks a lot to Paul!!! Here is his Swift version:

import UIKit

class ViewController: UIViewController, UIGestureRecognizerDelegate {

    var editorView: EditorView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let blueView = UIView(frame: .init(x: 100, y: 100, width: 300, height: 300))
        view.addSubview(blueView)
        blueView.backgroundColor = .blue
        addMovementGesturesToView(blueView)
    }

    func addMovementGesturesToView(_ view: UIView) {
        view.isUserInteractionEnabled = true

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        panGesture.delegate = self
        view.addGestureRecognizer(panGesture)

        let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
        pinchGesture.delegate = self
        view.addGestureRecognizer(pinchGesture)
    }

    @objc private func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
        guard let panView = panGesture.view else { return }

        let translation = panGesture.translation(in: panView.superview)

        if panGesture.state == .began || panGesture.state == .changed {
            panGesture.view?.center = CGPoint(x: panView.center.x + translation.x, y: panView.center.y + translation.y)

            // Reset translation, so we can get translation delta's (i.e. change in translation)
            panGesture.setTranslation(.zero, in: self.view)
        }
        // Don't need any logic for ended/failed/canceled states
    }

    @objc private func handlePinchGesture(_ pinchGesture: UIPinchGestureRecognizer) {
        guard let pinchView = pinchGesture.view else { return }

        if pinchGesture.state == .began || pinchGesture.state == .changed {
            let currentScale = scale(for: pinchView.transform)

            // Variables to adjust the max/min values of zoom
            let minScale: CGFloat = 0.2
            let maxScale: CGFloat = 3
            let zoomSpeed: CGFloat = 0.8

            var deltaScale = pinchGesture.scale

            // You need to translate the zoom to 0 (origin) so that you
            // can multiply a speed factor and then translate back to "zoomSpace" around 1
            deltaScale = ((deltaScale - 1) * zoomSpeed) + 1

            // Limit to min/max size (i.e maxScale = 2, current scale = 2, 2/2 = 1.0)
            //  A deltaScale is ~0.99 for decreasing or ~1.01 for increasing
            //  A deltaScale of 1.0 will maintain the zoom size
            deltaScale = min(deltaScale, maxScale / currentScale)
            deltaScale = max(deltaScale, minScale / currentScale)

            let zoomTransform = pinchView.transform.scaledBy(x: deltaScale, y: deltaScale)
            pinchView.transform = zoomTransform

            // Reset to 1 for scale delta's
            //  Note: not 0, or we won't see a size: 0 * width = 0
            pinchGesture.scale = 1
        }
    }

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

    private func scale(for transform: CGAffineTransform) -> CGFloat {
        return sqrt(CGFloat(transform.a * transform.a + transform.c * transform.c))
    }
}

Demo (on Simulator):

Can UIPinchGestureRecognizer and UIPanGestureRecognizer Be Merged?

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜