Sei in una situazione molto difficile, devo dire.
Si noti che è necessario utilizzare un UIScrollView con pagingEnabled=YES
per passare da una pagina all'altra, ma è necessario pagingEnabled=NO
scorrere verticalmente.
Ci sono 2 possibili strategie. Non so quale funzionerà / è più facile da implementare, quindi prova entrambi.
Primo: UIScrollViews nidificati. Francamente, devo ancora vedere una persona che deve farlo funzionare. Tuttavia non ho provato abbastanza duramente personalmente, e la mia pratica mostra che quando ci provi abbastanza, puoi fare in modo che UIScrollView faccia tutto ciò che vuoi .
Quindi la strategia consiste nel lasciare che la visualizzazione a scorrimento esterno gestisca solo lo scorrimento orizzontale e le visualizzazioni a scorrimento interno per gestire solo lo scorrimento verticale. Per ottenere ciò, devi sapere come funziona UIScrollView internamente. Ha la precedenzahitTest
metodo e restituisce sempre se stesso, in modo che tutti gli eventi di tocco vengano inseriti in UIScrollView. Quindi all'interno touchesBegan
, touchesMoved
ecc. Controlla se è interessato all'evento e lo gestisce o lo trasmette ai componenti interni.
Per decidere se il tocco deve essere gestito o inoltrato, UIScrollView avvia un timer al primo tocco:
Se non hai spostato il dito in modo significativo entro 150 ms, passa l'evento alla vista interna.
Se hai spostato il dito in modo significativo entro 150 ms, inizia a scorrere (e non passa mai l'evento alla vista interna).
Nota come quando tocchi una tabella (che è una sottoclasse della visualizzazione a scorrimento) e inizi a scorrere immediatamente, la riga che hai toccato non viene mai evidenziata.
Se si è non è mosso il dito in modo significativo all'interno di 150ms e UIScrollView ha iniziato passando gli eventi alla vista interiore, ma poi si è spostato il dito abbastanza lontano per i scorrimento per cominciare, le chiamate UIScrollViewtouchesCancelled
sulla vista interiore e inizia a scorrere.
Nota come quando tocchi una tabella, tieni premuto un po 'il dito e poi inizi a scorrere, la riga che hai toccato viene evidenziata per prima, ma poi de-evidenziata.
Queste sequenze di eventi possono essere modificate dalla configurazione di UIScrollView:
- Se
delaysContentTouches
è NO, non viene utilizzato alcun timer: gli eventi vanno immediatamente al controllo interno (ma vengono annullati se si sposta il dito abbastanza lontano)
- Se
cancelsTouches
è NO, una volta che gli eventi vengono inviati a un controllo, lo scorrimento non avverrà mai.
Nota che è UIScrollView che riceve tutti touchesBegin
, touchesMoved
, touchesEnded
e touchesCanceled
gli eventi da Cocoa Touch (perché la suahitTest
dice di farlo). Quindi li inoltra alla visione interiore se lo desidera, finché lo desidera.
Ora che sai tutto su UIScrollView, puoi modificarne il comportamento. Scommetto che vuoi dare la preferenza allo scorrimento verticale, in modo che una volta che l'utente tocca la vista e inizia a muovere il dito (anche leggermente), la vista inizi a scorrere in direzione verticale; ma quando l'utente sposta il dito in direzione orizzontale abbastanza lontano, si desidera annullare lo scorrimento verticale e avviare lo scorrimento orizzontale.
Vuoi sottoclasse la tua UIScrollView esterna (ad esempio, chiami la tua classe RemorsefulScrollView), in modo che al posto del comportamento predefinito inoltri immediatamente tutti gli eventi alla vista interna e solo quando viene rilevato un movimento orizzontale significativo scorre.
Come fare in modo che RemorsefulScrollView si comporti in questo modo?
Sembra che la disabilitazione dello scorrimento verticale e l'impostazione delaysContentTouches
su NO dovrebbe far funzionare UIScrollViews nidificati. Purtroppo no; UIScrollView sembra fare alcuni filtri aggiuntivi per i movimenti veloci (che non possono essere disabilitati), in modo che anche se UIScrollView può essere fatto scorrere solo orizzontalmente, mangerà sempre (e ignorerà) movimenti verticali abbastanza veloci.
L'effetto è così grave che lo scorrimento verticale all'interno di una visualizzazione a scorrimento nidificato è inutilizzabile. (Sembra che tu abbia esattamente questa configurazione, quindi provalo: tieni premuto un dito per 150 ms, quindi spostalo in direzione verticale - UIScrollView annidato funziona come previsto!)
Ciò significa che non è possibile utilizzare il codice di UIScrollView per la gestione degli eventi; devi sovrascrivere tutti e quattro i metodi di gestione del tocco in RemorsefulScrollView ed eseguire prima la tua elaborazione, inoltrando l'evento a super
(UIScrollView) solo se hai deciso di utilizzare lo scorrimento orizzontale.
Tuttavia devi passare touchesBegan
a UIScrollView, perché vuoi che ricordi una coordinata di base per lo scorrimento orizzontale futuro (se in seguito decidi che si tratta di uno scorrimento orizzontale). Non potrai inviare touchesBegan
a UIScrollView in un secondo momento, perché non puoi memorizzare l' touches
argomento: contiene oggetti che verranno mutati prima touchesMoved
dell'evento successivo e non puoi riprodurre il vecchio stato.
Quindi devi passare touchesBegan
immediatamente a UIScrollView, ma nasconderai eventuali ulteriori touchesMoved
eventi fino a quando non deciderai di scorrere in orizzontale. No touchesMoved
significa nessuno scorrimento, quindi questa iniziale touchesBegan
non farà male. Ma impostare delaysContentTouches
su NO, in modo che nessun timer sorpresa aggiuntivo interferisca.
(Offtopic: a differenza di te, UIScrollView può memorizzare i tocchi correttamente e può riprodurre e inoltrare l' touchesBegan
evento originale in un secondo momento. Ha un vantaggio ingiusto di utilizzare API non pubblicate, quindi può clonare gli oggetti touch prima che vengano mutati.)
Dato che inoltri sempre touchesBegan
, devi anche inoltrare touchesCancelled
e touchesEnded
. Dovete girare touchesEnded
in touchesCancelled
, però, perché UIScrollView interpreterebbe touchesBegan
, touchesEnded
sequenza come un touch-click, e ci si trasmette alla vista interiore. Stai già inoltrando tu stesso gli eventi appropriati, quindi non vuoi mai che UIScrollView inoltri qualcosa.
Fondamentalmente ecco lo pseudocodice per quello che devi fare. Per semplicità, non consento mai lo scorrimento orizzontale dopo che si è verificato un evento multitouch.
@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
Non ho provato a eseguirlo o addirittura a compilarlo (e ho digitato l'intera classe in un semplice editor di testo), ma puoi iniziare con quanto sopra e, si spera, farlo funzionare.
L'unico problema nascosto che vedo è che se aggiungi visualizzazioni figlio non UIScrollView a RemorsefulScrollView, gli eventi di tocco che inoltri a un bambino potrebbero tornare a te tramite la catena di risponditori, se il bambino non gestisce sempre i tocchi come fa UIScrollView. Un'implementazione di RemorsefulScrollView a prova di proiettile proteggerebbe dal touchesXxx
rientro.
Seconda strategia: se per qualche motivo le UIScrollViews annidate non funzionano o si dimostrano troppo difficili da ottenere, puoi provare ad andare d'accordo con un solo UIScrollView, cambiando la sua pagingEnabled
proprietà al volo dal tuo scrollViewDidScroll
metodo delegato.
Per evitare lo scorrimento diagonale, dovresti prima provare a ricordare contentOffset in scrollViewWillBeginDragging
e controllare e reimpostare contentOffset all'interno scrollViewDidScroll
se rilevi un movimento diagonale. Un'altra strategia da provare è reimpostare contentSize per abilitare solo lo scorrimento in una direzione, una volta deciso in quale direzione sta andando il dito dell'utente. (UIScrollView sembra abbastanza indulgente nel giocherellare con contentSize e contentOffset dai suoi metodi delegati.)
Se anche questo non funziona o produce immagini sciatte, è necessario eseguire l'override touchesBegan
, touchesMoved
ecc. E non inoltrare eventi di movimento diagonale a UIScrollView. (L'esperienza utente non sarà comunque ottimale in questo caso, perché dovrai ignorare i movimenti diagonali invece di forzarli in un'unica direzione. Se ti senti davvero avventuroso, puoi scrivere il tuo sosia di UITouch, qualcosa come RevengeTouch. Obiettivo -C è semplicemente il vecchio C, e non c'è niente di più papero al mondo di C; finché nessuno controlla la vera classe degli oggetti, cosa che credo nessuno faccia, puoi far sembrare qualsiasi classe qualsiasi altra classe. Questo si apre la possibilità di sintetizzare i tocchi che desideri, con le coordinate che desideri.)
Strategia di backup: c'è TTScrollView, una sana reimplementazione di UIScrollView nella libreria Three20 . Sfortunatamente, l'utente sembra molto innaturale e non iphonish. Ma se ogni tentativo di utilizzo di UIScrollView fallisce, puoi tornare a una visualizzazione a scorrimento con codice personalizzato. Lo raccomando se possibile; l'utilizzo di UIScrollView garantisce di ottenere l'aspetto nativo, indipendentemente da come si evolverà nelle future versioni del sistema operativo iPhone.
Ok, questo piccolo saggio è diventato un po 'troppo lungo. Mi piacciono ancora i giochi UIScrollView dopo il lavoro su ScrollingMadness diversi giorni fa.
PS Se riesci a far funzionare qualcuno di questi e hai voglia di condividerli, per favore mandami un e-mail con il codice pertinente a andreyvit@gmail.com, lo includerei volentieri nella mia borsa di trucchi ScrollingMadness .
PPS Aggiungendo questo piccolo saggio a ScrollingMadness README.