UIScrollView: pagination horizontalement, défilement vertical?

88

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

Je crois comprendre que la directionalLockEnabledpropriété devrait y parvenir, mais un balayage diagonal fait toujours défiler la vue en diagonale au lieu de restreindre le mouvement à un seul axe.

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

Cœur
la source
Voulez-vous contraindre une vue à faire défiler UNIQUEMENT Horiz / Vert ou voulez-vous que l'utilisateur fasse défiler Horiz OU Vert, mais pas les deux simultanément?
Will Hartung
Pouvez-vous me rendre service et changer le titre pour qu'il ait l'air un peu plus intéressant et non trivial? Quelque chose comme "UIScrollView: pagination horizontalement, défilement vertical?"
Andrey Tarantsov
Malheureusement, j'ai posté la question en tant qu'utilisateur non enregistré sur un ordinateur auquel je n'ai plus accès (avant de m'inscrire sous le même nom quand je suis rentré chez moi), ce qui signifie que je ne peux pas la modifier. Cela devrait également m'expliquer un suivi plus bas au lieu de modifier la question d'origine. Désolé d'avoir rompu l'étiquette de Stack cette fois!
Nick

Réponses:

117

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

Notez que vous devez utiliser un UIScrollView avec pagingEnabled=YESpour basculer entre les pages, mais vous devez pagingEnabled=NOfaire défiler verticalement.

Il existe 2 stratégies possibles. Je ne sais pas lequel fonctionnera / est le plus facile à implémenter, alors essayez les deux.

Premièrement: UIScrollViews imbriquées. Franchement, je n'ai pas encore vu de personne qui a réussi à faire fonctionner cela. Cependant, je n'ai pas essayé assez dur personnellement, et ma pratique montre que lorsque vous essayez assez dur, vous pouvez faire en sorte que UIScrollView fasse tout ce que vous voulez .

La stratégie consiste donc à laisser la vue de défilement externe gérer uniquement le défilement horizontal et les vues de défilement interne de gérer uniquement le défilement vertical. Pour ce faire, vous devez savoir comment UIScrollView fonctionne en interne. Il remplace la hitTestméthode et se retourne toujours lui-même, de sorte que tous les événements tactiles passent dans UIScrollView. Ensuite, à l'intérieur touchesBegan, touchesMovedetc., il vérifie s'il est intéressé par l'événement, et le gère ou le transmet aux composants internes.

Pour décider si le toucher doit être manipulé ou transféré, UIScrollView démarre un minuteur lorsque vous le touchez pour la première fois:

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

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

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

  • Si vous avez pas déplacé votre doigt de manière significative dans les 150ms et UIScrollView a commencé à passer les événements à la vue intérieure, mais alors vous avez déplacé le doigt assez loin pour le défilement pour commencer, UIScrollView appelle touchesCancelledà la vue intérieure et commence à défiler.

    Notez que lorsque vous touchez une table, maintenez votre doigt un peu puis commencez à faire défiler, la ligne que vous avez touchée est mise en surbrillance en premier, mais désélectionnée par la suite.

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

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

Notez qu'il est UIScrollView qui reçoit tous touchesBegin , touchesMoved, touchesEndedet les touchesCanceledévénements de CocoaTouch (parce que son hitTestlui dit de le faire). Il les transmet ensuite à la vue intérieure s'il le souhaite, aussi longtemps qu'il le souhaite.

Maintenant que vous savez tout sur UIScrollView, vous pouvez modifier son comportement. Je peux parier 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 à bouger son doigt (même légèrement), la vue commence à défiler dans le sens vertical; mais lorsque l'utilisateur déplace suffisamment son doigt dans la direction horizontale, vous souhaitez annuler le défilement vertical et démarrer le défilement horizontal.

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

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

  • Il semble que la désactivation du défilement vertical et le réglage delaysContentTouchessur NON devraient faire fonctionner UIScrollViews imbriquées. Malheureusement, ce n'est pas le cas; UIScrollView semble faire un filtrage supplémentaire pour les mouvements rapides (qui ne peuvent pas être désactivés), de sorte que même si UIScrollView ne peut être défilé qu'horizontalement, il mange toujours (et ignore) les mouvements verticaux assez rapides.

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

  • Cela signifie que vous ne pouvez pas utiliser le code d'UIScrollView pour la gestion des événements; vous devez remplacer les quatre méthodes de gestion tactile dans RemorsefulScrollView et effectuer d'abord votre propre traitement, en ne transférant l'événement à super(UIScrollView) que si vous avez décidé d'utiliser le défilement horizontal.

  • Cependant, vous devez passer touchesBeganà UIScrollView, car vous voulez qu'il se souvienne d'une coordonnée de base pour le défilement horizontal futur (si vous décidez plus tard qu'il s'agit d' un défilement horizontal). Vous ne pourrez pas envoyer touchesBeganà UIScrollView plus tard, car vous ne pouvez pas stocker l' touchesargument: il contient des objets qui seront mutés avant l' touchesMovedévénement suivant , et vous ne pouvez pas reproduire l'ancien état.

    Vous devez donc passer touchesBeganà UIScrollView immédiatement, mais vous en masquerez tous les touchesMovedévénements jusqu'à ce que vous décidiez de faire défiler horizontalement. Non touchesMovedsignifie pas de défilement, donc cette initiale touchesBeganne fera aucun mal. Mais réglez delaysContentTouchessur NON, de sorte qu'aucun minuteur de surprise supplémentaire n'interfère.

    (Offtopic - contrairement à vous, UIScrollView peut stocker correctement les touches et peut reproduire et transférer l' touchesBeganévénement d' origine ultérieurement. Il présente un avantage injuste d'utiliser des API non publiées, il peut donc cloner des objets tactiles avant qu'ils ne soient mutés.)

  • Étant donné que vous avancez toujours touchesBegan, vous devez également transmettre touchesCancelledet touchesEnded. Vous devez transformer touchesEndeden touchesCancelled, cependant, parce que UIScrollView interpréterait touchesBegan, touchesEndedséquence comme une touche-clic, et vous le transmettre à la vue intérieure. Vous transférez déjà vous-même les événements appropriés, vous ne voulez donc jamais que UIScrollView transfère quoi que ce soit.

En gros, voici le pseudocode pour ce que vous devez faire. Par souci de simplicité, je n'autorise jamais le défilement horizontal après qu'un événement multipoint s'est 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 brut), mais vous pouvez commencer par ce qui précède et, espérons-le, le faire fonctionner.

Le seul hic caché que je vois est que si vous ajoutez des vues enfants non UIScrollView à RemorsefulScrollView, les événements tactiles que vous transmettez à un enfant peuvent vous revenir via la chaîne de répondeurs, si l'enfant ne gère pas toujours les touches comme le fait UIScrollView. Une implémentation à toute épreuve de RemorsefulScrollView protégerait contre la touchesXxxrentrée.

Deuxième stratégie: si, pour une raison quelconque, les UIScrollViews imbriquées ne fonctionnent pas ou s'avèrent trop difficiles à obtenir, vous pouvez essayer de vous entendre avec un seul UIScrollView, en changeant sa pagingEnabledpropriété à la volée à partir de votre scrollViewDidScrollméthode de délégué.

Pour éviter le défilement diagonal, vous devez d'abord essayer de vous souvenir de contentOffset dans scrollViewWillBeginDragging, puis de vérifier et de réinitialiser contentOffset à l'intérieur scrollViewDidScrollsi vous détectez un mouvement diagonal. Une autre stratégie à essayer est de réinitialiser contentSize pour n'activer le défilement que dans une seule direction, une fois que vous avez décidé dans quelle direction le doigt de l'utilisateur va. (UIScrollView semble assez indulgent pour manipuler contentSize et contentOffset à partir de ses méthodes de délégué.)

Si cela ne fonctionne pas non plus ou entraîne des visuels bâclés, vous devez remplacer touchesBegan, touchesMovedetc. 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 UITouch, quelque chose comme RevengeTouch. Objectif -C est un vieux C simple, et il n'y a rien de plus canard dans le monde que C; tant que personne ne vérifie la classe réelle des objets, ce que je crois que personne ne fait, vous pouvez faire ressembler n'importe quelle classe à n'importe quelle autre classe. 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 d'UIScrollView dans la bibliothèque Three20 . Malheureusement, cela semble très artificiel et non iphonish pour l'utilisateur. Mais si chaque tentative d'utilisation d'UIScrollView échoue, vous pouvez revenir à une vue de défilement codée personnalisée. Je le déconseille si possible; l'utilisation d'UIScrollView garantit que vous obtenez l'aspect et la convivialité natifs, quelle que soit la façon dont il évolue dans les futures versions d'iPhone OS.

D'accord, ce petit essai est un peu trop long. Je suis encore dans les jeux UIScrollView après le travail sur ScrollingMadness il y a plusieurs jours.

PS Si l'un de ces éléments fonctionne et que vous avez envie de partager, veuillez m'envoyer le code correspondant à [email protected], je l'inclurais volontiers dans mon sac d'astuces ScrollingMadness .

PPS Ajout de ce petit essai à ScrollingMadness README.

Andrey Tarantsov
la source
7
Notez que ce comportement de scrollview imbriqué fonctionne dès maintenant dans le SDK, aucune activité amusante n'est nécessaire
andygeers
1
+ 1 a commenté andygeers, puis s'est rendu compte que ce n'était pas exact; vous pouvez toujours "casser" l'UIScrollView normal en faisant glisser en diagonale à partir du point de départ et ensuite vous avez "tiré le défilement perdant" et il ignorera le verrouillage directionnel jusqu'à ce que vous relâchiez et finissiez par dieu sait où.
Kalle
Merci pour l'excellente explication des coulisses! Je suis allé avec la solution de Mattias Wadman ci-dessous qui semble un peu moins invasive à l'OMI.
Ortwin Gentz
Ce serait formidable de voir cette réponse mise à jour maintenant que UIScrollView expose son outil de reconnaissance de mouvement de panoramique. (Mais c'est peut-être en dehors de la portée d'origine.)
jtbandes
Est-il possible que ce message soit mis à jour? Excellent message et merci pour votre contribution.
Pavan
56

Cela fonctionne pour moi à chaque fois ...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);
Tonetel
la source
20
Pour quiconque tombe sur cette question: je pense que cette réponse a été publiée avant la modification qui a clarifié la question. Cela vous permettra de faire défiler uniquement horizontalement, cela ne résout pas le problème de pouvoir faire défiler verticalement ou horizontalement mais pas en diagonale.
andygeers
Fonctionne comme un charme pour moi. J'ai un scrollView horizontal très long avec pagination et un UIWebView sur chaque page, qui gère le défilement vertical dont j'ai besoin.
Tom Redman
Excellent! Mais quelqu'un peut-il expliquer comment cela fonctionne (en définissant contentSize height à 1)!
Praveen
Cela fonctionne vraiment, mais pourquoi et comment cela fonctionne-t-il? Quelqu'un peut-il en tirer un sens?
Marko Francekovic
27

Pour les paresseux comme moi:

Régler scrollview.directionalLockEnabledsurYES

- (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;
}
icompot
la source
16

J'ai utilisé la méthode suivante pour résoudre ce problème, j'espère que cela 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 conservera la valeur contentOffset lorsque votre délégué recevra le message scrollViewWillBeginDragging . 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 voulue par les utilisateurs dans le message scrollViewDidScroll .

- (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 termine le glissement;

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (decelerate) {
       scrollDirection=3;
    }
 }
Yildiray Meric
la source
Hmm ... En fait, après plus d'investigation - cela fonctionne plutôt bien, mais le problème est qu'il perd le contrôle de la direction pendant la phase de décélération - donc si vous commencez à déplacer votre doigt en diagonale, il semblera se déplacer uniquement horizontalement ou verticalement jusqu'à ce que vous lâchiez prise à quel point il ralentira en diagonale pendant un moment
andygeers
@andygeers, cela pourrait fonctionner mieux s'il - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (decelerate) { scrollDirection=3; } }était changé en - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (!decelerate) { scrollDirection=0; } }et- (void)scrollViewDidEndDecelerating{ scrollDirection=0; }
cduck
Cela n'a pas fonctionné pour moi. Définir le contentOffset en vol semble détruire le comportement de pagination. J'ai choisi la solution de Mattias Wadman.
Ortwin Gentz
13

Je suis peut-être en train de creuser des fosses ici, mais je suis tombé sur ce post aujourd'hui alors que j'essayais de résoudre le même problème. Voici ma solution, semble fonctionner très bien.

Je souhaite afficher un écran plein de contenu, mais permettez à l'utilisateur de faire défiler jusqu'à l'un des 4 autres écrans, de haut en bas à gauche et à droite. J'ai un seul UIScrollView avec une taille de 320x480 et un contentSize de 960x1440. Le décalage de contenu commence à 320 480. Dans scrollViewDidEndDecelerating :, je redessine les 5 vues (vues du centre et les 4 qui l'entourent) et 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);
    }
}
James J
la source
Vous constaterez probablement que ce code ne "décélère" pas bien lorsque l'utilisateur lâche la vue de défilement - il s'arrêtera net. Si vous ne voulez pas de décélération, cela peut vous convenir.
andygeers
4

Les gars, c'est aussi simple que de rendre la hauteur de la sous-vue identique à la hauteur de la vue de défilement et à la hauteur de la taille du contenu. Ensuite, le défilement vertical est désactivé.

Julianlubenov
la source
2
Pour quiconque tombe sur cette question: je pense que cette réponse a été publiée avant la modification qui a clarifié la question. Cela vous permettra de faire défiler uniquement horizontalement, cela ne résout pas le problème de pouvoir faire défiler verticalement ou horizontalement mais pas en diagonale.
andygeers
3

Voici mon implémentation d'une page UIScrollViewqui n'autorise que les scrolls orthogonaux. Assurez-vous de régler pagingEnabledsur YES.

PagedOrthoScrollView.h

@interface PagedOrthoScrollView : UIScrollView
@end

PagedOrthoScrollView.m

#import "PagedOrthoScrollView.h"

typedef enum {
  PagedOrthoScrollViewLockDirectionNone,
  PagedOrthoScrollViewLockDirectionVertical,
  PagedOrthoScrollViewLockDirectionHorizontal
} PagedOrthoScrollViewLockDirection; 

@interface PagedOrthoScrollView ()
@property(nonatomic, assign) PagedOrthoScrollViewLockDirection dirLock;
@property(nonatomic, assign) CGFloat valueLock;
@end

@implementation PagedOrthoScrollView
@synthesize dirLock;
@synthesize valueLock;

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

  self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  self.valueLock = 0;

  return self;
}

- (void)setBounds:(CGRect)bounds {
  int mx, my;

  if (self.dirLock == PagedOrthoScrollViewLockDirectionNone) {
    // is on even page coordinates, set dir lock and lock value to closest page 
    mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
    if (mx != 0) {
      self.dirLock = PagedOrthoScrollViewLockDirectionHorizontal;
      self.valueLock = (round(CGRectGetMinY(bounds) / CGRectGetHeight(self.bounds)) *
                        CGRectGetHeight(self.bounds));
    } else {
      self.dirLock = PagedOrthoScrollViewLockDirectionVertical;
      self.valueLock = (round(CGRectGetMinX(bounds) / CGRectGetWidth(self.bounds)) *
                        CGRectGetWidth(self.bounds));
    }

    // show only possible scroll indicator
    self.showsVerticalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionVertical;
    self.showsHorizontalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionHorizontal;
  }

  if (self.dirLock == PagedOrthoScrollViewLockDirectionHorizontal) {
    bounds.origin.y = self.valueLock;
  } else {
    bounds.origin.x = self.valueLock;
  }

  mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
  my = abs((int)CGRectGetMinY(bounds) % (int)CGRectGetHeight(self.bounds));

  if (mx == 0 && my == 0) {
    // is on even page coordinates, reset lock
    self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  }

  [super setBounds:bounds];
}

@end
Mattias Wadman
la source
1
Fonctionne très bien, également sur iOS 7. Merci!
Ortwin Gentz
3

J'ai également dû résoudre ce problème, et bien que la réponse d'Andrey Tarantsov contienne certainement beaucoup d'informations utiles pour comprendre comment UIScrollViewsfonctionne le travail, j'estime que la solution est un peu trop compliquée. Ma solution, qui n'implique aucun sous-classement ou transfert tactile, est la suivante:

  1. Créez deux vues de défilement imbriquées, une pour chaque direction.
  2. Créez deux mannequins UIPanGestureRecognizers, encore une pour chaque direction.
  3. Créez des dépendances d'échec entre les UIScrollViewspropriétés 'panGestureRecognizer et le mannequin UIPanGestureRecognizercorrespondant à la direction perpendiculaire à la direction de défilement souhaitée de votre UIScrollView à l'aide du UIGestureRecognizer's -requireGestureRecognizerToFail:sélecteur.
  4. Dans les -gestureRecognizerShouldBegin:rappels de votre UIPanGestureRecognizers factice, calculez la direction initiale du panoramique à l'aide du sélecteur -translationInView: selector du geste (vous UIPanGestureRecognizersn'appellerez pas ce rappel de délégué tant que votre contact n'aura pas suffisamment traduit pour être enregistré en tant que panoramique). Autorisez ou interdisez le démarrage de votre outil de reconnaissance de gestes en fonction de la direction calculée, qui à son tour devrait contrôler si oui ou non la reconnaissance de mouvement de panoramique UIScrollView perpendiculaire sera autorisée à démarrer.
  5. Dans -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:, ne laissez pas votre mannequin UIPanGestureRecognizersfonctionner parallèlement au défilement (à moins que vous ne souhaitiez ajouter un comportement supplémentaire).
Kurt Horimoto
la source
2

Ce que j'ai fait a été de créer un UIScrollView de pagination avec un cadre de la taille de l'écran de visualisation et de définir la taille du contenu sur la largeur nécessaire pour tout mon contenu et une hauteur de 1 (merci Tonetel). Ensuite, j'ai créé des pages de menu UIScrollView avec des cadres définis pour chaque page, la taille de l'écran de visualisation, et j'ai défini la taille du contenu de chacune sur la largeur de l'écran et la hauteur nécessaire pour chacune. Ensuite, j'ai simplement ajouté chacune des pages de menu à la vue de pagination. Assurez-vous que la pagination est activée sur la vue de défilement de pagination mais désactivée dans les vues de menu (doit être désactivée par défaut) et vous devriez être prêt à partir.

La vue de défilement de pagination défile désormais horizontalement, mais pas verticalement en raison de la hauteur du contenu. Chaque page défile verticalement mais pas horizontalement en raison de ses contraintes de taille de contenu également.

Voici le code si cette explication laissait à désirer:

UIScrollView *newPagingScrollView =  [[UIScrollView alloc] initWithFrame:self.view.bounds]; 
[newPagingScrollView setPagingEnabled:YES];
[newPagingScrollView setShowsVerticalScrollIndicator:NO];
[newPagingScrollView setShowsHorizontalScrollIndicator:NO];
[newPagingScrollView setDelegate:self];
[newPagingScrollView setContentSize:CGSizeMake(self.view.bounds.size.width * NumberOfDetailPages, 1)];
[self.view addSubview:newPagingScrollView];

float pageX = 0;

for (int i = 0; i < NumberOfDetailPages; i++)
{               
    CGRect pageFrame = (CGRect) 
    {
        .origin = CGPointMake(pageX, pagingScrollView.bounds.origin.y), 
        .size = pagingScrollView.bounds.size
    };
    UIScrollView *newPage = [self createNewPageFromIndex:i ToPageFrame:pageFrame]; // newPage.contentSize custom set in here        
    [pagingScrollView addSubview:newPage];

    pageX += pageFrame.size.width;  
}           
Jon Conner
la source
2

Merci Tonetel. J'ai légèrement modifié votre approche, mais c'était exactement ce dont j'avais besoin pour éviter le défilement horizontal.

self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 1);
Jérémie
la source
2

C'est une solution améliorée d'icompot, qui était assez instable pour moi. Celui-ci fonctionne bien et il est facile à mettre en œuvre:

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

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

    float XOffset = fabsf(self.oldContentOffset.x - scrollView.contentOffset.x );
    float YOffset = fabsf(self.oldContentOffset.y - scrollView.contentOffset.y );

        if (scrollView.contentOffset.x != self.oldContentOffset.x && (XOffset >= YOffset) )
        {

            scrollView.pagingEnabled = YES;
            scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x,
                                                   self.oldContentOffset.y);

        }
        else
        {
            scrollView.pagingEnabled = NO;
               scrollView.contentOffset = CGPointMake( self.oldContentOffset.x,
                                                   scrollView.contentOffset.y);
        }

}


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

Pour référence future, j'ai construit sur la réponse d'Andrey Tanasov et est venu avec la solution suivante qui fonctionne bien.

Définissez d'abord une variable pour stocker le contentOffset et définissez-le sur 0,0 coordonnées

var positionScroll = CGPointMake(0, 0)

Maintenant, implémentez ce qui suit dans le délégué scrollView:

override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    // Note that we are getting a reference to self.scrollView and not the scrollView referenced passed by the method
    // For some reason, not doing this fails to get the logic to work
    self.positionScroll = (self.scrollView?.contentOffset)!
}

override func scrollViewDidScroll(scrollView: UIScrollView) {

    // Check that contentOffset is not nil
    // We do this because the scrollViewDidScroll method is called on viewDidLoad
    if self.scrollView?.contentOffset != nil {

        // The user wants to scroll on the X axis
        if self.scrollView?.contentOffset.x > self.positionScroll.x || self.scrollView?.contentOffset.x < self.positionScroll.x {

            // Reset the Y position of the scrollView to what it was BEFORE scrolling started
            self.scrollView?.contentOffset = CGPointMake((self.scrollView?.contentOffset.x)!, self.positionScroll.y);
            self.scrollView?.pagingEnabled = true
        }

        // The user wants to scroll on the Y axis
        else {
            // Reset the X position of the scrollView to what it was BEFORE scrolling started
            self.scrollView?.contentOffset = CGPointMake(self.positionScroll.x, (self.scrollView?.contentOffset.y)!);
            self.collectionView?.pagingEnabled = false
        }

    }

}

J'espère que cela t'aides.

Benjamin
la source
1

À partir de la documentation:

"la valeur par défaut est NON, ce qui signifie que le défilement est autorisé dans les directions horizontale et verticale. Si la valeur est OUI et que l'utilisateur commence à faire glisser dans une direction générale (horizontalement ou verticalement), la vue de défilement désactive le défilement dans l'autre direction. "

Je pense que la partie importante est "si ... l'utilisateur commence glisser dans une direction générale". Donc, s'ils commencent à glisser en diagonale, cela ne fonctionne pas. Non pas que ces documents soient toujours fiables en matière d'interprétation - mais cela semble correspondre à ce que vous voyez.

Cela semble en fait raisonnable. Je dois demander - pourquoi voulez-vous vous limiter à l'horizontale ou à la verticale à tout moment? Peut-être que UIScrollView n'est pas l'outil qu'il vous faut?

philsquared
la source
1

Ma vue se compose de 10 vues horizontales, et certaines de ces vues consistent en plusieurs vues verticales, vous obtiendrez donc quelque chose comme ceci:


1.0   2.0   3.1    4.1
      2.1   3.1
      2.2
      2.3

Avec la pagination activée sur les axes horizontal et vertical

La vue possède également les attributs suivants:

[self setDelaysContentTouches:NO];
[self setMultipleTouchEnabled:NO];
[self setDirectionalLockEnabled:YES];

Maintenant, pour éviter le défilement diagonal, procédez comme suit:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    int pageWidth = 768;
    int pageHeight = 1024;
    int subPage =round(self.contentOffset.y / pageHeight);
    if ((int)self.contentOffset.x % pageWidth != 0 && (int)self.contentOffset.y % pageHeight != 0) {
        [self setContentOffset:CGPointMake(self.contentOffset.x, subPage * pageHeight];
    } 
}

Fonctionne comme un charme pour moi!

user422756
la source
1
ne comprends pas comment cela pourrait fonctionner pour vous ... pourriez-vous télécharger un exemple de projet?
hfossli
1

Besoin de réinitialiser _isHorizontalScrollsur NON dans touchesEndedet touchesCancelled.

Praveen Matanam
la source
0

Si vous n'avez pas essayé de faire cela dans la version bêta 3.0, je vous recommande d'essayer. J'utilise actuellement une série de vues de table dans une vue de défilement, et cela fonctionne comme prévu. (Eh bien, le défilement le fait, de toute façon. J'ai trouvé cette question lors de la recherche d'une solution à un problème totalement différent ...)

Tim Keating
la source
0

Pour ceux qui recherchent dans Swift la deuxième solution @AndreyTarantsov, c'est comme ça dans le code:

    var positionScroll:CGFloat = 0

    func scrollViewWillBeginDragging(scrollView: UIScrollView) {
        positionScroll = self.theScroll.contentOffset.x
    }

    func scrollViewDidScroll(scrollView: UIScrollView) {
        if self.theScroll.contentOffset.x > self.positionScroll || self.theScroll.contentOffset.x < self.positionScroll{
             self.theScroll.pagingEnabled = true
        }else{
            self.theScroll.pagingEnabled = false
        }
    }

ce que nous faisons ici est de conserver la position actuelle juste avant que la vue de défilement ne soit déplacée, puis de vérifier si x a augmenté ou diminué par rapport à la position actuelle de x. Si oui, définissez pagingEnabled sur true, si non (y a augmenté / diminué), définissez pagingEnabled sur false

J'espère que cela sera utile aux nouveaux arrivants!

jonprasetyo
la source
0

J'ai réussi à résoudre ce problème en implémentant scrollViewDidBeginDragging (_ :) et en regardant la vitesse sur le UIPanGestureRecognizer sous-jacent.

NB: Avec cette solution, chaque panoramique qui aurait été en diagonale sera ignoré par le scrollView.

override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    super.scrollViewWillBeginDragging(scrollView)
    let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
    if velocity.x != 0 && velocity.y != 0 {
        scrollView.panGestureRecognizer.isEnabled = false
        scrollView.panGestureRecognizer.isEnabled = true
    }
}
Morten Saabye Kristensen
la source
-3

Si vous utilisez le générateur d'interface pour concevoir votre interface, cela pourrait être fait assez facilement.

Dans le constructeur d'interface, cliquez simplement sur UIScrollView et sélectionnez l'option (dans l'inspecteur) qui le limite à un seul défilement.

zpesk
la source
1
Il n'y a pas d'options dans IB pour ce faire. Les deux options auxquelles vous faites référence concernent peut-être l'affichage de la barre de défilement et non le défilement réel. En outre, le verrouillage de la direction ne verrouille la direction qu'une fois le défilement commencé.
Sophtware