web-dev-qa-db-fra.com

Pagination horizontale UIScrollView comme les onglets Mobile Safari

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éfilant horizontalement montre une partie du contenu de la vue suivante.

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

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

88
firstresponder

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

Ensuite, puisque vous voulez voir les autres pages, bien sûr, définissez clipsToBounds sur NO comme l'a déclaré mhjoy. L'astuce consiste maintenant à le faire défiler lorsque l'utilisateur commence le glissement en dehors de la plage du cadre de UIScrollView. Ma solution (quand j'ai dû le faire très récemment) était la suivante:

Créez une sous-classe UIView (c'est-à-dire ClipView) qui contiendra le UIScrollView et ses sous-vues. Essentiellement, il devrait avoir le cadre de ce que vous supposeriez que le UIScrollView aurait dans des circonstances normales. Placez le UIScrollView au centre du ClipView. Assurez-vous que le ClipView du clipsToBounds est défini sur YES si sa largeur est inférieure à celle de sa vue parent. De plus, le ClipView a besoin d'une référence au UIScrollView.

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

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

Cela étend essentiellement la zone tactile du UIScrollView au cadre de la vue de son parent, exactement ce dont vous avez besoin.

Une autre option serait de sous-classer UIScrollView et de remplacer sa méthode - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) event, mais vous aurez toujours besoin d'une vue de conteneur pour effectuer le découpage, et il peut être difficile de déterminer quand renvoyer YES basé uniquement sur le cadre de UIScrollView.

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

260
Ed Marty

La solution ClipView ci-dessus a fonctionné pour moi, mais j'ai dû faire une autre -[UIView hitTest:withEvent:] la mise en oeuvre. La version d'Ed Marty n'a pas permis à l'utilisateur d'interagir 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;
}
69
Juri Pakaste

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 sur self.view, et le contenu de scrollView défile.
Utiliser aussi scrollView.clipsToBounds = NO; pour empêcher l'écrêtage du contenu.

8

J'ai fini par utiliser moi-même l'UIScrollView personnalisé, car c'était la méthode la plus rapide et la plus simple qui me semblait. Cependant, je n'ai vu aucun code exact, alors j'ai pensé que je partagerais. Mes besoins étaient pour un UIScrollView qui avait un petit contenu et donc l'UIScrollView lui-même était petit pour obtenir l'effet de pagination. Comme l'indique la publication, vous ne pouvez pas glisser à travers. Mais maintenant c'est possible.

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!

5
djneely

J'ai fait une autre implémentation qui peut renvoyer la 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;
}
4
alvinhu

J'ai une autre modification potentiellement utile pour la mise en œuvre 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 hit-test de n'importe quoi, 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;
}
3
Rod Reddekopp

Activez le déclenchement des événements de prise sur les vues enfant 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 plus de 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

(Il s'agit d'une variante de la solution de user1856273. Nettoyée pour plus de lisibilité et correction du bogue de Bartserk. J'ai pensé à modifier la réponse de user1856273 mais c'était un changement trop important à faire.)

3
David James

J'ai implémenté la suggestion de vote positif ci-dessus, mais le UICollectionView que j'utilisais considérait que tout ce qui sortait du cadre était hors de l'écran. Cela a provoqué un rendu hors limites des cellules proches uniquement lorsque l'utilisateur faisait défiler vers elles, ce qui n'était pas idéal.

Ce que j'ai fini par faire était d'é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. UIScrollViewDelegate a une méthode similaire appelée scrollViewWillEndDragging:withVelocity:targetContentOffset: qui pourrait être utilisé pour paginer UITableViews et UIScrollViews.

3
Kyle

ma version Appuyer 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 défilement

2
kolbasek

Voici une réponse Swift basée sur la réponse d'Ed Marty mais incluant également la modification par Juri Pakaste pour permettre des tapotements de boutons, etc. à l'intérieur de 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
}
0
Ben Packard