UIPinchGestureRecognizer 和 UIPanGestureRecognizer 可以合并吗?

发布于 2024-10-16 14:16:43 字数 228 浏览 8 评论 0原文

我正在努力弄清楚是否可以创建一个将 UIPinchGestureRecognizer 与 UIPanGestureRecognizer 结合起来的单个组合手势识别器。

我使用平移进行视图平移,使用捏合进行视图缩放。我正在进行增量矩阵串联,以导出应用于视图的最终转换矩阵。这个矩阵既有尺度又有平移。使用单独的手势识别器会导致抖动/缩放。不是我想要的。因此,我想在一个手势中处理一次缩放和平移的串联。有人可以阐明如何做到这一点吗?

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 matrix 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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

°如果伤别离去 2024-10-23 14:16:43

2014 年 6 月 14 日:使用 ARC 更新了 iOS 7+ 的示例代码。

UIGestureRecognizer 可以一起工作,您只需要确保不会破坏当前视图的变换矩阵。使用 CGAffineTransformScale 方法和相关方法将变换作为输入,而不是从头开始创建它(除非您自己维护当前的旋转、缩放或平移。

下载 Xcode 项目

注意:对于应用了平移/捏合/旋转手势的 IB 中的 UIView,iOS 7 的行为很奇怪。iOS 8 修复了这个问题,但我的解决方法是在代码中添加所有视图,如此代码示例

Demo UIPinchGesture Video

  1. 将它们添加到视图中并符合 UIGestureRecognizerDelegate 协议

    @interfaceViewController() ;
    
    @结尾
    
    @实现ViewController
    
    - (void)viewDidLoad
    {
        [超级viewDidLoad];
    
        UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 150, 150)];
        blueView.backgroundColor = [UIColor 蓝色];
        [self.view addSubview:blueView];
        [self addMovementGesturesToView:blueView];
    
        // UIImageView 和 UILabel 默认没有 userInteractionEnabled!
        UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"BombDodge.png"]]; // Xcode 项目中的任意图像
        imageView.center = CGPointMake(100, 250);
        [图像视图大小适合];
        [self.view addSubview:imageView];
        [自我addMovementGesturesToView:imageView];
    
        // 注意:更改字体大小比缩放字体更清晰!
        UILabel *label = [[UILabel alloc] init];
        label.text = @"你好手势!";
        label.font = [UIFont systemFontOfSize:30];
        label.textColor = [UIColor blackColor];
        [标签大小适合];
        标签.center = CGPointMake(100, 400);
        [self.view addSubview:标签];
        [自我addMovementGesturesToView:标签];
    }
    
    - (void)addMovementGesturesToView:(UIView *)view {
        view.userInteractionEnabled = YES; // 启用用户交互
    
        UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
        panGesture.delegate = self;
        [查看addGestureRecognizer:panGesture];
    
        UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
        inchGesture.delegate = self;
        [查看addGestureRecognizer:pinchGesture];
    }
    
  2. 实现手势方法

    - (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
        CGPoint 翻译 = [panGesture TranslationInView:panGesture.view.superview];
    
        if (UIGestureRecognizerStateBegan == panGesture.state ||UIGestureRecognizerStateChanged == panGesture.state) {
            panGesture.view.center = CGPointMake(panGesture.view.center.x + 翻译.x,
                                                 panGesture.view.center.y + 翻译.y);
            // 重置翻译,这样我们就可以获得翻译增量(即翻译的变化)
            [panGesture setTranslation:CGPointZero inView:self.view];
        }
        // 不需要任何结束/失败/取消状态的逻辑
    }
    
    - (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinchGesture {
    
        if (UIGestureRecognizerStateBegan ==捏Gesture.state ||
            UIGestureRecognizerStateChanged ==捏Gesture.state) {
    
            // 使用 x 或 y 比例,它们对于典型缩放应该是相同的(非倾斜)
            float currentScale = [[pinchGesture.view.layer valueForKeyPath:@"transform.scale.x"] floatValue];
    
            // 调整缩放最大/最小值的变量
            浮动最小比例 = 1.0;
            浮动最大比例 = 2.0;
            浮动缩放速度= .5;
    
            浮动 deltaScale =inchGesture.scale;
    
            // 您需要将缩放平移到 0(原点),以便您
            // 可以乘以速度系数,然后转换回 1 左右的“zoomSpace”
            deltaScale = ((deltaScale - 1) * 缩放速度) + 1;
    
            // 限制最小/最大尺寸(即 maxScale = 2,当前比例 = 2, 2/2 = 1.0)
            // deltaScale 为 ~0.99(递减)或 ~1.01(递增)
            // deltaScale 为 1.0 将保持缩放大小
            deltaScale = MIN(deltaScale, maxScale / currentScale);
            deltaScale = MAX(deltaScale, minScale / currentScale);
    
            CGAffineTransform ZoomTransform = CGAffineTransformScale(pinchGesture.view.transform, deltaScale, deltaScale);
            inchGesture.view.transform = ZoomTransform;
    
            // 比例增量重置为 1
            // 注意:不是0,否则我们不会看到尺寸:0 * width = 0
            inchGesture.scale = 1;
        }
    }
    
    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
        返回是; // 适用于大多数捏合 + 缩放 + 平移的用例
    }
    

资源

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

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

Demo UIPinchGesture Video

  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

久伴你 2024-10-23 14:16:43

如果有人对使用 Metal 进行渲染的 Swift 实现感兴趣,我有一个项目这里

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

嗫嚅 2024-10-23 14:16:43

斯威夫特

非常感谢保罗!这是他的 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))
    }
}

演示(在模拟器上):

在此处输入图像描述

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

enter image description here

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文