UIPanGestureRecognizer: solo vertical u horizontal

Tengo una vista que tiene un UIPanGestureRecognizer para arrastrar la vista verticalmente. Entonces, en la callback del reconocedor, solo actualizo la coordenada y para moverla. La supervista de esta vista tiene un UIPanGestureRecognizer que arrastrará la vista horizontalmente, simplemente actualizando la coordenada x.

El problema es que el primer UIPanGestureRecognizer está tomando el evento para mover la vista verticalmente, por lo que no puedo usar el gesto de la supervista.

Yo he tratado

 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer *)otherGestureRecognizer; 

y ambos funcionarán, pero no quiero eso. Quiero que se detecte horizontalmente solo si el movimiento es claramente horizontal. Entonces sería genial si el UIPanGestureRecognizer tuviera una propiedad de dirección.

¿Cómo puedo lograr este comportamiento? Encuentro los documentos muy confusos, así que quizás alguien pueda explicarlo mejor aquí.

Solo haga esto para el reconocedor de pan vertical, funciona para mí:

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

Creé una solución con subclases como en la respuesta @LocoMike proporcionada, pero usé el mecanismo de detección más efectivo a través de la velocidad inicial proporcionada por @Hejazi. También estoy usando Swift, pero esto debería ser fácil de traducir a Obj-C si lo desea.

Ventajas sobre otras soluciones:

  • Más simple y más conciso que otras soluciones de subclases. Sin estado adicional para administrar.
  • La detección de dirección ocurre antes de enviar la acción de inicio, por lo que su selector de movimiento de pan no recibe mensajes si se desliza la dirección incorrecta.
  • Después de determinar la dirección inicial, ya no se consulta la lógica de dirección. Esto da como resultado el comportamiento generalmente deseado de activar su reconocedor si la dirección inicial es correcta, pero no cancela el gesto una vez que ha comenzado, si el dedo de un usuario no viaja perfectamente en la dirección.

Aquí está el código:

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

Lo descubrí creando una subclase de UIPanGestureRecognizer

DirectionPanGestureRecognizer:

 #import  #import  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 

Esto solo activará el gesto si el usuario comienza a arrastrar el comportamiento seleccionado. Establezca la propiedad de dirección en un valor correcto y todo estará configurado.

Traté de restringir el área válida horizontalmente con 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) > enableThreshold) { return NO; } } return YES; } 

Luego, solo deslizar dentro del umbral horizontalmente puede disparar este gesto de panorámica.

Respuesta de Swift 3.0: solo maneja el gesto vertical

  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 } 

La siguiente solución resolvió mi problema:

 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { if ([gestureRecognizer.view isEqual:self.view] && [otherGestureRecognizer.view isEqual:self.tableView]) { return NO; } return YES; } 

En realidad, es solo comprobar si pan está en la vista principal o tableView.

Swift 3 versión de la respuesta de Lee para los perezosos

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

Puede encontrar la dirección arrastrando en UIView través de UIPanGestureRecognizer . Por favor sigue el código.

  - (void)viewDidLoad { [super viewDidLoad]; flipFoward = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(doFlipForward:)]; [flipFoward setMaximumNumberOfTouches:1]; [flipFoward setMinimumNumberOfTouches:1]; [flipFoward setDelegate:self]; [self.view addGestureRecognizer:flipFoward]; flipBack = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(doFlipBack:)]; [flipBack setMaximumNumberOfTouches:1]; [flipBack setMinimumNumberOfTouches:1]; [flipBack setDelegate:self]; [self.view addGestureRecognizer:flipBack]; } #pragma mark - #pragma mark RESPONDER -(void)doFlipForward:(UIGestureRecognizer *)aGestureRecognizer{ NSLog(@"doFlipForward"); if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateBegan) { NSLog(@"UIGestureRecognizerStateBegan"); } if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateChanged) { NSLog(@"UIGestureRecognizerStateChanged"); } if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateEnded) { NSLog(@"UIGestureRecognizerStateEnded"); } } -(void)doFlipBack:(UIGestureRecognizer *)aGestureRecognizer{ NSLog(@"doFlipBack"); if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateBegan) { NSLog(@"UIGestureRecognizerStateBegan1"); } if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateChanged) { NSLog(@"UIGestureRecognizerStateChanged1"); } if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateEnded) { NSLog(@"UIGestureRecognizerStateEnded1"); } } #pragma mark - #pragma mark DELEGATE -(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ CGSize size = [self.view bounds].size; CGFloat touchX = [gestureRecognizer locationInView:self.view].x; if((gestureRecognizer == flipFoward) && touchX >= (size.width - 88.0f)) { return YES; } if((gestureRecognizer == flipBack) && touchX <= 88.0f) { return YES; } return NO; } 

Puede usar panGestureRecognizer simple. No es necesario utilizar el pandirectionregognizer o cosas. Solo use el valor y de la translationInview Vista A continuación, el código mueve la vista de arrastre solo hacia arriba y hacia abajo

 - (void)gesturePan_Handle:(UIPanGestureRecognizer *)gesture { if (gesture.state == UIGestureRecognizerStateChanged) { CGPoint translation = [gesture translationInView:gesture.view]; recognizer.view.center = CGPointMake(recognizer.view.center.x, recognizer.view.center.y + translation.y); [gesture setTranslation:CGPointMake(0, 0) inView:gesture.view]; } } 

Así es como resolví:

Primero habilité el Reconocimiento de PanGesture simultáneamente.

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

A continuación, aíslo los gestos Pan horizontal y vertical (el acumulador es propiedad NSMutableArray):

 - (void)verticalPan :(UIPanGestureRecognizer *) sender { CGPoint touch = [sender translationInView:self]; NSValue *value = [NSValue valueWithCGPoint:touch]; [accumulator addObject:value]; int firstXObjectValue = (int)[[accumulator objectAtIndex:0] CGPointValue].x ; int lastXObjectValue = (int)[[accumulator lastObject] CGPointValue].x; int firstYObjectValue = (int)[[accumulator objectAtIndex:0] CGPointValue].y; int lastYObjectValue = (int)[[accumulator lastObject] CGPointValue].y; if (abs(lastYObjectValue - firstYObjectValue) < 4 && abs(lastXObjectValue - firstXObjectValue) > 4) { NSLog(@"Horizontal Pan"); //do something here } else if (abs(lastYObjectValue - firstYObjectValue) > 4 && abs(lastXObjectValue - firstXObjectValue) < 4){ NSLog(@"Vertical Pan"); //do something here } if (accumulator.count > 3) [accumulator removeAllObjects]; 

Presioné un ejemplo aquí:

agregar pan personalizado en scrollview

 let pangesture = UIPanGestureRecognizer(target: self, action: "dragview:") yourview.addGestureRecognizer(pangesture) func dragview(panGestureRecognizer:UIPanGestureRecognizer) { let touchlocation = panGestureRecognizer.locationInView(parentview) yourview.center.y = touchlocation.y //x for horizontal } 
 - (void)dragAction:(UIPanGestureRecognizer *)gesture{ UILabel *label = (UILabel *)gesture.view; CGPoint translation = [gesture translationInView:label]; label.center = CGPointMake(label.center.x + translation.x, label.center.y + 0); [gesture setTranslation:CGPointZero inView:label];} 

Creé el método de acción PanGestureRecognizer @selector para el objeto que solo necesitaba desplazamiento horizontal.

  UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(smileyDragged:)]; [buttonObject addGestureRecognizer:gesture]; 

Tomé la respuesta de Lee Goodrich y la extendí ya que necesitaba específicamente una bandeja de una sola dirección. Úselo así: let pan = PanDirectionGestureRecognizer(direction: .vertical(.up), target: self, action: #selector(handleCellPan(_:)))

También agregué algunos comentarios para aclarar un poco qué decisiones se están tomando en realidad.

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

Manera rápida

 override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer { return isVerticalGesture(panGestureRecognizer) } return false } private func isVerticalGesture(_ recognizer: UIPanGestureRecognizer) -> Bool { let translation = recognizer.translation(in: superview!) if fabs(translation.y) > fabs(translation.x) { return true } return false } 

Para todos los usuarios de Swift, esto hará el trabajo 🙂

 import Foundation import UIKit.UIGestureRecognizerSubclass class DirectionPanGestureRecognizer: UIPanGestureRecognizer { let kDirectionPanThreshold = CGFloat(5) var drag = true var moveX = CGFloat(0) var moveY = CGFloat(0) override init(target: AnyObject, action: Selector) { super.init(target: target, action: action) } override func touchesMoved(touches: NSSet, withEvent event: UIEvent) { super.touchesMoved(touches, withEvent: event) if state == .Failed { return } let nowPoint = touches.anyObject()?.locationInView(view) let prevPoint = touches.anyObject()?.previousLocationInView(view) moveX += prevPoint!.x - nowPoint!.x moveY += prevPoint!.y - nowPoint!.y if !drag { if abs(moveX) > kDirectionPanThreshold { state = .Failed } else { drag = true } } } override func reset() { super.reset() moveX = 0 moveY = 0 drag = false } } 

Recibí una excelente respuesta de Lee Goodrich y la transferí a Swift 3

 import UIKit 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, 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 } } } } 

Me encantaría compartir mi enfoque porque todos los demás enfoques se basan en UIGestureRecognizerDelegate o en la subclase de UIPanGestureRecognizer .

Mi enfoque se basa en el tiempo de ejecución y el swizzling. No estoy 100% seguro acerca de este enfoque, pero puede probarlo y mejorarlo usted mismo.

Establezca la dirección de cualquier UIPanGestureRecognizer con solo una línea de código:

 UITableView().panGestureRecognizer.direction = UIPanGestureRecognizer.Direction.vertical 

utilice el pod 'UIPanGestureRecognizerDirection' o el código:

 public extension UIPanGestureRecognizer { override open class func initialize() { super.initialize() guard self === UIPanGestureRecognizer.self else { return } func replace(_ method: Selector, with anotherMethod: Selector, for clаss: AnyClass) { let original = class_getInstanceMethod(clаss, method) let swizzled = class_getInstanceMethod(clаss, anotherMethod) switch class_addMethod(clаss, method, method_getImplementation(swizzled), method_getTypeEncoding(swizzled)) { case true: class_replaceMethod(clаss, anotherMethod, method_getImplementation(original), method_getTypeEncoding(original)) case false: method_exchangeImplementations(original, swizzled) } } let selector1 = #selector(UIPanGestureRecognizer.touchesBegan(_:with:)) let selector2 = #selector(UIPanGestureRecognizer.swizzling_touchesBegan(_:with:)) replace(selector1, with: selector2, for: self) let selector3 = #selector(UIPanGestureRecognizer.touchesMoved(_:with:)) let selector4 = #selector(UIPanGestureRecognizer.swizzling_touchesMoved(_:with:)) replace(selector3, with: selector4, for: self) } @objc private func swizzling_touchesBegan(_ touches: Set, with event: UIEvent) { self.swizzling_touchesBegan(touches, with: event) guard direction != nil else { return } touchesBegan = true } @objc private func swizzling_touchesMoved(_ touches: Set, with event: UIEvent) { self.swizzling_touchesMoved(touches, with: event) guard let direction = direction, touchesBegan == true else { return } defer { touchesBegan = false } let forbiddenDirectionsCount = touches .flatMap({ ($0.location(in: $0.view) - $0.previousLocation(in: $0.view)).direction }) .filter({ $0 != direction }) .count if forbiddenDirectionsCount > 0 { state = .failed } } } public extension UIPanGestureRecognizer { public enum Direction: Int { case horizontal = 0 case vertical } private struct UIPanGestureRecognizerRuntimeKeys { static var directions = "\(#file)+\(#line)" static var touchesBegan = "\(#file)+\(#line)" } public var direction: UIPanGestureRecognizer.Direction? { get { let object = objc_getAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.directions) return object as? UIPanGestureRecognizer.Direction } set { let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC objc_setAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.directions, newValue, policy) } } fileprivate var touchesBegan: Bool { get { let object = objc_getAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.touchesBegan) return (object as? Bool) ?? false } set { let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC objc_setAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.touchesBegan, newValue, policy) } } } fileprivate extension CGPoint { var direction: UIPanGestureRecognizer.Direction? { guard self != .zero else { return nil } switch fabs(x) > fabs(y) { case true: return .horizontal case false: return .vertical } } static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint { return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) } } 

PanGestureRecognizer interfaz PanGestureRecognizer contiene las siguientes definiciones:

 unsigned int _canPanHorizontally:1; unsigned int _canPanVertically:1; 

No revisé esto, pero tal vez esté accesible a través de la subclase.