60 votes

UIScrollView : pagination horizontale, défilement vertical ?

Comment puis-je forcer un UIScrollView dans lequel la pagination et le défilement sont activés à ne se déplacer que verticalement ou horizontalement à un moment donné ?

Je crois savoir que le directionalLockEnabled devrait permettre d'atteindre cet objectif, mais un balayage diagonal fait toujours défiler la vue en diagonale au lieu de limiter le mouvement à un seul axe.

Edit : pour être plus clair, j'aimerais permettre à l'utilisateur de défiler horizontalement OU verticalement, mais pas les deux simultanément.

87voto

Andrey Tarantsov Points 4487

Vous êtes dans une situation très difficile, je dois dire.

Notez que vous devez utiliser un UIScrollView avec pagingEnabled=YES pour passer d'une page à l'autre, mais vous devez pagingEnabled=NO pour faire défiler verticalement.

Il y a 2 stratégies possibles. Je ne sais pas laquelle fonctionnera / sera la plus facile à mettre en œuvre, alors essayez les deux.

D'abord : des UIScrollViews imbriqués. Franchement, je n'ai pas encore vu de personne qui ait réussi à faire fonctionner cela. Cependant, je n'ai pas essayé personnellement, et ma pratique montre que lorsque vous faites des efforts, vous pouvez faire faire à UIScrollView ce que vous voulez .

La stratégie consiste donc à laisser la vue de défilement extérieure gérer uniquement le défilement horizontal, et les vues de défilement intérieures gérer uniquement le défilement vertical. Pour y parvenir, vous devez savoir comment UIScrollView fonctionne en interne. Il remplace hitTest et se renvoie toujours elle-même, de sorte que tous les événements tactiles sont transmis à UIScrollView. Ensuite, dans touchesBegan , touchesMoved etc, il vérifie s'il est intéressé par l'événement, et le traite ou le transmet aux composants internes.

Pour décider si la touche doit être traitée ou transférée, UIScrollView démarre une minuterie lors de la première touche :

  • Si vous n'avez pas bougé votre doigt de manière significative dans les 150 ms, l'événement est transmis à la vue intérieure.

  • Si vous avez déplacé votre doigt de manière significative dans les 150 ms, le défilement commence (et ne transmet jamais l'événement à la vue intérieure).

    Notez que lorsque vous touchez un tableau (qui est une sous-classe de la vue défilante) et que vous commencez à le faire défiler immédiatement, la ligne que vous avez touchée n'est jamais mise en évidence.

  • Si vous avez no vous avez déplacé votre doigt de manière significative dans les 150 ms et UIScrollView a commencé à transmettre les événements à la vue interne, mais puis vous avez déplacé le doigt suffisamment loin pour que le défilement commence, UIScrollView appelle touchesCancelled sur la vue intérieure et commence le défilement.

    Notez que lorsque vous touchez un tableau, que vous maintenez votre doigt un peu appuyé et que vous commencez à le faire défiler, la ligne que vous avez touchée est d'abord mise en évidence, mais elle est ensuite retirée de la liste.

Ces séquences d'événements peuvent être modifiées par la configuration de UIScrollView :

  • Si delaysContentTouches est NO, alors aucune minuterie n'est utilisée - les événements vont immédiatement vers le contrôle interne (mais sont ensuite annulés si vous déplacez votre doigt suffisamment loin).
  • Si cancelsTouches est NON, alors une fois que les événements sont envoyés à un contrôle, le défilement ne se produira jamais.

Notez que c'est UIScrollView qui reçoit todo touchesBegin , touchesMoved , touchesEnded y touchesCanceled de CocoaTouch (parce que son hitTest lui dit de le faire). Il les transmet ensuite à la vue interne s'il le souhaite, aussi longtemps qu'il le veut.

Maintenant que vous savez tout sur UIScrollView, vous pouvez modifier son comportement. Je parie que vous voulez donner la préférence au défilement vertical, de sorte qu'une fois que l'utilisateur touche la vue et commence à déplacer son doigt (même légèrement), la vue commence à défiler dans la direction verticale ; mais lorsque l'utilisateur déplace son doigt dans la direction horizontale suffisamment loin, vous voulez annuler le défilement vertical et commencer le défilement horizontal.

Vous voulez sous-classer votre UIScrollView externe (disons que vous nommez votre classe RemorsefulScrollView), de sorte qu'au lieu du comportement par défaut, elle transmette immédiatement tous les événements à la vue interne, et qu'elle ne défile que lorsqu'un mouvement horizontal significatif est détecté.

Comment faire pour que RemorsefulScrollView se comporte de cette façon ?

  • Il semble que la désactivation du défilement vertical et la mise en place de delaysContentTouches à NO devrait faire fonctionner les UIScrollViews imbriqués. Malheureusement, ce n'est pas le cas ; UIScrollView semble effectuer un filtrage supplémentaire pour les mouvements rapides (qui ne peut être désactivé), de sorte que même si UIScrollView ne peut être défilé qu'horizontalement, il absorbera toujours (et ignorera) les mouvements verticaux suffisamment rapides.

    L'effet est si grave que le défilement vertical à l'intérieur d'une vue de défilement imbriquée est inutilisable. (Il semble que vous ayez exactement cette configuration, alors essayez : maintenez un doigt pendant 150 ms, puis déplacez-le dans la direction verticale - la vue de défilement imbriquée UIScrollView fonctionne alors comme prévu).

  • Cela signifie que vous ne pouvez pas utiliser le code de UIScrollView pour le traitement des événements ; vous devez surcharger les quatre méthodes de traitement des contacts dans RemorsefulScrollView et effectuer votre propre traitement en premier lieu, en transmettant seulement l'événement à super (UIScrollView) si vous avez décidé d'opter pour un défilement horizontal.

  • Cependant, vous devez passer touchesBegan à UIScrollView, parce que vous voulez qu'il se souvienne d'une coordonnée de base pour un futur défilement horizontal (si vous décidez plus tard qu'il est nécessaire d'utiliser la fonction de défilement horizontal). es un défilement horizontal). Vous ne serez pas en mesure d'envoyer touchesBegan à UIScrollView plus tard, parce que vous ne pouvez pas stocker la touches argument : il contient les objets qui seront mutés avant le prochain changement d'objet. touchesMoved et vous ne pouvez pas reproduire l'ancien état.

    Donc vous devez passer touchesBegan à UIScrollView immédiatement, mais vous masquerez toute autre touchesMoved jusqu'à ce que vous décidiez de faire défiler les pages horizontalement. Non touchesMoved signifie qu'il n'y a pas de défilement, donc cette touchesBegan ne fera pas de mal. Mais mettez delaysContentTouches sur NON, afin qu'aucune autre minuterie surprise n'interfère.

    (Hors sujet - contrairement à vous, UIScrollView peut stocker correctement les touches et peut reproduire et transmettre l'original touchesBegan plus tard. Il a l'avantage d'utiliser des API non publiées, et peut donc cloner les objets tactiles avant qu'ils ne soient mutés).

  • Étant donné que vous transmettez toujours touchesBegan vous devez également transmettre touchesCancelled y touchesEnded . Vous devez tourner touchesEnded en touchesCancelled Cependant, parce que UIScrollView interpréterait l'option touchesBegan , touchesEnded comme un clic tactile, et le transmettrait à la vue interne. Vous faites déjà suivre les événements appropriés vous-même, donc vous ne voulez jamais que UIScrollView fasse suivre quoi que ce soit.

En gros, voici le pseudo-code de ce que vous devez faire. Pour la simplicité, je n'autorise jamais le défilement horizontal après que l'événement multitouch se soit produit.

// RemorsefulScrollView.h

@interface RemorsefulScrollView : UIScrollView {
  CGPoint _originalPoint;
  BOOL _isHorizontalScroll, _isMultitouch;
  UIView *_currentChild;
}
@end

// RemorsefulScrollView.m

// the numbers from an example in Apple docs, may need to tune them
#define kThresholdX 12.0f
#define kThresholdY 4.0f

@implementation RemorsefulScrollView

- (id)initWithFrame:(CGRect)frame {
  if (self = [super initWithFrame:frame]) {
    self.delaysContentTouches = NO;
  }
  return self;
}

- (id)initWithCoder:(NSCoder *)coder {
  if (self = [super initWithCoder:coder]) {
    self.delaysContentTouches = NO;
  }
  return self;
}

- (UIView *)honestHitTest:(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;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event]; // always forward touchesBegan -- there's no way to forward it later
    if (_isHorizontalScroll)
      return; // UIScrollView is in charge now
    if ([touches count] == [[event touchesForView:self] count]) { // initial touch
      _originalPoint = [[touches anyObject] locationInView:self];
    _currentChild = [self honestHitTest:_originalPoint withEvent:event];
    _isMultitouch = NO;
    }
  _isMultitouch |= ([[event touchesForView:self] count] > 1);
  [_currentChild touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  if (!_isHorizontalScroll && !_isMultitouch) {
    CGPoint point = [[touches anyObject] locationInView:self];
    if (fabsf(_originalPoint.x - point.x) > kThresholdX && fabsf(_originalPoint.y - point.y) < kThresholdY) {
      _isHorizontalScroll = YES;
      [_currentChild touchesCancelled:[event touchesForView:self] withEvent:event]
    }
  }
  if (_isHorizontalScroll)
    [super touchesMoved:touches withEvent:event]; // UIScrollView only kicks in on horizontal scroll
  else
    [_currentChild touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  if (_isHorizontalScroll)
    [super touchesEnded:touches withEvent:event];
    else {
    [super touchesCancelled:touches withEvent:event];
    [_currentChild touchesEnded:touches withEvent:event];
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
  [super touchesCancelled:touches withEvent:event];
  if (!_isHorizontalScroll)
    [_currentChild touchesCancelled:touches withEvent:event];
}

@end

Je n'ai pas essayé d'exécuter ou même de compiler ceci (et j'ai tapé toute la classe dans un éditeur de texte simple), mais vous pouvez commencer avec ce qui précède et espérer le faire fonctionner.

Le seul problème caché que je vois est que si vous ajoutez à RemorsefulScrollView des vues enfants autres que UIScrollView, les événements tactiles que vous transmettez à un enfant peuvent vous revenir par l'intermédiaire de la chaîne de réponse, si l'enfant ne gère pas toujours les touchers comme UIScrollView. Une implémentation de RemorsefulScrollView à l'épreuve des balles protégerait contre les éléments suivants touchesXxx réinsertion.

Deuxième stratégie : Si, pour une raison quelconque, les UIScrollViews imbriqués ne fonctionnent pas ou s'avèrent trop difficiles à mettre en place, vous pouvez essayer de vous contenter d'un seul UIScrollView, en changeant sa fonction pagingEnabled à la volée à partir de votre scrollViewDidScroll la méthode du délégué.

Pour éviter le défilement en diagonale, vous devez d'abord essayer de vous souvenir de contentOffset dans scrollViewWillBeginDragging ainsi que la vérification et la réinitialisation du ContentOffset à l'intérieur de l'UE. scrollViewDidScroll si vous détectez un mouvement diagonal. Une autre stratégie à essayer est de réinitialiser contentSize pour ne permettre le défilement que dans une seule direction, une fois que vous avez déterminé la direction du doigt de l'utilisateur. (UIScrollView semble plutôt indulgent à l'égard de la manipulation de contentSize et contentOffset à partir de ses méthodes déléguées).

Si cela ne fonctionne pas non plus ou si cela donne des visuels peu soignés, vous devez passer outre. touchesBegan , touchesMoved etc. et ne pas transmettre les événements de mouvement diagonal à UIScrollView. (L'expérience utilisateur sera toutefois sous-optimale dans ce cas, car vous devrez ignorer les mouvements diagonaux au lieu de les forcer dans une seule direction. Si vous vous sentez vraiment aventureux, vous pouvez écrire votre propre sosie de UITouch, quelque chose comme RevengeTouch. Objective-C est du bon vieux C, et il n'y a rien de plus inoffensif au monde que le C ; tant que personne ne vérifie la classe réelle des objets, ce que je crois que personne ne fait, vous pouvez faire en sorte que n'importe quelle classe ressemble à n'importe quelle autre classe. Cela ouvre une possibilité de synthétiser toutes les touches que vous voulez, avec toutes les coordonnées que vous voulez).

Stratégie de sauvegarde : il y a TTScrollView, une réimplémentation sensée de UIScrollView dans le système de gestion de l'information. Bibliothèque Three20 . Malheureusement, l'utilisateur a l'impression que ce n'est pas naturel et qu'il n'est pas à l'aise. Mais si toutes les tentatives d'utilisation de UIScrollView échouent, vous pouvez vous rabattre sur une vue de défilement codée de manière personnalisée. Je vous le déconseille dans la mesure du possible ; l'utilisation de UIScrollView vous assure d'obtenir l'aspect et la sensation natifs, quelle que soit l'évolution des futures versions de l'iPhone OS.

Ok, ce petit essai est devenu un peu trop long. Je suis juste encore dans les jeux UIScrollView après le travail sur ScrollingMadness il y a quelques jours.

P.S. Si vous parvenez à faire fonctionner l'une de ces méthodes et que vous souhaitez les partager, envoyez-moi le code correspondant à l'adresse andreyvit@gmail.com, je l'inclurai volontiers dans mon site Web. Sac d'astuces de ScrollingMadness .

P.P.S. J'ajoute ce petit essai au README de ScrollingMadness.

46voto

Tonetel Points 469

Cela fonctionne pour moi à chaque fois...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);

16voto

Yildiray Meric Points 161

J'ai utilisé la méthode suivante pour résoudre ce problème, j'espère qu'elle vous aidera.

Définissez d'abord les variables suivantes dans le fichier d'en-tête de vos contrôleurs.

CGPoint startPos;
int     scrollDirection;

startPos gardera le contentOffset lorsque votre délégué reçoit scrollViewWillBeginDragging message. Donc, dans cette méthode, nous faisons ceci ;

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    startPos = scrollView.contentOffset;
    scrollDirection=0;
}

puis nous utilisons ces valeurs pour déterminer la direction de défilement souhaitée par les utilisateurs dans la fenêtre scrollViewDidScroll message.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{

   if (scrollDirection==0){//we need to determine direction
       //use the difference between positions to determine the direction.
       if (abs(startPos.x-scrollView.contentOffset.x)<abs(startPos.y-scrollView.contentOffset.y)){          
          NSLog(@"Vertical Scrolling");
          scrollDirection=1;
       } else {
          NSLog(@"Horitonzal Scrolling");
          scrollDirection=2;
       }
    }
//Update scroll position of the scrollview according to detected direction.     
    if (scrollDirection==1) {
       [scrollView setContentOffset:CGPointMake(startPos.x,scrollView.contentOffset.y) animated:NO];
    } else if (scrollDirection==2){
       [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x,startPos.y) animated:NO];
    }
 }

enfin, nous devons arrêter toutes les opérations de mise à jour lorsque l'utilisateur cesse de traîner ;

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (decelerate) {
       scrollDirection=3;
    }
 }

12voto

James J Points 3589

Je suis peut-être en train de faire de la récupération, mais je suis tombé sur ce post aujourd'hui alors que j'essayais de résoudre le même problème. Voici ma solution, qui semble fonctionner parfaitement.

Je veux afficher un écran complet de contenu, mais permettre à l'utilisateur de faire défiler l'un des 4 autres écrans, vers le haut, le bas, la gauche et la droite. J'ai un seul UIScrollView avec une taille de 320x480 et un contentSize de 960x1440. Le décalage du contenu commence à 320,480. Dans scrollViewDidEndDecelerating :, je redessine les 5 vues (la vue centrale et les 4 autour), et je réinitialise le décalage du contenu à 320,480.

Voici l'essentiel de ma solution, la méthode scrollViewDidScroll :.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{   
    if (!scrollingVertically && ! scrollingHorizontally)
    {
        int xDiff = abs(scrollView.contentOffset.x - 320);
        int yDiff = abs(scrollView.contentOffset.y - 480);

        if (xDiff > yDiff)
        {
            scrollingHorizontally = YES;
        }
        else if (xDiff < yDiff)
        {
            scrollingVertically = YES;
        }
    }

    if (scrollingHorizontally)
    {
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, 480);
    }
    else if (scrollingVertically)
    {
        scrollView.contentOffset = CGPointMake(320, scrollView.contentOffset.y);
    }
}

9voto

icompot Points 91

Pour les personnes paresseuses comme moi :

Définir scrollview.directionalLockEnabled a YES

- (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}

- (void) scrollViewDidScroll: (UIScrollView *) scrollView
{
    if (scrollView.contentOffset.x != self.oldContentOffset.x)
    {
        scrollView.pagingEnabled = YES;
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x,
                                               self.oldContentOffset.y);
    }
    else
    {
        scrollView.pagingEnabled = NO;
    }
}

- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X