Vous êtes dans une situation très difficile, je dois dire.
Notez que vous devez utiliser un UIScrollView avec pagingEnabled=YES
pour basculer entre les pages, mais vous devez pagingEnabled=NO
faire 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 hitTest
méthode et se retourne toujours lui-même, de sorte que tous les événements tactiles passent dans UIScrollView. Ensuite, à l'intérieur touchesBegan
, touchesMoved
etc., 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
delaysContentTouches
est 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
cancelsTouches
est 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
, touchesEnded
et les touchesCanceled
événements de CocoaTouch (parce que son hitTest
lui 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 delaysContentTouches
sur 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' touches
argument: 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 touchesMoved
signifie pas de défilement, donc cette initiale touchesBegan
ne fera aucun mal. Mais réglez delaysContentTouches
sur 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 touchesCancelled
et touchesEnded
. Vous devez transformer touchesEnded
en touchesCancelled
, cependant, parce que UIScrollView interpréterait touchesBegan
, touchesEnded
sé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.
@interface RemorsefulScrollView : UIScrollView {
CGPoint _originalPoint;
BOOL _isHorizontalScroll, _isMultitouch;
UIView *_currentChild;
}
@end
#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];
if (_isHorizontalScroll)
return;
if ([touches count] == [[event touchesForView:self] count]) {
_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];
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 touchesXxx
rentré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 pagingEnabled
propriété à la volée à partir de votre scrollViewDidScroll
mé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 scrollViewDidScroll
si 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
, 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 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 à andreyvit@gmail.com, je l'inclurais volontiers dans mon sac d'astuces ScrollingMadness .
PPS Ajout de ce petit essai à ScrollingMadness README.