J'ai une vue qui a une UIPanGestureRecognizer
pour faire glisser la vue verticalement. Donc, dans le rappel du système de reconnaissance, je ne mets à jour que la coordonnée y pour le déplacer. La vue supérieure de cette vue a une UIPanGestureRecognizer
qui fera glisser la vue horizontalement, mettant juste à jour la coordonnée x.
Le problème est que la première UIPanGestureRecognizer
prend l'événement pour déplacer la vue verticalement, donc je ne peux pas utiliser le geste Superview.
J'ai essayé
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer *)otherGestureRecognizer;
et les deux vont travailler, mais je ne veux pas cela. Je veux que l'horizontale ne soit détectée que si le mouvement est clairement horizontal. Il serait donc bon que la variable UIPanGestureRecognizer
ait une propriété directionnelle.
Comment puis-je obtenir ce comportement? Je trouve la documentation très déroutante, alors peut-être que quelqu'un peut mieux l'expliquer ici.
Je l'ai compris en créant une sous-classe de 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
Cela ne déclenchera le geste que si l'utilisateur commence à faire glisser le comportement sélectionné. Définissez la propriété direction sur une valeur correcte et vous êtes tous définis.
Faites ceci pour la reconnaissance de geste panoramique vertical, cela fonctionne pour moi:
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)panGestureRecognizer {
CGPoint velocity = [panGestureRecognizer velocityInView:someView];
return fabs(velocity.y) > fabs(velocity.x);
}
J'ai créé une solution avec des sous-classes comme dans la réponse fournie par @LocoMike, mais j'ai utilisé le mécanisme de détection plus efficace via la vitesse initiale fourni par @Hejazi. J'utilise aussi Swift, mais cela devrait être facile à traduire en Obj-C si vous le souhaitez.
Avantages par rapport aux autres solutions:
Voici le code:
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
}
}
}
}
J'ai essayé de contraindre la zone valide horizontalement avec 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;
}
Ensuite, seul le fait de glisser dansseuilAngledegré horizontalement peut déclencher ce panoramique.
Réponse rapide de Swift 3.0: gère simplement le geste 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 solution suivante a résolu mon problème:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ([gestureRecognizer.view isEqual:self.view] && [otherGestureRecognizer.view isEqual:self.tableView]) {
return NO;
}
return YES;
}
En fait, il suffit de vérifier si le panoramique est actif dans la vue principale ou dans la table.
Swift 3 version de la réponse de Lee pour les paresseux
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
}
}
}
}
Vous pouvez utiliser un simple panGestureRecognizer
. Pas besoin d'utiliser pandirectionregognizer
ou d'autres choses. Utilisez simplement la valeur y de translationInview
- (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];
}
}
J'ai pris Lee Goodrich 's réponse et l'ai étendue car j'avais besoin spécifiquement d'un panoramique simple direction. Utilisez-le comme ceci: let pan = PanDirectionGestureRecognizer(direction: .vertical(.up), target: self, action: #selector(handleCellPan(_:)))
J'ai également ajouté des commentaires pour clarifier un peu les décisions qui sont prises.
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
}
}
}
}
}
Vous pouvez trouver la direction en glissant sur UIView
à UIPanGestureRecognizer
. S'il vous plaît suivez le code.
- (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;
}
Façon rapide
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
}
- (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];}
J'ai créé la méthode d'action PanGestureRecognizer @selector pour l'objet qui nécessitait uniquement le défilement horizontal.
UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(smileyDragged:)];
[buttonObject addGestureRecognizer:gesture];
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
}
Voici comment j'ai résolu:
J'ai d'abord activé simultanément la reconnaissance PanGesture.
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
Ensuite, j'isole les mouvements de panoramique horizontal et vertical (l'accumulateur est la propriété 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];
J'ai poussé un exemple ici:
ajouter un panoramique personnalisé dans le champ de défilement
Je suis allé plus loin et ai fait une direction Pan Gesture:
enum PanDirection {
case up
case left
case right
case down
}
class PanDirectionGestureRecognizer: UIPanGestureRecognizer {
fileprivate 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)
guard state != .failed else { return }
let vel = velocity(in: view)
let velocities: [PanDirection: CGFloat]
= [.up: -vel.y,
.left: -vel.x,
.right: vel.x,
.down: vel.y]
let sortedKeys = velocities.sorted { $0.1 < $1.1 }
if let key = sortedKeys.last?.key,
key != direction {
state = .cancelled
}
}
}
(Utilisé: https://github.com/fastred/SloppySwiper et https://stackoverflow.com/a/30607392/5790492 )
J'ai pris un excellent réponse par Lee Goodrich et porté à 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<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
}
}
}
}
Pour tous les utilisateurs de Swift, cela fera l'affaire :)
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
}
}
J'aimerais partager mon approche car toutes les autres approches sont basées sur UIGestureRecognizerDelegate
ou sous-classe UIPanGestureRecognizer
.
Mon approche est basée sur le runtime et le swizzling. Je ne suis pas sûr à 100% de cette approche, mais vous pouvez la tester et l’améliorer vous-même.
Définissez la direction de toute variable UIPanGestureRecognizer
avec une seule ligne de code:
UITableView().panGestureRecognizer.direction = UIPanGestureRecognizer.Direction.vertical
utilisez pod 'UIPanGestureRecognizerDirection'
ou le code:
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<UITouch>, with event: UIEvent) {
self.swizzling_touchesBegan(touches, with: event)
guard direction != nil else { return }
touchesBegan = true
}
@objc private func swizzling_touchesMoved(_ touches: Set<UITouch>, 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)
}
}
Swift 4.2
La solution est juste pour que soutenir le geste panoramique verticalement, identique à horizontal.
let pan = UIPanGestureRecognizer(target: self, action: #selector(test1))
pan.cancelsTouchesInView = false
panView.addGestureRecognizer(pan)
Solution 1 :
@objc func panAction(pan: UIPanGestureRecognizer) {
let velocity = pan.velocity(in: panView)
guard abs(velocity.y) > abs(velocity.x) else {
return
}
}
Solution 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)
}
Ensuite, le geste de balayage avalera le geste de panoramique. Bien sûr, vous n'avez rien à faire dans swipeAction
.
J'ai essayé ceci: ce qui a fonctionné pour moi selon la question décrite
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer {
return true
} else {
return false
}
}