web-dev-qa-db-fra.com

Faire défiler avec deux doigts avec un UIScrollView

J'ai une application pour laquelle ma vue principale accepte à la fois touchesBegan et touchesMoved, et accepte donc les contacts à un doigt et les traînées. Je souhaite implémenter une UIScrollView et je la fais fonctionner, mais elle annule les traînées et, par conséquent, mon contentView ne les reçoit jamais. J'aimerais implémenter une UIScrollview, dans laquelle un glissement de deux doigts indique un défilement, et un événement de glissement de doigt à un doigt est transmis à mon affichage de contenu, de sorte qu'il fonctionne normalement. Dois-je créer ma propre sous-classe de UIScrollView

Voici le code de ma appDelegate où j'implémente la 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];
}
41
Craig

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

  • faire en sorte que les événements à un doigt passent dans l'affichage de contenu (facile), et

  • faire en sorte que les événements à deux doigts défilent dans la vue de défilement (peut être facile, difficile ou impossible).

La suggestion de Patrick est généralement satisfaisante: informez votre sous-classe UIScrollView de la vue de votre contenu, puis, dans le cas des gestionnaires d'événements tactiles, vérifiez le nombre de doigts et transmettez l'événement en conséquence. Assurez-vous simplement que (1) les événements que vous envoyez à la vue du contenu ne sont pas renvoyés à UIScrollView par le biais de la chaîne de répondeur (c.-à-d. Assurez-vous de les gérer tous), (2) respectez le flux habituel d'événements tactiles (c'est-à-dire touchesBegan, un certain nombre de {touchesBegan, touchesMoved, touchesEnded}, terminées par des touchesEnded ou touchesAnnulé), en particulier avec UIScrollView. # 2 peut être difficile.

Si vous décidez que l'événement est destiné à 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 (NSSet *)touches - notez qu'il ne contient que les touches modifiées - et en ignorant 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 similaire à UITouch. Le code C sous-jacent ne vérifie pas les types, alors peut-être que la conversion (YourTouch *) en (UITouch *) fonctionnera et que vous pourrez tromper UIScrollView afin de gérer les manipulations qui ne se sont pas réellement produites.

Vous voudrez probablement lire mon article sur les astuces avancées de UIScrollView (et y voir quelques exemples de code UIScrollView totalement sans rapport).

Bien sûr, si vous ne parvenez pas à le faire fonctionner, vous avez toujours la possibilité de contrôler le déplacement d'UIScrollView manuellement ou d'utiliser une vue de défilement entièrement personnalisée. Il y a la classe TTScrollView dans Bibliothèque Three20 ; cela ne fait pas du bien à l'utilisateur, mais au programmeur.

10
Andrey Tarantsov

Dans le SDK 3.2, la manipulation tactile pour UIScrollView est gérée à l'aide de la reconnaissance de geste.

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

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

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

self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;

Le glissement d'un doigt est ignoré par panGestureRecognizer, de sorte que l'événement de glissement d'un doigt est transmis à l'affichage du contenu.

34
Guto Araujo

Dans iOS 3.2+, vous pouvez maintenant réaliser le défilement à deux doigts assez facilement. Ajoutez simplement un identifiant de mouvement de panoramique à la vue de défilement et définissez son maximumNumberOfTouches à 1. Il revendiquera tous les défilements à un doigt, mais permettra à 2 défilés de plus de faire passer la chaîne au reconnaisseur intégré de la vue de défilement permettre un comportement de défilement normal).

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

Ces réponses sont un gâchis, car 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 été inversée) La réponse acceptée est trop vague pour être utile et suggère une méthode différente.

Synthétiser, ça marche

    // 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 parchemin. J'ai fait cela dans une sous-classe, mais si ce n'est pas le cas, remplacez simplement self.gestureRecognizers par myScrollView.gestureRecognizers et le tour est joué. 

La seule chose que j'ai ajoutée est d'utiliser id pour éviter une distribution laide :)

Cela fonctionne, mais peut devenir assez compliqué si vous voulez que votre UIScrollView fasse aussi le zoom ... les gestes ne fonctionnent pas correctement, puisque pincer pour zoomer et faire défiler les combattre. Je mettrai à jour ceci si je trouve une réponse appropriée.

8
Dan Rosenstark

nous avons réussi à implémenter des fonctionnalités similaires dans notre application de dessin pour iPhone en sous-classant UIScrollView et en filtrant les événements 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 est 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;

un simple tap ne fait rien (vous pouvez le gérer comme vous le souhaitez), tapotez avec la vue défilement/zoom de deux doigts comme prévu. aucun GestureRecognizer n'est utilisé, donc fonctionne depuis iOS 3.1

2
IPv6

Mauvaise nouvelle: iPhone SDK version 3.0 ou ultérieure, ne passez plus au contact avec les méthodes -touchesBegan: et -touchesEnded: ** UIScrollview **. Vous pouvez utiliser les méthodes touchesShouldBegin et touchesShouldCancelInContentView qui ne sont pas identiques.

Si vous voulez vraiment avoir ce contact, ayez un bidouille qui le permet.

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

- (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 passera à votre sous-classe ceci touche, cependant vous ne pouvez pas annuler les touches à UIScrollView super class. 

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

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

Et dans la vue de mon enfant, j’ai une minuterie car les contacts à 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 un deuxième contact commence: l'événement arrive avec plus d'un doigt, la vue de défilement est autorisée à annuler les contacts. Ainsi, si l'utilisateur effectue un panoramique avec deux doigts, cette vue recevra un message touchesCanceled:.

1
lucius

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 d'une manière très satisfaisante d'une manière différente, essentiellement en remplaçant mon propre outil de reconnaissance de geste dans l'équation. Je recommande fortement à toute personne cherchant à obtenir l’effet demandé par l’affiche originale d’envisager cette alternative au sous-classement agressif de UIScrollView.

Le processus suivant fournira:

  • Une 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

Supposons d’abord 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 pour qu'elle ne soit pas redimensionnée. Dans les attributs de la vue de défilement, activez tout ce qui dit "rebond" et activez off "delaysContentTouches". De plus, vous devez régler les zooms minimum et maximum sur des valeurs autres que la valeur par défaut de 1,0, comme l'indiquent la documentation d'Apple, pour que le zoom fonctionne.

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

Vous devrez ajouter le code suivant à la sous-classe UIScrollView pour qu'il passe de manière transparente les événements tactiles (je suppose que cela pourrait être fait de manière plus élégante, peut-être même en contournant 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];
}

}

Notez que dans cette méthode, il est fait référence à 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 fini!

Malheureusement, je ne pouvais pas demander à scrollView de montrer ses défilement 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 saisit tout zoom final final d’environ 100% et l’arrondit juste à cela. Vous pouvez ajuster votre niveau de tolérance. J'avais vu cela dans le comportement de zoom de Pages et pensé que ce serait une belle touche.

1
SG1

Je mets cela dans la méthode viewDidLoad, ce qui permet à la vue de défilement de gérer le comportement de panoramique tactile et un autre gestionnaire de mouvements de panoramique gérant le comportement de panoramique tactile ->

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 existe simplement une instruction print permettant de vérifier que la méthode est en cours de saisie ->

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

HTH

0
Adam Freeman

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
    }
}
0
tnylee

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 contact et ne s'arrête pas lorsque l'utilisateur le touche avec deux doigts après en avoir utilisé un. Néanmoins, cela permet d’annuler un événement tactile qui vient de commencer en utilisant une minuterie.

0
Jan

Oui, vous devrez sous-classe UIScrollView et substituer ses méthodes -touchesBegan: et -touchesEnded: pour passer les touches "en haut". Cela impliquera probablement aussi que la sous-classe ait une variable membre UIView, de sorte qu'elle sache à quoi elle sert.

0
anisoptera