Défilement avec deux doigts avec un UIScrollView

J’ai une application où ma vue principale accepte les deux touchesBegan et touchesMoved , et prend donc des touchesMoved avec un seul doigt, et entraîne. Je veux implémenter un UIScrollView , et je le fais fonctionner, mais il remplace les balises, et donc mon contentView ne les reçoit jamais. Je voudrais implémenter une vue UIScrollview , où un glissement à deux doigts indique un défilement, et un événement de glissement à un doigt est transmis à mon affichage de contenu, de sorte qu’il fonctionne normalement. Ai-je besoin de créer ma propre sous-classe de UIScrollView ?

Voici mon code de mon appDelegateappDelegate UIScrollView .

 @implementation MusicGridAppDelegate @synthesize window; @synthesize viewController; @synthesize scrollView; - (void)applicationDidFinishLaunching:(UIApplication *)application { // Override point for customization after app launch //[application setStatusBarHidden:YES animated:NO]; //[window addSubview:viewController.view]; scrollView.contentSize = CGSizeMake(720, 480); scrollView.showsHorizontalScrollIndicator = YES; scrollView.showsVerticalScrollIndicator = YES; scrollView.delegate = self; [scrollView addSubview:viewController.view]; [window makeKeyAndVisible]; } - (void)dealloc { [viewController release]; [scrollView release]; [window release]; [super dealloc]; } 

Vous devez sous-classer UIScrollView (bien sûr!). Ensuite, vous devez:

  • créer des événements à un doigt pour accéder à votre vue de contenu (facile), et

  • faire défiler les événements à deux doigts dans la vue de défilement (cela peut être facile, peut être difficile, peut être impossible).

La suggestion de Pasortingck est généralement correcte: laissez votre sous-classe UIScrollView connaître votre vue du contenu, puis en contact les gestionnaires d’événements vérifient le nombre de doigts et transfèrent l’événement en conséquence. Assurez-vous simplement que (1) les événements que vous envoyez à la vue du contenu ne reviennent pas à UIScrollView via la chaîne de répondeurs (par exemple, assurez-vous de les gérer tous), (2) respectez le stream habituel des événements tactiles un certain nombre de {touchesBegan, touchesMoved, touchesEnded}, fini avec touchEnded ou touchCancelled), en particulier avec UIScrollView. # 2 peut être difficile.

Si vous décidez que l’événement est pour UIScrollView, une autre astuce consiste à faire croire à UIScrollView que votre geste à deux doigts est en réalité un geste à un doigt (car UIScrollView ne peut pas être défilé avec deux doigts). Essayez de ne transmettre que les données d’un doigt à super (en filtrant l’argument des (NSSet *)touches – notez qu’il ne contient que les touches modifiées – et (NSSet *)touches complètement les événements pour le mauvais doigt).

Si cela ne fonctionne pas, vous êtes en difficulté. Théoriquement, vous pouvez essayer de créer des touches artificielles pour alimenter UIScrollView en créant une classe qui ressemble à UITouch. Le code C sous-jacent ne vérifie pas les types, alors peut-être que lancer (YourTouch *) dans (UITouch *) fonctionnera, et vous pourrez tromper UIScrollView pour qu’il traite les touches qui ne se sont pas réellement produites.

Vous voulez probablement lire mon article sur les astuces avancées d’UIScrollView (et y voir un exemple de code UIScrollView totalement indépendant).

Bien sûr, si vous ne parvenez pas à le faire fonctionner, il est toujours possible de contrôler manuellement les mouvements de UIScrollView ou d’utiliser une vue de défilement entièrement personnalisée. Il y a la classe TTScrollView dans la bibliothèque Three20 ; il ne se sent pas bien pour l’utilisateur, mais se sent bien pour le programmeur.

Dans le kit de développement logiciel 3.2, la gestion tactile d’UIScrollView est gérée à l’aide de Gesture Recognizers.

Si vous souhaitez effectuer un panoramique à deux doigts au lieu du panoramique par défaut à un doigt, vous pouvez utiliser le code suivant:

 for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) { if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) { UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *) gestureRecognizer; panGR.minimumNumberOfTouches = 2; } } 

Pour iOS 5+, le paramétrage de cette propriété a le même effet que la réponse de Mike Laurence:

 self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2; 

Un glisser-déplacer d’un doigt est ignoré par panGestureRecognizer et l’événement de glisser un doigt est transmis à la vue de contenu.

Dans iOS 3.2+, vous pouvez maintenant effectuer le défilement à deux doigts très facilement. Ajoutez simplement un identificateur de mouvement panoramique à la vue de défilement et définissez ses valeurs maximales sur NombreNoFormées à 1. Il va réclamer tous les défilement à un doigt, mais autoriser les défilements à deux doigts pour passer à la chaîne intégrée de reconnaissance de défilement (et donc autoriser le comportement de défilement normal).

 UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(recognizePan:)]; panGestureRecognizer.maximumNumberOfTouches = 1; [scrollView addGestureRecognizer:panGestureRecognizer]; [panGestureRecognizer release]; 

Cette réponse est un gâchis puisque vous ne pouvez trouver la bonne réponse qu’en lisant toutes les autres réponses et les commentaires (la réponse la plus proche a eu la question en arrière). La réponse acceptée est trop vague pour être utile et suggère une méthode différente.

Synthétiser, cela fonctionne

  // makes it so that only two finger scrolls go for (id gestureRecognizer in self.gestureRecognizers) { if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) { UIPanGestureRecognizer *panGR = gestureRecognizer; panGR.minimumNumberOfTouches = 2; panGR.maximumNumberOfTouches = 2; } } 

Cela nécessite deux doigts pour un défilement. Je l’ai fait dans une sous-classe, mais sinon, remplacez simplement self.gestureRecognizers par myScrollView.gestureRecognizers et vous êtes myScrollView.gestureRecognizers à partir.

La seule chose que j’ai ajoutée est d’utiliser id pour éviter un casting moche 🙂

Cela fonctionne mais peut devenir assez désordonné si vous voulez que votre UIScrollView fasse un zoom aussi … les gestes ne fonctionnent pas correctement, puisque le pincement pour zoomer et le défilement le font. Je mettrai ceci à jour si je trouve une réponse appropriée.

Nous avons réussi à implémenter des fonctionnalités similaires dans notre application de dessin iPhone en sous-classant UIScrollView et les événements de filtrage en fonction du nombre de touches de manière simple et grossière:

 //OCRScroller.h @interface OCRUIScrollView: UIScrollView { double pass2scroller; } @end //OCRScroller.mm @implementation OCRUIScrollView - (id)initWithFrame:(CGRect)aRect { pass2scroller = 0; UIScrollView* newv = [super initWithFrame:aRect]; return newv; } - (void)setupPassOnEvent:(UIEvent *)event { int touch_cnt = [[event allTouches] count]; if(touch_cnt<=1){ pass2scroller = 0; }else{ double timems = double(CACurrentMediaTime()*1000); pass2scroller = timems+200; } } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self setupPassOnEvent:event]; [super touchesBegan:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [self setupPassOnEvent:event]; [super touchesMoved:touches withEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { pass2scroller = 0; [super touchesEnded:touches withEvent:event]; } - (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view { return YES; } - (BOOL)touchesShouldCancelInContentView:(UIView *)view { double timems = double(CACurrentMediaTime()*1000); if (pass2scroller == 0 || timems> pass2scroller){ return NO; } return YES; } @end 

ScrollView configuré comme suit:

 scroll_view = [[OCRUIScrollView alloc] initWithFrame:rect]; scroll_view.contentSize = img_size; scroll_view.contentOffset = CGPointMake(0,0); scroll_view.canCancelContentTouches = YES; scroll_view.delaysContentTouches = NO; scroll_view.scrollEnabled = YES; scroll_view.bounces = NO; scroll_view.bouncesZoom = YES; scroll_view.maximumZoomScale = 10.0f; scroll_view.minimumZoomScale = 0.1f; scroll_view.delegate = self; self.view = scroll_view; 

simple taper ne fait rien (vous pouvez le gérer de la manière dont vous en avez besoin), appuyez avec deux doigts pour faire défiler / zoomer comme prévu. aucun GestureRecognizer n’est utilisé, donc fonctionne depuis iOS 3.1

J’ai une nouvelle amélioration du code ci-dessus. Le problème était que même après avoir défini setCanCancelContentTouches:NO Nous avons le problème, un geste de zoom interrompra le contenu. Il n’annule pas le toucher du contenu, mais permet de zoomer entre-temps. Pour éviter cela, je verrouille le zoom en définissant les valeurs minimumZoomScale et maximumZoomScale sur les mêmes valeurs à chaque fois, le minuteur se déclenche.

Un comportement assez étrange est que lorsqu’un événement à un doigt est annulé par un geste de deux doigts dans la période de temps autorisée, le temporisateur sera retardé. Il est déclenché après l’appel de l’événement touchCanceled. Nous avons donc le problème, que nous essayons de verrouiller le zoom bien que l’événement soit déjà annulé et donc de désactiver le zoom pour le prochain événement. Pour gérer ce comportement, la méthode de rappel du minuteur vérifie si touchesCanceled a été appelée auparavant. @implementation JWTwoFingerScrollView

 #pragma mark - #pragma mark Event Passing - (id)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) { for (UIGestureRecognizer* r in self.gestureRecognizers) { if ([r isKindOfClass:[UIPanGestureRecognizer class]]) { [((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2]; [((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2]; zoomScale[0] = -1.0; zoomScale[1] = -1.0; } timerWasDelayed = NO; } } return self; } -(void)lockZoomScale { zoomScale[0] = self.minimumZoomScale; zoomScale[1] = self.maximumZoomScale; [self setMinimumZoomScale:self.zoomScale]; [self setMaximumZoomScale:self.zoomScale]; NSLog(@"locked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale); } -(void)unlockZoomScale { if (zoomScale[0] != -1 && zoomScale[1] != -1) { [self setMinimumZoomScale:zoomScale[0]]; [self setMaximumZoomScale:zoomScale[1]]; zoomScale[0] = -1.0; zoomScale[1] = -1.0; NSLog(@"unlocked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale); } } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"began %i",[event allTouches].count); [self setCanCancelContentTouches:YES]; if ([event allTouches].count == 1){ touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(firstTouchTimerFired:) userInfo:nil repeats:NO]; [touchesBeganTimer retain]; [touchFilter touchesBegan:touches withEvent:event]; } } //if one finger touch gets canceled by two finger touch, this timer gets delayed // so we can! use this method to disable zooming, because it doesnt get called when two finger touch events are wanted; otherwise we would disable zooming while zooming -(void)firstTouchTimerFired:(NSTimer*)timer { NSLog(@"fired"); [self setCanCancelContentTouches:NO]; //if already locked: unlock //this happens because two finger gesture delays timer until touch event finishes.. then we dont want to lock! if (timerWasDelayed) { [self unlockZoomScale]; } else { [self lockZoomScale]; } timerWasDelayed = NO; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { // NSLog(@"moved %i",[event allTouches].count); [touchFilter touchesMoved:touches withEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"ended %i",[event allTouches].count); [touchFilter touchesEnded:touches withEvent:event]; [self unlockZoomScale]; } //[self setCanCancelContentTouches:NO]; -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"canceled %i",[event allTouches].count); [touchFilter touchesCancelled:touches withEvent:event]; [self unlockZoomScale]; timerWasDelayed = YES; } @end 

Mauvaise nouvelle: iPhone SDK 3.0 et supérieur, ne passez plus aux touches -touchesBegan: et – touchesEnded: ** Les méthodes de sous-classe UIScrollview **. Vous pouvez utiliser les méthodes touchesShouldBegin et touchesShouldCancelInContentView qui ne sont pas les mêmes.

Si vous voulez vraiment avoir cette touche, ayez un hack qui le permet.

Dans votre sous-classe de UIScrollView remplacez la méthode hitTest comme hitTest :

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *result = nil; for (UIView *child in self.subviews) if ([child pointInside:point withEvent:event]) if ((result = [child hitTest:point withEvent:event]) != nil) break; return result; } 

Cela vous passera sous cette sous-classe, mais vous ne pouvez pas annuler les touches à la super-classe UIScrollView .

Ce que je fais, c’est que mon contrôleur de vue configure la vue de défilement:

 [scrollView setCanCancelContentTouches:NO]; [scrollView setDelaysContentTouches:NO]; 

Et dans ma vue d’enfant, j’ai une timer car les touches à deux doigts commencent généralement par un doigt suivi rapidement par deux doigts .:

 - (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // Hand tool or two or more touches means a pan or zoom gesture. if ((selectedTool == kHandToolIndex) || (event.allTouches.count > 1)) { [[self parentScrollView] setCanCancelContentTouches:YES]; [firstTouchTimer invalidate]; firstTouchTimer = nil; return; } // Use a timer to delay first touch because two-finger touches usually start with one touch followed by a second touch. [[self parentScrollView] setCanCancelContentTouches:NO]; anchorPoint = [[touches anyObject] locationInView:self]; firstTouchTimer = [NSTimer scheduledTimerWithTimeInterval:kFirstTouchTimeInterval target:self selector:@selector(firstTouchTimerFired:) userInfo:nil repeats:NO]; firstTouchTimeStamp = event.timestamp; } 

Si une seconde toucheBegan: l’événement arrive avec plus d’un doigt, la vue de défilement est autorisée à annuler les contacts. Donc, si l’utilisateur utilise deux doigts, cette vue aura un effet sur le message.

Cela semble être la meilleure ressource pour cette question sur Internet. Une autre solution proche peut être trouvée ici .

J’ai résolu ce problème de manière très satisfaisante d’une manière différente, essentiellement en supplantant mon propre dispositif de reconnaissance des gestes dans l’équation. Je recommande fortement à tous ceux qui tentent d’atteindre l’effet demandé par l’affiche originale d’envisager cette alternative par rapport à un sous- UIScrollView agressif de UIScrollView .

Le processus suivant fournira:

  • Un UIScrollView contenant votre vue personnalisée

  • Zoom et panoramique avec deux doigts (via UIPinchGestureRecognizer )

  • Le traitement des événements de votre vue pour toutes les autres touches

Tout d’abord, supposons que vous avez un contrôleur de vue et sa vue. Dans IB, faites de la vue une sous-vue d’un scrollView et ajustez les règles de redimensionnement de votre vue afin qu’elle ne soit pas redimensionnée. Dans les atsortingbuts de la vue de défilement, activez tout ce qui dit “rebond” et désactivezdelaysContentTouches “. Vous devez également définir le zoom min et max à une valeur autre que celle par défaut de 1,0, car, comme le disent les documents Apple, cela est nécessaire pour que le zoom fonctionne.

Créez une sous-classe personnalisée de UIScrollView et faites-en cette liste de défilement personnalisée. Ajoutez une prise à votre contrôleur de vue pour la vue de défilement et connectez-les. Vous êtes maintenant totalement configuré.

Vous devrez append le code suivant à la sous-classe UIScrollView afin qu’il transmette de manière transparente les événements tactiles (je pense que cela pourrait être fait de manière plus élégante, voire en ignorant complètement la sous-classe):

 #pragma mark - #pragma mark Event Passing - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self.nextResponder touchesBegan:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [self.nextResponder touchesMoved:touches withEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self.nextResponder touchesEnded:touches withEvent:event]; } - (BOOL)touchesShouldCancelInContentView:(UIView *)view { return NO; } 

Ajoutez ce code à votre contrôleur de vue:

 - (void)setupGestures { UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)]; [self.view addGestureRecognizer:pinchGesture]; [pinchGesture release]; } - (IBAction)handlePinchGesture:(UIPinchGestureRecognizer *)sender { if ( sender.state == UIGestureRecognizerStateBegan ) { //Hold values previousLocation = [sender locationInView:self.view]; previousOffset = self.scrollView.contentOffset; previousScale = self.scrollView.zoomScale; } else if ( sender.state == UIGestureRecognizerStateChanged ) { //Zoom [self.scrollView setZoomScale:previousScale*sender.scale animated:NO]; //Move location = [sender locationInView:self.view]; CGPoint offset = CGPointMake(previousOffset.x+(previousLocation.x-location.x), previousOffset.y+(previousLocation.y-location.y)); [self.scrollView setContentOffset:offset animated:NO]; } else { if ( previousScale*sender.scale < 1.15 && previousScale*sender.scale > .85 ) [self.scrollView setZoomScale:1.0 animated:YES]; } 

}

Veuillez noter que dans cette méthode, il existe des références à un certain nombre de propriétés que vous devez définir dans les fichiers de classe de votre contrôleur de vue:

  • CGFloat previousScale;
  • CGPoint previousOffset;
  • CGPoint previousLocation;
  • CGPoint location;

Ok c’est bon!

Malheureusement, je n’ai pas pu obtenir le scrollView pour montrer ses rouleaux pendant le geste. J’ai essayé toutes ces stratégies:

 //Scroll indicators self.scrollView.showsVerticalScrollIndicator = YES; self.scrollView.showsVerticalScrollIndicator = YES; [self.scrollView flashScrollIndicators]; [self.scrollView setNeedsDisplay]; 

Une chose que j’ai vraiment appréciée, c’est que si vous regardez la dernière ligne, vous remarquerez qu’elle prend tout zoom final à environ 100% et arrondit le résultat à cela. Vous pouvez ajuster votre niveau de tolérance; J’avais vu cela dans le comportement de zoom de Pages et je pensais que ce serait une bonne idée.

Découvrez ma solution :

 #import “JWTwoFingerScrollView.h” @implementation JWTwoFingerScrollView - (id)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) { for (UIGestureRecognizer* r in self.gestureRecognizers) { NSLog(@“%@”,[r class]); if ([r isKindOfClass:[UIPanGestureRecognizer class]]) { [((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2]; [((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2]; } } } return self; } -(void)firstTouchTimerFired:(NSTimer*)timer { [self setCanCancelContentTouches:NO]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self setCanCancelContentTouches:YES]; if ([event allTouches].count == 1){ touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(firstTouchTimerFired:) userInfo: nil repeats:NO]; [touchesBeganTimer retain]; [touchFilter touchesBegan:touches withEvent:event]; } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [touchFilter touchesMoved:touches withEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@“ended %i”,[event allTouches].count); [touchFilter touchesEnded:touches withEvent:event]; } -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@“canceled %i”,[event allTouches].count); [touchFilter touchesCancelled:touches withEvent:event]; } @end 

Il ne retarde pas le premier toucher et ne s’arrête pas lorsque l’utilisateur touche avec deux doigts après en avoir utilisé un. Pourtant, il permet d’annuler un événement tactile qui vient juste de commencer en utilisant une timer.

Oui, vous devrez sous- UIScrollView et remplacer ses – touchesBegan: et -touchesEnded: méthodes pour passer les touches “up”. Cela impliquera probablement que la sous-classe ait une variable membre UIView afin qu’elle sache à quoi elle est censée passer.

Je mets ceci dans la méthode viewDidLoad et cela accomplit la vue de défilement gérant le comportement du panoramique à deux touches et un autre gestionnaire de mouvements panoramiques gérant le comportement du panoramique à une touche ->

 scrollView.panGestureRecognizer.minimumNumberOfTouches = 2 let panGR = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:))) panGR.minimumNumberOfTouches = 1 panGR.maximumNumberOfTouches = 1 scrollView.gestureRecognizers?.append(panGR) 

et dans la méthode handlePan qui est une fonction attachée à ViewController, il y a simplement une instruction print pour vérifier que la méthode est entrée ->

 @IBAction func handlePan(_ sender: UIPanGestureRecognizer) { print("Entered handlePan numberOfTuoches: \(sender.numberOfTouches)") } 

HTH

La réponse de Kenshi dans Swift 4

 for gestureRecognizer: UIGestureRecognizer in self.gestureRecognizers! { if (gestureRecognizer is UIPanGestureRecognizer) { let panGR = gestureRecognizer as? UIPanGestureRecognizer panGR?.minimumNumberOfTouches = 2 } }