UIPanGestureRecognizer - 只允许垂直或水平移动

157
我有一个视图,带有一个 UIPanGestureRecognizer 以垂直方向拖动视图。因此,在手势识别回调中,我只更新 y 坐标以移动它。这个视图的 superview 有一个 UIPanGestureRecognizer ,将视图水平拖动,只更新 x 坐标。
问题是第一个 UIPanGestureRecognizer 正在接收事件以使视图垂直移动,所以我不能使用 superview 的手势。
我已经尝试过:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
 shouldRecognizeSimultaneouslyWithGestureRecognizer:
                            (UIGestureRecognizer *)otherGestureRecognizer;

两种方法都可以使用,但我不想这么做。我只希望在移动明显是水平方向时才检测到水平方向。因此,如果UIPanGestureRecognizer有一个方向属性就太好了。

如何实现这种行为?我觉得文档写得很混乱,所以也许有人可以在这里更好地解释一下。


如果你已经找到了解决方案,回答自己的问题并接受答案是可以的。 - jtbandes
@JoeBlow 真的吗?那么,也许你创建了滑动手势类别来接收翻译和手势速度? - Roman Truba
2
我不明白你在说什么。如果你想要检测水平滑动,这完全是内置于操作系统中的。所有的工作都已经为你完成了。你什么也不需要做! :) 只需将此示例中的两行代码粘贴即可.. http://stackoverflow.com/a/20988648/294884 请注意,您可以选择“仅左”、“仅右”或“两者皆可”。 - Fattie
22个回答

227

对于垂直拖动手势识别器,只需按照以下步骤进行即可,这在我的情况下有效:

- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)panGestureRecognizer {
    CGPoint velocity = [panGestureRecognizer velocityInView:someView];
    return fabs(velocity.y) > fabs(velocity.x);
}

对于Swift语言:

func gestureRecognizerShouldBegin(_ gestureRecognizer: UIPanGestureRecognizer) -> Bool {
    let velocity = gestureRecognizer.velocity(in: someView)
    return abs(velocity.y) > abs(velocity.x)
}

3
尝试过这个,但翻译经常是 == (0,0),所以不够精确。 - zxcat
12
当使用velocityInView:而不是translationInView:时,(0,0)问题并不明显。 - cbh2000
1
@cbh2000 我更新了答案,使用velocityInView替代了translationInView - Hejazi
20
UISwipeGestureRecognizer是响应滑动手势触发转场的简便方式,但它是一个离散的手势。如果有人寻求连续的方法来使用手势进行转场动画,UIPanGestureRecognizer就是最好的选择。 - stillmotion
1
这是一个聪明的解决方案。 - Jakub Truhlář
1
绝妙的解决方案 :-) - sabiland

89

我使用了子类化的解决方案,就像@LocoMike提供的答案一样,但使用了更有效的初始速度检测机制,就像@Hejazi提供的那样。我也在使用Swift,但如果需要的话,这应该很容易翻译成Obj-C。

与其他解决方案相比的优点:

  • 比其他子类化解决方案更简单、更简洁。无需管理额外的状态。
  • 方向检测发生在发送 Began 动作之前,因此如果滑动的方向错误,则您的平移手势选择器不会收到任何消息。
  • 确定初始方向后,将不再考虑方向逻辑。这导致了通常期望的行为:如果初始方向正确,则激活您的识别器,但如果用户的手指沿着方向没有完美地滑动,则不会在手势开始后取消手势。

这是代码:

import UIKit.UIGestureRecognizerSubclass

enum PanDirection {
    case vertical
    case horizontal
}

class PanDirectionGestureRecognizer: UIPanGestureRecognizer {

    let direction: PanDirection

    init(direction: PanDirection, target: AnyObject, action: Selector) {
        self.direction = direction
        super.init(target: target, action: action)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesMoved(touches, with: event)

        if state == .began {
            let vel = velocity(in: view)
            switch direction {
            case .horizontal where fabs(vel.y) > fabs(vel.x):
                state = .cancelled
            case .vertical where fabs(vel.x) > fabs(vel.y):
                state = .cancelled
            default:
                break
            }
        }
    }
}

使用示例:

let panGestureRecognizer = PanDirectionGestureRecognizer(direction: .horizontal, target: self, action: #selector(handlePanGesture(_:)))
panGestureRecognizer.cancelsTouchesInView = false
self.view.addGestureRecognizer(panGestureRecognizer)

func handlePanGesture(_ pan: UIPanGestureRecognizer) {
    let percent = max(pan.translation(in: view).x, 0) / view.frame.width

    switch pan.state {
    case .began:
    ...
}

6
这绝对是最好的答案。很遗憾,苹果公司还没有像这样为 UIPanGestureRecognizer 添加功能。 - NRitH
你能提供一个使用示例吗? - user82395214
太好了!谢谢!在水平和垂直堆叠时都能完美运行:`let horizontalPanRecognizer = PanDirectionGestureRecognizer(direction: .horizontal, target: self, action: #selector(handleHorizontalPanGesture(recognizer:))) self.view?.addGestureRecognizer(horizontalPanRecognizer); let verticalPanRecognizer = PanDirectionGestureRecognizer(direction: .vertical, target: self, action: #selector(handleVerticalPanGesture(recognizer:))) self.view?.addGestureRecognizer(verticalPanRecognizer);` - Han
哦,这太棒了!谢谢! - Baran Emre

51

我找到了解决方法,创建了UIPanGestureRecognizer的一个子类。

DirectionPanGestureRecognizer:

#import <Foundation/Foundation.h>
#import <UIKit/UIGestureRecognizerSubclass.h>

typedef enum {
    DirectionPangestureRecognizerVertical,
    DirectionPanGestureRecognizerHorizontal
} DirectionPangestureRecognizerDirection;

@interface DirectionPanGestureRecognizer : UIPanGestureRecognizer {
    BOOL _drag;
    int _moveX;
    int _moveY;
    DirectionPangestureRecognizerDirection _direction;
}

@property (nonatomic, assign) DirectionPangestureRecognizerDirection direction;

@end

DirectionPanGestureRecognizer.m:

#import "DirectionPanGestureRecognizer.h"

int const static kDirectionPanThreshold = 5;

@implementation DirectionPanGestureRecognizer

@synthesize direction = _direction;

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    if (self.state == UIGestureRecognizerStateFailed) return;
    CGPoint nowPoint = [[touches anyObject] locationInView:self.view];
    CGPoint prevPoint = [[touches anyObject] previousLocationInView:self.view];
    _moveX += prevPoint.x - nowPoint.x;
    _moveY += prevPoint.y - nowPoint.y;
    if (!_drag) {
        if (abs(_moveX) > kDirectionPanThreshold) {
            if (_direction == DirectionPangestureRecognizerVertical) {
                self.state = UIGestureRecognizerStateFailed;
            }else {
                _drag = YES;
            }
        }else if (abs(_moveY) > kDirectionPanThreshold) {
            if (_direction == DirectionPanGestureRecognizerHorizontal) {
                self.state = UIGestureRecognizerStateFailed;
            }else {
                _drag = YES;
            }
        }
    }
}

- (void)reset {
    [super reset];
    _drag = NO;
    _moveX = 0;
    _moveY = 0;
}

@end

只有在用户开始使用所选的行为进行拖动时,才会触发此手势。将方向属性设置为正确的值,然后您就完成了。


我认为“reset”最初没有被调用。添加了一个initWithTarget:action:方法并调用了reset,一切都很好。 - colinta
5
在当前的实现中,DirectionPanGestureRecognizer会忽略快速拖动事件,除非你设置kDirectionPanThreshold = 20或类似的值,这种情况下可能会产生误报。我建议使用abs(_moveX) > abs(_moveY)代替abs(_moveX) > kDirectionPanThreshold,并相应地更改水平方向的情况。 - Dennis Krut
2
我应该补充一下,这对我也很有帮助,但是为了让平移手势识别器触发,我必须在if语句的else部分,在_drag = YES行下面添加self.state = UIGestureRecognizerStateChanged; - bolnad

13

我尝试使用UIPanGestureRecognizer在水平方向上限制有效区域。

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {

        UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer;
        CGPoint velocity = [panGesture velocityInView:panGesture.view];

        double radian = atan(velocity.y/velocity.x);
        double degree = radian * 180 / M_PI;

        double thresholdAngle = 20.0;
        if (fabs(degree) > thresholdAngle) {
            return NO;
        }
    }
    return YES;
}

然后,只有在水平方向上滑动小于thresholdAngle度才能触发此拖拽手势。


2
很棒的答案。当我混合UIScrollView手势和常规手势时,这确实帮了我很多。我认为示例应该是指"thresholdAngle"而不是"enableThreshold"。而且你应该很少使用 atan(),因为它可能会产生一个NAN。改用 atan2()。 - Brainware

9

Swift 3.0的答案:只需处理垂直手势即可。

    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    if let pan = gestureRecognizer as? UIPanGestureRecognizer {
        let velocity = pan.velocity(in: self)
        return fabs(velocity.y) > fabs(velocity.x)
    }
    return true

}

6
以下解决方案解决了我的问题:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if ([gestureRecognizer.view isEqual:self.view] && [otherGestureRecognizer.view isEqual:self.tableView]) {
        return NO;
    }
    return YES;
}

实际上只是检查pan是否在主视图或表格视图上进行。


3
为什么要使用-isEqual:方法比较两个视图是否相同?简单的身份验证应该就足够了。gestureRecognizer.view == self.view。 - openfrog

6

懒人版的 Lee 答案的 Swift 3 版本

import UIKit
import UIKit.UIGestureRecognizerSubclass

enum PanDirection {
    case vertical
    case horizontal
}

class UIPanDirectionGestureRecognizer: UIPanGestureRecognizer {

    let direction : PanDirection

    init(direction: PanDirection, target: AnyObject, action: Selector) {
        self.direction = direction
        super.init(target: target, action: action)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesMoved(touches, with: event)

        if state == .began {

            let vel = velocity(in: self.view!)
            switch direction {
            case .horizontal where fabs(vel.y) > fabs(vel.x):
                state = .cancelled
            case .vertical where fabs(vel.x) > fabs(vel.y):
                state = .cancelled
            default:
                break
            }
        }
    }
}

5

这里是用Swift 5编写的自定义平移手势。

您可以限制它的方向和方向上的最大角度,还可以限制它在方向上的最小速度。

enum PanDirection {
    case vertical
    case horizontal
}

struct Constaint {
    let maxAngle: Double
    let minSpeed: CGFloat

    static let `default` = Constaint(maxAngle: 50, minSpeed: 50)
}


class PanDirectionGestureRecognizer: UIPanGestureRecognizer {

    let direction: PanDirection

    let constraint: Constaint


    init(direction orientation: PanDirection, target: AnyObject, action: Selector, constraint limits: Constaint = Constaint.default) {
        direction = orientation
        constraint = limits
        super.init(target: target, action: action)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesMoved(touches, with: event)
        let tangent = tan(constraint.maxAngle * Double.pi / 180)
        if state == .began {
            let vel = velocity(in: view)
            switch direction {
            case .horizontal where abs(vel.y)/abs(vel.x) > CGFloat(tangent) || abs(vel.x) < constraint.minSpeed:
                state = .cancelled
            case .vertical where abs(vel.x)/abs(vel.y) > CGFloat(tangent) || abs(vel.y) < constraint.minSpeed:
                state = .cancelled
            default:
                break
            }
        }
    }
}

这样调用:

    let pan = PanDirectionGestureRecognizer(direction: .vertical, target: self, action: #selector(self.push(_:)))
    view.addGestureRecognizer(pan)

    @objc func push(_ gesture: UIPanGestureRecognizer){
        if gesture.state == .began{
            // command for once
        }
    }

或者
    let pan = PanDirectionGestureRecognizer(direction: .horizontal, target: self, action: #selector(self.push(_:)), constraint: Constaint(maxAngle: 5, minSpeed: 80))
    view.addGestureRecognizer(pan)

完美答案。谢谢。 - Hardik Thakkar

4
我采用了Lee Goodrich答案并进行了扩展,因为我需要一个特定的单向平移。使用方法如下:let pan = PanDirectionGestureRecognizer(direction: .vertical(.up), target: self, action: #selector(handleCellPan(_:))) 我还添加了一些注释,以使实际做出的决策更清晰。
import UIKit.UIGestureRecognizerSubclass

enum PanVerticalDirection {
    case either
    case up
    case down
}

enum PanHorizontalDirection {
    case either
    case left
    case right
}

enum PanDirection {
    case vertical(PanVerticalDirection)
    case horizontal(PanHorizontalDirection)
}

class PanDirectionGestureRecognizer: UIPanGestureRecognizer {

    let direction: PanDirection

    init(direction: PanDirection, target: AnyObject, action: Selector) {
        self.direction = direction
        super.init(target: target, action: action)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesMoved(touches, with: event)

        if state == .began {
            let vel = velocity(in: view)
            switch direction {

            // expecting horizontal but moving vertical, cancel
            case .horizontal(_) where fabs(vel.y) > fabs(vel.x):
                state = .cancelled

            // expecting vertical but moving horizontal, cancel
            case .vertical(_) where fabs(vel.x) > fabs(vel.y):
                state = .cancelled

            // expecting horizontal and moving horizontal
            case .horizontal(let hDirection):
                switch hDirection {

                    // expecting left but moving right, cancel
                    case .left where vel.x > 0: state = .cancelled

                    // expecting right but moving left, cancel
                    case .right where vel.x < 0: state = .cancelled
                    default: break
                }

            // expecting vertical and moving vertical
            case .vertical(let vDirection):
                switch vDirection {
                    // expecting up but moving down, cancel
                    case .up where vel.y > 0: state = .cancelled

                    // expecting down but moving up, cancel
                    case .down where vel.y < 0: state = .cancelled
                    default: break
                }
            }
        }
    }
}

override func touchesMoved 中出现错误 - 该方法没有覆盖其超类中的任何方法. - Anjan Biswas
@Annjawn 你必须使用 "import UIKit.UIGestureRecognizerSubclass"。 - shawnynicole
好的。我不知道那个。我以为导入UIKit会自动导入它。我会试一下。 - Anjan Biswas

3

Swift 4.2

这个解决方案只支持在垂直方向上的平移手势,与水平方向相同。

let pan = UIPanGestureRecognizer(target: self, action: #selector(test1))
pan.cancelsTouchesInView = false
panView.addGestureRecognizer(pan)

解决方案1:

@objc func panAction(pan: UIPanGestureRecognizer) {

        let velocity = pan.velocity(in: panView)
        guard abs(velocity.y) > abs(velocity.x) else {
            return
        }
}

解决方案2:

  [UISwipeGestureRecognizer.Direction.left, .right].forEach { direction in
        let swipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeAction))
        swipe.direction = direction
        panView.addGestureRecognizer(swipe)
        pan.require(toFail: swipe)
    }

然后滑动手势会覆盖平移手势。当然,在 swipeAction 中你不需要做任何事情。

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