Horizontales Paging von UIScrollView wie Registerkarten von Mobile Safari

88

Mit Mobile Safari können Sie Seiten wechseln, indem Sie eine Art horizontale UIScrollView-Paging-Ansicht mit einem Seitensteuerelement unten eingeben.

Ich versuche, dieses bestimmte Verhalten zu replizieren, bei dem eine horizontal scrollbare UIScrollView einige Inhalte der nächsten Ansicht anzeigt.

Das von Apple bereitgestellte Beispiel: PageControl zeigt, wie eine UIScrollView für horizontales Paging verwendet wird, aber alle Ansichten nehmen die gesamte Bildschirmbreite ein.

Wie kann ich eine UIScrollView dazu bringen, Inhalte der nächsten Ansicht anzuzeigen, wie dies bei Mobile Safari der Fall ist?

erster Rückmelder
quelle
2
Nur ein Gedanke ... versuchen Sie, die Grenzen der Bildlaufansicht kleiner als der Bildschirm zu machen, und spielen Sie damit, dass die Ansichten richtig angezeigt werden. (und setze die clipsToBounds der Bildlaufansicht auf NO)
mjhoy
Ich wollte Seiten haben, die größer sind als die Breite des Uiscrollview (horizontaler Bildlauf). Und mjhoys Gedanke hat mir tatsächlich geholfen!
Sasho

Antworten:

263

A UIScrollViewmit aktiviertem Paging stoppt bei einem Vielfachen seiner Rahmenbreite (oder -höhe). Der erste Schritt besteht also darin, herauszufinden, wie breit Ihre Seiten sein sollen. Machen Sie das die Breite der UIScrollView. Stellen Sie dann die Größe Ihrer Unteransicht so groß ein, wie Sie sie benötigen, und legen Sie ihre Zentren basierend auf Vielfachen der UIScrollViewBreite der Unteransicht fest .

Dann, da Sie die anderen Seiten sehen möchten, stellen Sie natürlich ein clipsToBounds, NOwie mhjoy angegeben hat. Der Trick besteht nun darin, einen Bildlauf durchzuführen, wenn der Benutzer das Ziehen außerhalb des Bereichs des UIScrollViewRahmens startet . Meine Lösung (als ich dies kürzlich tun musste) war wie folgt:

Erstellen Sie eine UIViewUnterklasse (dh ClipView), die die UIScrollViewund ihre Unteransichten enthält. Im Wesentlichen sollte es den Rahmen haben, den Sie UIScrollViewunter normalen Umständen annehmen würden. Platzieren Sie das UIScrollViewin der Mitte des ClipView. Stellen Sie sicher, dass das ClipView's auf clipsToBoundsgesetzt ist, YESwenn seine Breite kleiner als die der übergeordneten Ansicht ist. Auch das ClipViewbraucht einen Verweis auf die UIScrollView.

Der letzte Schritt ist das Überschreiben - (UIView *)hitTest:withEvent:innerhalb der ClipView.

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

Dies erweitert im Grunde den Berührungsbereich von UIScrollViewauf den Rahmen der Ansicht seiner Eltern, genau das, was Sie brauchen.

Eine andere Möglichkeit wäre, UIScrollViewdie - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventMethode zu unterklassifizieren und zu überschreiben. Sie benötigen jedoch weiterhin eine Containeransicht, um das Abschneiden durchzuführen, und es kann schwierig sein, anhand YESdes UIScrollViewFrames des Frames zu bestimmen, wann zurückgegeben werden soll.

HINWEIS: Sie sollten sich auch Juri Pakaste's hitTest: withEvent: Modifikation ansehen, wenn Sie Probleme mit der Benutzerinteraktion in der Unteransicht haben.

Ed Marty
quelle
3
Danke Ed. Ich ging mit einer UIScrollViewUnterklasse zum verzögerten Laden von Ansichten (in layoutSubviews) und benutzte a clippingRect, um die pointInside:withEvent:Tests durchzuführen. Funktioniert sehr gut und es ist keine zusätzliche Containeransicht erforderlich.
Ohhorob
1
Gute Antwort. Ich habe eine UIScrollViewUnterklasse wie @ohhorob aber mit einem Behälter Ansicht für Clipping und Rückkehr [super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];in pointInside:withEvent:. Dies funktioniert sehr gut und es gibt keine Probleme mit der Benutzerinteraktion, die in der Antwort von @ JuriPakaste erwähnt werden.
Cduck
1
Wow, das ist eine wirklich gute Lösung. Nur eine Sache, in meinem Setup haben die Seiten, die ich hinzufüge, UIButtons. Wenn ich die hitTest-Methode überschreibe, kann ich nicht auf diese Schaltflächen tippen. Ich kann scrollen, aber auf keiner Seite kann eine Aktion ausgewählt werden.
Ein Salcedo
8
Für diejenigen, die in jüngerer Zeit darauf - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetstoßen, ist zu beachten, dass das Hinzufügen zu UIScrollViewDelegate in iOS5 bedeutet, dass Sie diesen Palaver nicht mehr durchlaufen müssen.
Mike Pollard
1
@MikePollard der Effekt ist nicht gleich leise, targetContentOffset passt nicht gut in diese Situation.
Kyle Fang
70

Die ClipViewobige Lösung hat bei mir funktioniert, aber ich musste eine andere -[UIView hitTest:withEvent:]Implementierung durchführen. In der Version von Ed Marty konnte die Benutzerinteraktion nicht mit vertikalen Bildlaufansichten arbeiten, die ich in der horizontalen habe.

Die folgende Version hat bei mir funktioniert:

-(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
quelle
Dies hilft auch dabei, Schaltflächen und andere Unteransichten mit Benutzerinteraktion zum Laufen zu bringen. :) danke
Thomas Clayson
4
Vielen Dank für diesen Code. Wie kann ich diese Änderung verwenden, um Ereignisse aus Unteransichten (Schaltflächen) abzurufen, die nicht in den Grenzen der Bildlaufansicht enthalten sind? Dies funktioniert, wenn sich eine Schaltfläche beispielsweise innerhalb der Grenzen der zentralen Bildlaufansicht befindet, jedoch nicht außerhalb.
DreamOfMirrors
4
Das hat wirklich geholfen. Es sollte als Modifikation der ausgewählten Antwort enthalten sein.
Pacu
2
Ja! Dadurch funktioniert auch die Benutzerinteraktion in der Bildlaufansicht wieder! Ich musste userInteractionEnabled in der ClipView auf YES setzen, damit dies funktioniert.
Tom van Zummeren
Ich musste self.scrollView.subviews durchlaufen und zuerst hitTest durchführen.
Bao Lei
8

Stellen Sie die Rahmengröße von scrollView wie folgt ein:

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

Jetzt können Sie schwenken self.viewund der Inhalt von scrollView wird gescrollt.
Verwenden Sie scrollView.clipsToBounds = NO;diese Option auch , um ein Abschneiden des Inhalts zu verhindern.

Nikolay Tabunchenko
quelle
5

Ich habe mich selbst für das benutzerdefinierte UIScrollView entschieden, da es die schnellste und einfachste Methode war, die mir erschien. Ich habe jedoch keinen genauen Code gesehen, also dachte ich, ich würde ihn teilen. Ich brauchte eine UIScrollView mit kleinem Inhalt, und daher war die UIScrollView selbst klein, um den Paging-Effekt zu erzielen. Wie in der Post angegeben, können Sie nicht darüber streichen. Aber jetzt kannst du.

Erstellen Sie eine Klasse CustomScrollView und eine Unterklasse UIScrollView. Dann müssen Sie dies nur noch in die .m-Datei einfügen:

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

Auf diese Weise können Sie von einer Seite zur anderen (horizontal) scrollen. Passen Sie die Grenzen entsprechend an, um Ihren Wisch- / Bildlauf-Berührungsbereich festzulegen. Genießen!

djneely
quelle
4

Ich habe eine andere Implementierung vorgenommen, die die Bildlaufansicht automatisch zurückgeben kann. Es muss also kein IBOutlet vorhanden sein, das die Wiederverwendung im Projekt einschränkt.

- (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
quelle
1
Dies ist diejenige, die Sie verwenden sollten, wenn Sie bereits ein xib / Storyboard haben. Andernfalls bin ich mir nicht sicher, wie Sie den Verweis auf Paging / Scrollview erhalten würden. Irgendwelche Ideen?
mskw
3

Ich habe eine weitere potenziell nützliche Änderung für die ClipView hitTest-Implementierung. Ich wollte keinen UIScrollView-Verweis auf ClipView bereitstellen. Mit der folgenden Implementierung können Sie die ClipView-Klasse erneut verwenden, um den Hit-Test-Bereich von irgendetwas zu erweitern, ohne ihn mit einer Referenz versehen zu müssen.

- (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
quelle
3

Ich habe den oben genannten Vorschlag umgesetzt, aber das, was UICollectionViewich verwendet habe, hat alles außerhalb des Rahmens als außerhalb des Bildschirms liegend angesehen . Dies führte dazu, dass Zellen in der Nähe nur außerhalb der Grenzen gerendert wurden, wenn der Benutzer auf sie zu scrollte, was nicht ideal war.

Am Ende habe ich das Verhalten einer Bildlaufansicht emuliert, indem ich dem Delegaten (oder UICollectionViewLayout) die folgende Methode hinzugefügt habe .

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

Dies vermeidet die Delegation der Swipe-Aktion vollständig, was ebenfalls ein Bonus war. Das UIScrollViewDelegatehat eine ähnliche Methode, scrollViewWillEndDragging:withVelocity:targetContentOffset:die zum Blättern von UITableViews und UIScrollViews verwendet werden kann.

Kyle
quelle
2
Dave, ich hatte Probleme, ein solches Verhalten auch mit UICollectionView zu erreichen, und am Ende habe ich denselben Ansatz verwendet, den Sie hier beschreiben. Die Bildlauferfahrung ist jedoch nicht so gut wie bei einer Bildlaufansicht mit aktiviertem Paging. Wenn ich mit größerer Geschwindigkeit wischte, wurden mehrere Seiten gescrollt und es wurde auf der vorgeschlagenen Seite angehalten. Was ich erreichen möchte, ist das Speichern des Verhaltens als normale Bildlaufansicht mit aktiviertem Paging. Unabhängig von der Geschwindigkeit, die ich verwende, erhalte ich nur 1 Seite mehr. Hast du eine Idee, wie es geht?
Peter Stajger
3

Aktivieren Sie das Auslösen von Tap-Ereignissen in untergeordneten Ansichten der Bildlaufansicht, während Sie die Technik dieser SO-Frage unterstützen. Verwendet einen Verweis auf die Bildlaufansicht (self.scrollView) zur besseren Lesbarkeit.

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

Fügen Sie dies Ihrer untergeordneten Ansicht hinzu, um das Berührungsereignis zu erfassen:

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

(Dies ist eine Variation der Lösung von user1856273. Aus Gründen der Lesbarkeit bereinigt und mit der Fehlerbehebung von Bartserk versehen. Ich dachte daran, die Antwort von user1856273 zu bearbeiten, aber es war eine zu große Änderung, um sie vorzunehmen.)

David James
quelle
Um das Tippen auf einen in die Unteransicht eingebetteten UIButton zu aktivieren, habe ich den Code hitView = [childViews objectAtIndex: i] ersetzt. mit // finde den UIButton für (UIView * paneView in [[childViews objectAtIndex: i] subviews]) {if ([paneView isKindOfClass: [UIButton class]]) return paneView; }
CharlesA
2

meine Version Drücken Sie die Taste auf der Schriftrolle - Arbeit =)

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

aber nur [[self subviews] objectAtIndex: 0] muss eine Schriftrolle sein

kolbasek
quelle
Dies hat mein Problem gelöst :) Beachten Sie nur, dass Sie den Bildlaufversatz nicht zu point.x hinzufügen, wenn Sie die Grenzen überprüfen. Dadurch funktioniert der Code bei horizontalen Bildläufen nicht gut. Nachdem ich das hinzugefügt hatte, funktionierte es einwandfrei!
Bartserk
2

Hier ist eine schnelle Antwort, die auf Ed Martys Antwort basiert, aber auch die Modifikation von Juri Pakaste enthält, um das Tippen auf Schaltflächen usw. in der Bildlaufansicht zu ermöglichen.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}
Ben Packard
quelle
Vielen Dank für die Aktualisierung :)
Liam Bolling