UIScrollView: paging orizzontalmente, scrolling verticalmente?


88

Come posso forzare un UIScrollViewin cui la paginazione e lo scorrimento sono attivi per spostarsi solo verticalmente o orizzontalmente in un dato momento?

La mia comprensione è che il file directionalLockEnabled proprietà dovrebbe raggiungere questo obiettivo, ma uno scorrimento diagonale fa comunque scorrere la vista in diagonale invece di limitare il movimento a un singolo asse.

Modifica: per essere più chiari, vorrei consentire all'utente di scorrere orizzontalmente O verticalmente, ma non entrambi contemporaneamente.


Si desidera limitare lo scorrimento di una vista SOLO in orizzontale / verticale o si desidera che l'utente scorra in orizzontale O in verticale, ma non entrambi contemporaneamente?
Will Hartung

Puoi farmi un favore e cambiare il titolo in modo che appaia un po 'più interessante e non banale? Qualcosa come "UIScrollView: paging orizzontalmente, scrolling verticalmente?"
Andrey Tarantsov

Sfortunatamente, ho pubblicato la domanda come utente non registrato su un computer a cui non ho più accesso (prima di registrarmi con lo stesso nome quando sono tornato a casa), il che significa che non posso modificarlo. Questo dovrebbe anche spiegarmi come seguire più in basso invece di modificare la domanda originale. Ci scusiamo per aver infranto l'etichetta dello Stack questa volta!
Nick

Risposte:


117

Sei in una situazione molto difficile, devo dire.

Si noti che è necessario utilizzare un UIScrollView con pagingEnabled=YESper passare da una pagina all'altra, ma è necessario pagingEnabled=NOscorrere 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, touchesMovedecc. 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, touchesEndede touchesCanceledgli 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 delaysContentTouchessu 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 touchesBegana 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 touchesBegana UIScrollView in un secondo momento, perché non puoi memorizzare l' touchesargomento: contiene oggetti che verranno mutati prima touchesMoveddell'evento successivo e non puoi riprodurre il vecchio stato.

    Quindi devi passare touchesBeganimmediatamente a UIScrollView, ma nasconderai eventuali ulteriori touchesMovedeventi fino a quando non deciderai di scorrere in orizzontale. No touchesMovedsignifica nessuno scorrimento, quindi questa iniziale touchesBegannon farà male. Ma impostare delaysContentTouchessu 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' touchesBeganevento 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 touchesCancellede touchesEnded. Dovete girare touchesEndedin touchesCancelled, però, perché UIScrollView interpreterebbe touchesBegan, touchesEndedsequenza 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.

// 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

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 touchesXxxrientro.

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 pagingEnabledproprietà al volo dal tuo scrollViewDidScrollmetodo delegato.

Per evitare lo scorrimento diagonale, dovresti prima provare a ricordare contentOffset in scrollViewWillBeginDragginge controllare e reimpostare contentOffset all'interno scrollViewDidScrollse 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, touchesMovedecc. 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.


7
Si noti che questo comportamento di scorrimento nidificato funziona immediatamente nell'SDK, non sono necessarie attività divertenti
andygeers

1
Hai fatto +1 sul commento di andygeers, poi si è reso conto che non è accurato; puoi ancora "rompere" il normale UIScrollView trascinando diagonalmente dal punto di partenza e poi hai "tirato lo scroller per perdere" e ignorerà il blocco direzionale fino a quando non lo rilasci e Dio sa dove.
Kalle

Grazie per l'ottima spiegazione dietro le quinte! Sono andato con la soluzione di Mattias Wadman di seguito che sembra un po 'meno invasiva IMO.
Ortwin Gentz

Sarebbe bello vedere questa risposta aggiornata ora che UIScrollView espone il suo riconoscimento dei gesti di pan. (Ma forse questo è al di fuori dell'ambito originale.)
jtbandes

C'è la possibilità che questo post venga aggiornato? Ottimo post e grazie per il tuo contributo.
Pavan

56

Questo funziona per me ogni volta ...

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

20
Per chiunque altro si sia imbattuto in questa domanda: penso che questa risposta sia stata pubblicata prima della modifica che ha chiarito la domanda. Ciò ti consentirà di scorrere solo in orizzontale, non risolve il problema di poter scorrere verticalmente o orizzontalmente ma non in diagonale.
andygeers

Funziona come un fascino per me. Ho una scrollView orizzontale molto lunga con paging e una UIWebView su ogni pagina, che gestisce lo scorrimento verticale di cui ho bisogno.
Tom Redman

Eccellente! Ma qualcuno può spiegare come funziona (impostando l'altezza di contentSize su 1)!
Praveen

Funziona davvero, ma perché e come funziona? Qualcuno può dare un senso a tutto ciò?
Marko Francekovic

27

Per i pigri come me:

Imposta scrollview.directionalLockEnabledsuYES

- (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;
}

16

Ho usato il seguente metodo per risolvere questo problema, spero che ti aiuti.

Definisci prima le seguenti variabili nel file di intestazione del controller.

CGPoint startPos;
int     scrollDirection;

startPos manterrà il valore contentOffset quando il delegato riceve il messaggio scrollViewWillBeginDragging . Quindi in questo metodo lo facciamo;

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

quindi utilizziamo questi valori per determinare la direzione di scorrimento prevista dagli utenti nel messaggio 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];
    }
 }

infine dobbiamo interrompere tutte le operazioni di aggiornamento durante il trascinamento dell'utente;

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

Hmm ... In realtà, dopo ulteriori indagini - funziona abbastanza bene, ma il problema è che perde il controllo della direzione durante la fase di decelerazione - quindi se inizi a muovere il dito in diagonale, sembrerà che si muova solo orizzontalmente o verticalmente finché non lo lasci andare, a quel punto
decelererà in

@andygeers, potrebbe funzionare meglio se - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (decelerate) { scrollDirection=3; } }fosse cambiato in - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (!decelerate) { scrollDirection=0; } }e- (void)scrollViewDidEndDecelerating{ scrollDirection=0; }
cduck

Non ha funzionato per me. L'impostazione del contentOffset in volo sembra distruggere il comportamento di paginazione. Sono andato con la soluzione di Mattias Wadman.
Ortwin Gentz

13

Potrei scavare qui, ma oggi sono incappato in questo post perché stavo cercando di risolvere lo stesso problema. Ecco la mia soluzione, sembra funzionare alla grande.

Desidero visualizzare una schermata piena di contenuti, ma consenti all'utente di scorrere fino a una delle altre 4 schermate, in alto a sinistra ea destra. Ho un unico UIScrollView con dimensioni 320x480 e contentSize di 960x1440. L'offset del contenuto inizia da 320.480. In scrollViewDidEndDecelerating :, ridisegno le 5 visualizzazioni (viste centrali e le 4 intorno) e reimposto l'offset del contenuto a 320,480.

Ecco la carne della mia soluzione, il metodo 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);
    }
}

Probabilmente scoprirai che questo codice non "decelera" bene quando l'utente lascia andare la visualizzazione a scorrimento: si fermerà. Se non vuoi la decelerazione, questo potrebbe andare bene per te.
andygeers

4

Ragazzi, è semplice come rendere l'altezza della sottoview uguale all'altezza della visualizzazione di scorrimento e l'altezza della dimensione del contenuto. Quindi lo scorrimento verticale è disabilitato.


2
Per chiunque altro si sia imbattuto in questa domanda: penso che questa risposta sia stata pubblicata prima della modifica che ha chiarito la domanda. Ciò ti consentirà di scorrere solo in orizzontale, non risolve il problema di poter scorrere verticalmente o orizzontalmente ma non in diagonale.
andygeers

3

Ecco la mia implementazione di una pagina UIScrollViewche consente solo pergamene ortogonali. Assicurarsi di impostare pagingEnableda 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

1
Funziona alla grande, anche su iOS 7. Grazie!
Ortwin Gentz

3

Ho anche dovuto risolvere questo problema e, sebbene la risposta di Andrey Tarantsov abbia sicuramente molte informazioni utili per capire come UIScrollViewsfunziona, ritengo che la soluzione sia un po 'troppo complicata. La mia soluzione, che non prevede alcuna sottoclasse o inoltro di tocco, è la seguente:

  1. Crea due viste a scorrimento nidificate, una per ciascuna direzione.
  2. Crea due manichini UIPanGestureRecognizers , di nuovo uno per ogni direzione.
  3. Crea dipendenze di errore tra le UIScrollViewsproprietà 'panGestureRecognizer e il dummy UIPanGestureRecognizercorrispondente alla direzione perpendicolare alla direzione di scorrimento desiderata di UIScrollView utilizzandoUIGestureRecognizer's -requireGestureRecognizerToFail: selettore.
  4. Nei -gestureRecognizerShouldBegin:callback per il tuo UIPanGestureRecognizers fittizio, calcola la direzione iniziale della panoramica utilizzando il selettore -translationInView: del gesto ( UIPanGestureRecognizersnon chiamerai questo callback delegato finché il tuo tocco non avrà tradotto abbastanza per registrarti come panoramica). Consentire o impedire l'avvio del riconoscimento dei gesti a seconda della direzione calcolata, che a sua volta dovrebbe controllare se il riconoscimento dei gesti di pan UIScrollView perpendicolare potrà iniziare o meno.
  5. In -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:, non consentire al tuo manichino UIPanGestureRecognizersdi funzionare insieme allo scorrimento (a meno che tu non abbia qualche comportamento extra che desideri aggiungere).

2

Quello che ho fatto è stato creare un UIScrollView di paging con una cornice delle dimensioni dello schermo di visualizzazione e impostare la dimensione del contenuto sulla larghezza necessaria per tutti i miei contenuti e un'altezza di 1 (grazie Tonetel). Quindi ho creato le pagine del menu UIScrollView con i frame impostati su ciascuna pagina, la dimensione della schermata di visualizzazione e ho impostato la dimensione del contenuto di ciascuna sulla larghezza dello schermo e l'altezza necessaria per ciascuna. Quindi ho semplicemente aggiunto ciascuna delle pagine del menu alla visualizzazione impaginazione. Assicurati che la paginazione sia abilitata nella visualizzazione a scorrimento delle pagine ma disabilitata nelle viste del menu (dovrebbe essere disabilitata per impostazione predefinita) e dovresti essere a posto.

La visualizzazione dello scorrimento di paging ora scorre orizzontalmente, ma non verticalmente a causa dell'altezza del contenuto. Ogni pagina scorre verticalmente ma non orizzontalmente a causa dei vincoli di dimensione del contenuto.

Ecco il codice se quella spiegazione lasciava a desiderare:

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;  
}           

2

Grazie Tonetel. Ho leggermente modificato il tuo approccio, ma era esattamente ciò di cui avevo bisogno per evitare lo scorrimento orizzontale.

self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 1);

2

Questa è una soluzione migliorata di icompot, che era abbastanza instabile per me. Questo funziona bene ed è facile da implementare:

- (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;
}

2

Per riferimento futuro, mi sono basato sulla risposta di Andrey Tanasov e ho trovato la seguente soluzione che funziona bene.

Definire prima una variabile per memorizzare il contentOffset e impostarlo su coordinate 0,0

var positionScroll = CGPointMake(0, 0)

Ora implementa quanto segue nel delegato 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
        }

    }

}

Spero che sia di aiuto.


1

Dai documenti:

"il valore predefinito è NO, il che significa che lo scorrimento è consentito sia in direzione orizzontale che verticale. Se il valore è YES e l'utente inizia a trascinare in una direzione generale (orizzontale o verticale), la visualizzazione a scorrimento disabilita lo scorrimento nell'altra direzione. "

Penso che la parte importante sia "se ... l'utente inizia trascinare in una direzione generale". Quindi, se iniziano a trascinare in diagonale, ciò non si attiva. Non che questi documenti siano sempre affidabili quando si tratta di interpretazione, ma sembra che si adatti a ciò che stai vedendo.

Questo sembra davvero sensato. Devo chiederti: perché vuoi limitare solo in orizzontale o solo in verticale in qualsiasi momento? Forse UIScrollView non è lo strumento che fa per te?


1

La mia vista è composta da 10 viste orizzontali e alcune di queste viste sono costituite da più viste verticali, quindi otterrai qualcosa del genere:


1.0   2.0   3.1    4.1
      2.1   3.1
      2.2
      2.3

Con impaginazione abilitata sia sull'asse orizzontale che su quello verticale

La vista ha anche i seguenti attributi:

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

Ora per evitare lo scorrimento diagonale fai questo:

-(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];
    } 
}

Funziona come un fascino per me!


1
non capisci come potrebbe funzionare per te ... potresti caricare un progetto di esempio?
hfossli

1

È necessario reimpostare _isHorizontalScrollsu NO in touchesEndede touchesCancelled.



0

Se non hai provato a farlo nella versione 3.0 beta, ti consiglio di provarlo. Attualmente sto utilizzando una serie di visualizzazioni tabella all'interno di una visualizzazione a scorrimento e funziona come previsto. (Beh, lo scorrimento lo fa, comunque. Ho trovato questa domanda mentre cercavo una soluzione a un problema completamente diverso ...)


0

Per coloro che cercano in Swift la seconda soluzione @AndreyTarantsov è così nel codice:

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

quello che stiamo facendo qui è mantenere la posizione corrente appena prima che scrollview venga trascinato e quindi controllare se x è aumentata o diminuita rispetto alla posizione corrente di x. Se sì, allora imposta pagingEnabled su true, se no (y è aumentato / diminuito) quindi imposta pagingEnabled su false

Spero che sia utile ai nuovi arrivati!


0

Sono riuscito a risolverlo implementando scrollViewDidBeginDragging (_ :) e osservando la velocità sul sottostante UIPanGestureRecognizer.

NB: Con questa soluzione, ogni panoramica che sarebbe stata diagonale verrà ignorata dallo 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
    }
}

-3

Se stai usando il generatore di interfacce per progettare la tua interfaccia, questo potrebbe essere fatto abbastanza facilmente.

In Interface Builder è sufficiente fare clic su UIScrollView e selezionare l'opzione (nell'ispettore) che lo limita allo scorrimento unidirezionale.


1
Non ci sono opzioni in IB per farlo. Le due opzioni a cui ti riferisci hanno a che fare con la visualizzazione della barra di scorrimento, non con lo scorrimento effettivo. Inoltre, il blocco della direzione blocca la direzione solo una volta che lo scorrimento è iniziato.
Sophtware
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.