UIScrollView paging orizzontale come le schede Mobile Safari


88

Mobile Safari consente di passare da una pagina all'altra inserendo una sorta di visualizzazione a pagine orizzontali di UIScrollView con un controllo di pagina in basso.

Sto cercando di replicare questo particolare comportamento in cui un UIScrollView scorrevole orizzontalmente mostra alcuni dei contenuti della vista successiva.

L'esempio fornito da Apple: PageControl mostra come utilizzare un UIScrollView per il paging orizzontale, ma tutte le visualizzazioni occupano l'intera larghezza dello schermo.

Come ottengo un UIScrollView per mostrare alcuni contenuti della vista successiva come fa Safari mobile?


2
Solo un pensiero ... prova a rendere i limiti della visualizzazione di scorrimento più piccoli dello schermo e giocherella per far sì che le visualizzazioni vengano visualizzate correttamente. (e imposta clipsToBounds della vista di scorrimento su NO)
mjhoy

Volevo avere pagine più grandi della larghezza di uiscrollview (scorrimento orizzontale). E il pensiero di mjhoy mi ha davvero aiutato!
Sasho

Risposte:


263

Un UIScrollViewcon il paging abilitato si fermerà ai multipli della sua larghezza (o altezza) del frame. Quindi il primo passo è capire quanto ampie vuoi che siano le tue pagine. Rendi quella la larghezza del file UIScrollView. Quindi, imposta le dimensioni della tua sottoview per quanto grande ti serve che siano, e imposta i loro centri in base ai multipli della UIScrollViewlarghezza di.

Poi, dal momento che si desidera visualizzare le altre pagine, naturalmente, insieme clipsToBoundsa NOcome mhjoy dichiarato. La parte del trucco ora è farla scorrere quando l'utente inizia il trascinamento al di fuori dell'intervallo del UIScrollViewframe di. La mia soluzione (quando ho dovuto farlo di recente) è stata la seguente:

Crea una UIViewsottoclasse (cioè ClipView) che conterrà le UIScrollViewe le sue sottoviste. Essenzialmente, dovrebbe avere la cornice di ciò che si presume UIScrollViewavrebbe in circostanze normali. Posiziona il UIScrollViewal centro del file ClipView. Assicurati che ClipView's clipsToBoundssia impostato su YESse la sua larghezza è minore di quella della sua vista genitore. Inoltre, ClipViewnecessita di un riferimento a UIScrollView.

Il passaggio finale consiste nell'override - (UIView *)hitTest:withEvent:all'interno di ClipView.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  return [self pointInside:point withEvent:event] ? scrollView : nil;
}

Questo fondamentalmente espande l'area di tocco di UIScrollViewalla cornice della vista del suo genitore, esattamente ciò di cui hai bisogno.

Un'altra opzione potrebbe essere quella di creare una sottoclasse UIScrollViewe sovrascrivere il suo - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventmetodo, tuttavia avrai ancora bisogno di una vista contenitore per eseguire il ritaglio e potrebbe essere difficile determinare quando tornare in YESbase solo al UIScrollViewframe di.

NOTA: Dovresti anche dare un'occhiata a hitTest: withEvent: modification di Juri Pakaste se hai problemi con l'interazione con l'utente della sottoview.


3
Grazie Ed. Sono andato con una UIScrollViewsottoclasse per il caricamento lento delle visualizzazioni (in layoutSubviews) e ho usato a clippingRectper fare il pointInside:withEvent:test. Funziona molto bene e non è richiesta alcuna visualizzazione contenitore aggiuntiva.
ohhorob

1
Bella risposta. Ho usato una UIScrollViewsottoclasse come @ohhorob ma con una vista contenitore per ritaglio e il ritorno [super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];in pointInside:withEvent:. Funziona molto bene e non ci sono problemi con l'interazione dell'utente menzionati nella risposta di @ JuriPakaste.
cduck

1
Wow, questa è davvero un'ottima soluzione. Solo una cosa però, nella mia configurazione, le pagine che sto aggiungendo hanno i pulsanti UIB. Quando sovrascrivo il metodo hitTest, non mi consente di toccare quei pulsanti. Posso scorrere, ma nessuna azione può essere selezionata all'interno di nessuna pagina.
A Salcedo

8
Per coloro che si imbattono in questo più di recente, - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsettieni presente che aggiunto a UIScrollViewDelegate in iOS5 significa che non devi più passare attraverso questo discorso.
Mike Pollard

1
@MikePollard l'effetto non è lo stesso, targetContentOffset non si adatta bene a questa situazione.
Kyle Fang

70

La ClipViewsoluzione sopra ha funzionato per me, ma ho dovuto eseguire -[UIView hitTest:withEvent:]un'implementazione diversa . La versione di Ed Marty non ha ottenuto l'interazione dell'utente lavorando con le visualizzazioni a scorrimento verticale che ho all'interno di quella orizzontale.

La seguente versione ha funzionato per me:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}

Questo aiuta anche a ottenere pulsanti e altre visualizzazioni secondarie con l'interazione dell'utente funzionante. :) grazie
Thomas Clayson

4
Grazie per questo codice, come posso utilizzare questa modifica per ottenere eventi da visualizzazioni secondarie (pulsanti) non contenuti nei confini della visualizzazione a scorrimento? Funziona se un pulsante, ad esempio, si trova entro i confini della scrollview centrale ma non all'esterno.
DreamOfMirrors

4
Questo ha davvero aiutato. Dovrebbe essere incluso come una modifica della risposta selezionata.
Pacu

2
Sì! Questo fa sì che anche l'interazione dell'utente all'interno della scrollview funzioni di nuovo! Ho dovuto impostare userInteractionEnabled su YES su ClipView affinché funzionasse.
Tom van Zummeren

Ho dovuto scorrere self.scrollView.subviews ed eseguire prima hitTest.
Bao Lei

8

Imposta la dimensione del frame di scrollView come sarebbe la dimensione delle tue pagine:

[self.view addSubview:scrollView];
[self.view addGestureRecognizer:mainScrollView.panGestureRecognizer];

Ora puoi eseguire la panoramica self.viewe il contenuto su scrollView verrà fatto scorrere.
Utilizzare anche scrollView.clipsToBounds = NO;per evitare il clipping del contenuto.


5

Ho finito per usare l'UIScrollView personalizzato da solo perché era il metodo più rapido e semplice che mi sembrava. Tuttavia, non ho visto alcun codice esatto, quindi ho pensato di condividerlo. Le mie esigenze erano per un UIScrollView con contenuto ridotto e quindi lo stesso UIScrollView era piccolo per ottenere l'effetto di paging. Come afferma il post, non puoi scorrere. Ma ora puoi.

Creare una classe CustomScrollView e una sottoclasse UIScrollView. Quindi tutto ciò che devi fare è aggiungerlo al file .m:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  return (point.y >= 0 && point.y <= self.frame.size.height);
}

Ciò consente di scorrere da un lato all'altro (orizzontale). Regola i limiti di conseguenza per impostare l'area di sfioramento / scorrimento. Godere!


4

Ho realizzato un'altra implementazione che può restituire automaticamente lo scrollview. Quindi non è necessario disporre di un IBOutlet che limiterà il riutilizzo nel progetto.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self pointInside:point withEvent:event]) {
        for (id view in [self subviews]) {
            if ([view isKindOfClass:[UIScrollView class]]) {
                return view;
            }
        }
    }
    return nil;
}

1
Questo è quello da usare se hai già un xib / storyboard. Altrimenti non sono sicuro di come otterresti il ​​riferimento a Paging / Scrollview. Qualche idea?
mskw

3

Ho un'altra modifica potenzialmente utile per l'implementazione hitTest di ClipView. Non mi piaceva dover fornire un riferimento UIScrollView a ClipView. La mia implementazione di seguito consente di riutilizzare la classe ClipView per espandere l'area hit-test di qualsiasi cosa e non è necessario fornire un riferimento.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (([self pointInside:point withEvent:event]) && (self.subviews.count >= 1))
    {
        // An extended-hit view should only have one sub-view, or make sure the
        // first subview is the one you want to expand the hit test for.
        return [self.subviews objectAtIndex:0];
    }

    return nil;
}

3

Ho implementato il suggerimento sopravvalutato sopra, ma UICollectionViewstavo usando considerava qualsiasi cosa fuori dall'inquadratura come fuori dallo schermo. Ciò ha causato il rendering delle celle vicine solo fuori dai limiti quando l'utente stava scorrendo verso di loro, il che non era l'ideale.

Quello che ho finito per fare è stato emulare il comportamento di una scrollview aggiungendo il metodo seguente al delegato (o UICollectionViewLayout).

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{    
  if (velocity.x > 0) {
    proposedContentOffset.x = ceilf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }
  else {
    proposedContentOffset.x = floorf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }

  return proposedContentOffset;
}

Ciò evita completamente la delega dell'azione di scorrimento, che era anche un bonus. Il UIScrollViewDelegateha un metodo simile chiamata scrollViewWillEndDragging:withVelocity:targetContentOffset:che potrebbe essere utilizzato per UITableViews pagina e UIScrollViews.


2
Dave, stavo lottando per ottenere un simile comportamento usando anche UICollectionView e ho finito per usare lo stesso approccio che descrivi qui. Tuttavia, l'esperienza di scorrimento non è buona quanto lo era una visualizzazione a scorrimento con il paging abilitato. Quando ho strisciato con una velocità maggiore, sono state fatte scorrere diverse pagine e si è fermato sulla pagina proposta. Quello che voglio ottenere è salvare il comportamento come normale visualizzazione a scorrimento con il paging abilitato: qualunque sia la velocità che uso, otterrò solo 1 pagina in più. Hai idea di come farlo?
Peter Stajger

3

Abilita l'attivazione di eventi di tocco sulle visualizzazioni figlio della visualizzazione a scorrimento supportando la tecnica di questa domanda SO. Utilizza un riferimento alla visualizzazione a scorrimento (self.scrollView) per la leggibilità.

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = nil;
    NSArray *childViews = [self.scrollView subviews];
    for (NSUInteger i = 0; i < childViews.count; i++) {
        CGRect childFrame = [[childViews objectAtIndex:i] frame];
        CGRect scrollFrame = self.scrollView.frame;
        CGPoint contentOffset = self.scrollView.contentOffset;
        if (childFrame.origin.x + scrollFrame.origin.x < point.x + contentOffset.x &&
            point.x + contentOffset.x < childFrame.origin.x + scrollFrame.origin.x + childFrame.size.width &&
            childFrame.origin.y + scrollFrame.origin.y < point.y + contentOffset.y &&
            point.y + contentOffset.y < childFrame.origin.y + scrollFrame.origin.y + childFrame.size.height
        ){
            hitView = [childViews objectAtIndex:i];
            return hitView;
        }
    }
    hitView = [super hitTest:point withEvent:event];
    if (hitView == self)
        return self.scrollView;
    return hitView;
}

Aggiungilo alla visualizzazione di tuo figlio per catturare l'evento tocco:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

(Questa è una variazione della soluzione di user1856273. Ripulita per leggibilità e incorporata la correzione di bug di Bartserk. Ho pensato di modificare la risposta di user1856273 ma era una modifica troppo grande da apportare.)


Per fare in modo che il tocco funzioni su un pulsante UIB incorporato nella sottoview, ho sostituito il codice hitView = [childViews objectAtIndex: i]; con // trova il pulsante UIB per (UIView * paneView in [[childViews objectAtIndex: i] subviews]) {if ([paneView isKindOfClass: [UIButton class]]) return paneView; }
CharlesA

2

la mia versione Premendo il pulsante che si trova sulla pergamena - lavoro =)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView* child = nil;
    for (int i=0; i<[[[[self subviews] objectAtIndex:0] subviews] count];i++) {

        CGRect myframe =[[[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i] frame];
        CGRect myframeS =[[[self subviews] objectAtIndex:0] frame];
        CGPoint myscroll =[[[self subviews] objectAtIndex:0] contentOffset];
        if (myframe.origin.x < point.x && point.x < myframe.origin.x+myframe.size.width &&
            myframe.origin.y+myframeS.origin.y < point.y+myscroll.y && point.y+myscroll.y < myframe.origin.y+myframeS.origin.y +myframe.size.height){
            child = [[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i];
            return child;
        }


    }

    child = [super hitTest:point withEvent:event];
    if (child == self)
        return [[self subviews] objectAtIndex:0];
    return child;
    }

ma solo [[self subviews] objectAtIndex: 0] deve essere uno scroll


Questo ha risolto il mio problema :) Nota solo che non stai aggiungendo l'offset di scorrimento a point.x quando controlli i limiti, e questo fa sì che il codice non funzioni bene con gli scroll orizzontali. Dopo averlo aggiunto, ha funzionato perfettamente!
Bartserk

2

Ecco una risposta rapida basata sulla risposta di Ed Marty ma che include anche la modifica di Juri Pakaste per consentire il tocco dei pulsanti ecc. All'interno della visualizzazione a scorrimento.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}

Grazie per l'aggiornamento :)
Liam Bolling
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.