UIScrollView pagination horizontale comme les onglets Mobile Safari

88

Mobile Safari vous permet de changer de page en entrant une sorte de vue de pagination horizontale UIScrollView avec un contrôle de page en bas.

J'essaie de reproduire ce comportement particulier où un UIScrollView à défilement horizontal montre une partie du contenu de la vue suivante.

L'exemple fourni par Apple: PageControl montre comment utiliser un UIScrollView pour la pagination horizontale, mais toutes les vues occupent toute la largeur de l'écran.

Comment obtenir un UIScrollView pour afficher du contenu de la vue suivante comme le fait Safari mobile?

premier répondeur
la source
2
Juste une pensée ... essayez de rendre les limites de la vue de défilement plus petites que l'écran et essayez de faire en sorte que les vues s'affichent correctement. (et définissez les clipsToBounds de la vue de défilement sur NON)
mjhoy
Je voulais avoir des pages plus grandes que la largeur de uiscrollview (défilement horizontal). Et la pensée de mjhoy m'a vraiment aidé!
Sasho

Réponses:

263

Un UIScrollViewavec la pagination activée s'arrêtera à des multiples de sa largeur (ou hauteur) de cadre. La première étape consiste donc à déterminer la largeur souhaitée de vos pages. Faites que la largeur du UIScrollView. Ensuite, définissez les tailles de votre sous-vue, quelle que soit la taille dont vous avez besoin, et définissez leurs centres en fonction de multiples de la UIScrollViewlargeur de.

Ensuite, puisque vous voulez voir les autres pages, bien sûr, mis clipsToBoundsà NOcomme mhjoy déclaré. La partie astuce consiste maintenant à le faire défiler lorsque l'utilisateur commence le glissement en dehors de la plage du UIScrollViewcadre de. Ma solution (lorsque j'ai dû le faire très récemment) était la suivante:

Créez une UIViewsous - classe (c'est-à-dire ClipView) qui contiendra les UIScrollViewet ses sous-vues. Essentiellement, il devrait avoir le cadre de ce que vous supposeriez UIScrollViewqu'il aurait dans des circonstances normales. Placez le UIScrollViewau centre du ClipView. Assurez-vous que le ClipView's clipsToBoundsest défini sur YESsi sa largeur est inférieure à celle de sa vue parent. En outre, le ClipViewnécessite une référence au UIScrollView.

La dernière étape consiste à remplacer - (UIView *)hitTest:withEvent:à l'intérieur du fichier ClipView.

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

Cela élargit fondamentalement la zone tactile de la UIScrollViewau cadre de la vue de son parent, exactement ce dont vous avez besoin.

Une autre option serait de sous UIScrollView-classer et de remplacer sa - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventméthode, mais vous aurez toujours besoin d'une vue de conteneur pour effectuer le découpage, et il peut être difficile de déterminer quand retourner en YESfonction uniquement du UIScrollViewcadre de.

REMARQUE: vous devriez également jeter un oeil à hitTest: withEvent: modification de Juri Pakaste si vous rencontrez des problèmes avec l'interaction de l'utilisateur de sous-vue.

Ed Marty
la source
3
Merci Ed. Je suis allé avec une UIScrollViewsous - classe pour le chargement paresseux des vues (in layoutSubviews), et utilisé a clippingRectpour faire les pointInside:withEvent:tests. Fonctionne très bien et aucune vue de conteneur supplémentaire n'est requise.
ohhorob
1
Très bonne réponse. J'ai utilisé une UIScrollViewsous - classe comme @ohhorob mais avec une vue de conteneur pour écrêtage et le retour [super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];en pointInside:withEvent:. Cela fonctionne très bien et il n'y a aucun problème d'interaction avec l'utilisateur mentionné dans la réponse de @ JuriPakaste.
cduck le
1
Wow, c'est une très bonne solution. Juste une chose cependant, sur ma configuration, les pages que j'ajoute ont des UIButtons. Lorsque je remplace la méthode hitTest, cela ne me laisse pas appuyer sur ces boutons. Je peux faire défiler, mais aucune action ne peut être sélectionnée à l'intérieur d'une page.
A Salcedo le
8
Pour ceux qui rencontrent cela plus récemment, sachez que l' - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetajout de UIScrollViewDelegate dans iOS5 signifie que vous n'avez plus à passer par cette palabre.
Mike Pollard
1
@MikePollard l'effet n'est pas le même, targetContentOffset ne correspond pas bien à cette situation.
Kyle Fang
70

La ClipViewsolution ci-dessus a fonctionné pour moi, mais j'ai dû faire une -[UIView hitTest:withEvent:]implémentation différente . La version d'Ed Marty ne fonctionnait pas avec les vues de défilement verticales que j'ai à l'intérieur de l'horizontale.

La version suivante a fonctionné pour moi:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}
Juri Pakaste
la source
Cela permet également d'obtenir des boutons et d'autres sous-vues avec l'interaction de l'utilisateur. :) merci
Thomas Clayson
4
Merci pour ce code, comment puis-je utiliser cette modification pour obtenir des événements à partir de sous-vues (boutons) non contenues dans les limites de scrollview? Cela fonctionne si un bouton, par exemple, se trouve dans les limites de la vue de défilement centrale mais pas à l'extérieur.
DreamOfMirrors
4
Cela a vraiment aidé. Il doit être inclus en tant que modification de la réponse sélectionnée.
Pacu
2
Oui! Cela permet également à l'utilisateur d'interagir avec le scrollview fonctionner à nouveau! J'ai dû définir userInteractionEnabled sur YES sur le ClipView pour que cela fonctionne.
Tom van Zummeren
J'ai dû parcourir self.scrollView.subviews et effectuer d'abord hitTest.
Bao Lei
8

Définissez la taille du cadre de scrollView comme la taille de vos pages serait:

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

Vous pouvez maintenant effectuer un panoramique self.viewet le contenu de scrollView défilera.
Utilisez également scrollView.clipsToBounds = NO;pour éviter de couper le contenu.

Nikolay Tabunchenko
la source
5

J'ai fini par utiliser le UIScrollView personnalisé moi-même car c'était la méthode la plus rapide et la plus simple qui me semblait. Cependant, je n'ai vu aucun code exact, donc je pensais partager. Mes besoins étaient pour un UIScrollView qui avait un petit contenu et donc le UIScrollView lui-même était petit pour obtenir l'effet de pagination. Comme le message l'indique, vous ne pouvez pas faire glisser votre doigt. Mais maintenant vous pouvez.

Créez une classe CustomScrollView et une sous-classe UIScrollView. Ensuite, tout ce que vous avez à faire est d'ajouter ceci dans le fichier .m:

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

Cela vous permet de faire défiler d'un côté à l'autre (horizontal). Ajustez les limites en conséquence pour définir votre zone tactile de balayage / défilement. Prendre plaisir!

djneely
la source
4

J'ai fait une autre implémentation qui peut renvoyer le scrollview automatiquement. Il n'est donc pas nécessaire d'avoir un IBOutlet qui limitera la réutilisation dans le projet.

- (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;
}
alvinhu
la source
1
C'est celui à utiliser si vous avez déjà un xib / storyboard. Sinon, je ne sais pas comment vous obtiendriez la référence à la pagination / défilement. Des idées?
mskw
3

J'ai une autre modification potentiellement utile pour l'implémentation de ClipView hitTest. Je n'aimais pas avoir à fournir une référence UIScrollView au ClipView. Mon implémentation ci-dessous vous permet de réutiliser la classe ClipView pour étendre la zone de test de succès de quoi que ce soit, sans avoir à lui fournir une référence.

- (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;
}
Rod Reddekopp
la source
3

J'ai implémenté la suggestion de vote positif ci-dessus, mais UICollectionViewj'utilisais tout ce qui sortait du cadre de l'écran. Cela faisait que les cellules voisines n'étaient rendues hors limites que lorsque l'utilisateur faisait défiler vers elles, ce qui n'était pas idéal.

Ce que j'ai fini par faire, c'est émuler le comportement d'un scrollview en ajoutant la méthode ci-dessous au délégué (ou 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;
}

Cela évite entièrement la délégation de l'action de balayage, ce qui était également un bonus. Le UIScrollViewDelegatea une méthode similaire appelée scrollViewWillEndDragging:withVelocity:targetContentOffset:qui pourrait être utilisée pour paginer UITableViews et UIScrollViews.

Kyle
la source
2
Dave, j'avais également du mal à obtenir un tel comportement en utilisant UICollectionView et j'ai fini par utiliser la même approche que vous décrivez ici. Cependant, l'expérience de défilement n'est pas aussi bonne qu'il s'agissait d'une vue de défilement avec pagination activée. Lorsque j'ai glissé avec une plus grande vitesse, plusieurs pages ont été défilées et cela s'est arrêté sur la page proposée. Ce que je veux, c'est enregistrer le comportement en tant que défilement normal avec la pagination activée - quelle que soit la vitesse que j'utilise, je n'obtiendrai qu'une page de plus. Avez-vous une idée de la façon de procéder?
Peter Stajger
3

Activez le déclenchement d'événements de tap sur les vues enfants de la vue de défilement tout en prenant en charge la technique de cette question SO. Utilise une référence à la vue de défilement (self.scrollView) pour la lisibilité.

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

Ajoutez ceci à votre vue enfant pour capturer l'événement tactile:

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

(Ceci est une variante de la solution de user1856273. Nettoyé pour plus de lisibilité et incorporé le correctif de bogue de Bartserk. J'ai pensé à modifier la réponse de user1856273 mais c'était un changement trop important à faire.)

David James
la source
Pour que le tapotage fonctionne sur un UIButton intégré dans la sous-vue, j'ai remplacé le code hitView = [childViews objectAtIndex: i]; avec // trouvez l'UIButton pour (UIView * paneView dans [[childViews objectAtIndex: i] sous-vues]) {if ([paneView isKindOfClass: [UIButton class]]) return paneView; }
CharlesA
2

ma version En appuyant sur le bouton posé sur le parchemin - travail =)

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

mais seul [[self subviews] objectAtIndex: 0] doit être un scroll

kolbasek
la source
Cela a résolu mon problème :) Notez simplement que vous n'ajoutez pas le décalage de défilement à point.x lorsque vous vérifiez les limites, et cela rend le code ne fonctionne pas bien sur les défilés horizontaux. Après avoir ajouté cela, cela a très bien fonctionné!
Bartserk
2

Voici une réponse Swift basée sur la réponse d'Ed Marty, mais incluant également la modification par Juri Pakaste pour permettre les tapotements de boutons, etc. dans la vue de défilement.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}
Ben Packard
la source
Merci d'avoir mis à jour ceci :)
Liam Bolling