Можно ли объединить UIPinchGestureRecognizer и UIPanGestureRecognizer?

Я немного пытаюсь выяснить, возможно ли создать единый комбинированный распознаватель жестов, который сочетает в себе UIPinchGestureRecognizer с UIPanGestureRecognizer.

Я использую панорамирование для перевода просмотра и масштабирование просмотра. Я делаю инкрементную конкатенацию матриц, чтобы получить результирующую матрицу окончательного преобразования, которая применяется к представлению. Эта матрица имеет как масштаб, так и перевод. Использование отдельных распознавателей жестов приводит к нестабильному движению/масштабированию. Не то, что я хочу. Таким образом, я хочу обрабатывать конкатенацию масштаба и перевода один раз в одном жесте. Может кто-нибудь, пожалуйста, пролить свет на то, как это сделать?


person dugla    schedule 08.02.2011    source источник
comment
Дуг, я обновил свой ответ полным исходным кодом и примером проекта.   -  person Paul Solt    schedule 15.06.2014


Ответы (3)


14 июня 2014 г.: обновлен пример кода для iOS 7+ с ARC.

UIGestureRecognizers могут работать вместе, и вам просто нужно убедиться, что вы не испортите матрицу преобразования текущего представления. Используйте метод CGAffineTransformScale и связанные с ним методы, которые принимают преобразование в качестве входных данных, а не создают его с нуля (если вы не поддерживаете текущий поворот, масштаб или перемещение самостоятельно).

Скачать проект Xcode

Примечание. iOS 7 ведет себя странно с UIView в IB, к которым применены жесты Pan/Pinch/Rotate. iOS 8 исправляет это, но мой обходной путь — добавить все представления в код, как в этом примере кода.

Демонстрационное видео

Демонстрационное видео UIPinchGesture

  1. Добавьте их в представление и соблюдайте протокол UIGestureRecognizerDelegate.

    @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. Реализовать методы жестов

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

Ресурсы

person Paul Solt    schedule 27.03.2011
comment
Это не работает, если одновременное распознавание жестов не разрешено с помощью методаgestRecognizer:shouldRecognizeSimultaneousWithGestureRecognizer: UIGestureRecognizerDelegate. - person borisgolovnev; 23.09.2012
comment
в handlePanGesture: состояние дважды сравнивается с UIGestureRecognizerStateChanged - person Klaas; 11.02.2013
comment
многие жесты будут срабатывать несколько раз в UIGestureRecognizerStateChanged. Прочитайте руководства по событиям для iOS: developer.apple.com/library/ios/#documentation/EventHandling/ - person Paul Solt; 14.02.2013
comment
ссылка на класс также содержит много полезной информации: developer.apple.com/library/ios/#documentation/uikit/reference/ - person Paul Solt; 14.02.2013
comment
Я думаю, вы неправильно поняли комментарий Клааса, в операторе if состояние дважды сравнивалось с UIGestureRecognizerStateChanged. Я отредактировал ваш ответ, чтобы удалить одно из сравнений. - person Aurelien Porte; 04.01.2014

Если кого-то интересует реализация Swift с использованием Metal для рендеринга, у меня есть проект, доступный здесь.

person Tyler White    schedule 11.03.2020
comment
Привет, Тайлер. У него нет функции поворота пальцами. - person MRizwan33; 15.12.2020

Быстрый

Большое спасибо Павлу!!! Вот его версия Swift:

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

Демо (на симуляторе):

введите здесь описание изображения

person Tung Fam    schedule 24.10.2020