UIScrollView poziome stronicowanie, takie jak karty Mobile Safari

88

Mobilna przeglądarka Safari umożliwia przełączanie stron przez wprowadzenie pewnego rodzaju poziomego widoku stronicowania UIScrollView z kontrolką strony u dołu.

Próbuję odtworzyć to szczególne zachowanie, w którym UIScrollView przewijany w poziomie pokazuje część zawartości następnego widoku.

Przykład dostarczony przez Apple: PageControl pokazuje, jak używać UIScrollView do stronicowania poziomego, ale wszystkie widoki zajmują całą szerokość ekranu.

Jak uzyskać widok UIScrollView, aby wyświetlał zawartość następnego widoku, tak jak robi to mobilna przeglądarka Safari?

pierwszy korespondent
źródło
2
Tylko myśl ... spróbuj zmniejszyć granice widoku przewijania niż ekran i bawić się, aby widoki były wyświetlane poprawnie. (i ustaw klipy widoku przewijania na NO)
mjhoy
Chciałem, aby strony były większe niż szerokość uiscrollview (przewijanie w poziomie). A myśl mjhoy właściwie mi pomogła!
Sasho

Odpowiedzi:

263

A UIScrollViewz włączonym stronicowaniem zatrzyma się przy wielokrotnościach szerokości (lub wysokości) ramki. Więc pierwszym krokiem jest ustalenie, jak szerokie mają być Twoje strony. Spraw, aby szerokość UIScrollView. Następnie ustaw rozmiary widoku podrzędnego, tak duże, jak potrzebujesz, i ustaw ich środki na podstawie wielokrotności UIScrollViewszerokości.

Następnie, ponieważ chcesz zobaczyć inne strony, oczywiście, zestaw clipsToBoundsdo NOjak mhjoy stwierdził. Sztuczka polega teraz na tym, że przewija się, gdy użytkownik rozpoczyna przeciąganie poza zakresem UIScrollViewramki. Moje rozwiązanie (kiedy musiałem to zrobić bardzo niedawno) było następujące:

Utwórz UIViewpodklasę (tj. ClipView), UIScrollViewKtóra będzie zawierać podglądy i jej podziały. Zasadniczo powinien mieć ramy takie, jakie można by przypuszczać UIScrollVieww normalnych okolicznościach. Umieść UIScrollVieww środku ClipView. Upewnij się, że wartość ClipView's clipsToBoundsjest ustawiona na, YESjeśli jej szerokość jest mniejsza niż szerokość widoku nadrzędnego. Ponadto ClipViewpotrzeby odnoszą się do UIScrollView.

Ostatnim krokiem jest zastąpienie - (UIView *)hitTest:withEvent:wewnątrz pliku ClipView.

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

To w zasadzie rozszerza obszar dotykowy UIScrollViewdo ramy widoku jego rodzica, dokładnie to, czego potrzebujesz.

Inną opcją byłoby utworzenie podklasy UIScrollViewi nadpisanie jej - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventmetody, jednak nadal będziesz potrzebować widoku kontenera, aby wykonać obcinanie, a określenie, kiedy powrócić, może być trudne YEStylko na podstawie UIScrollViewramki.

UWAGA: Powinieneś także przyjrzeć się hitTest Juri Pakaste : withEvent: modyfikacja, jeśli masz problemy z interakcją użytkownika subview .

Ed Marty
źródło
3
Dzięki Ed. Poszedłem z UIScrollViewpodklasą do leniwego ładowania widoków (w layoutSubviews) i użyłem clippingRectdo pointInside:withEvent:testowania. Działa naprawdę dobrze i nie jest wymagany dodatkowy widok kontenera.
ohhorob
1
Świetna odpowiedź. I wykorzystywane UIScrollViewpodklasy jak @ohhorob ale widokiem pojemnika do przycinania i powrotu [super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];w pointInside:withEvent:. Działa to bardzo dobrze i nie ma problemów z interakcją użytkownika wspomnianych w odpowiedzi @ JuriPakaste.
cduck
1
Wow, to naprawdę dobre rozwiązanie. Jednak w mojej konfiguracji strony, które dodam, mają przyciski UIButtons. Kiedy nadpisuję metodę hitTest, nie pozwala mi dotknąć tych przycisków. Mogę przewijać, ale na żadnej stronie nie można wybrać żadnej czynności.
Salcedo
8
Dla tych, którzy napotkali to ostatnio, pamiętaj, że - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetdodanie do UIScrollViewDelegate w iOS5 oznacza, że ​​nie musisz już przechodzić przez ten palaver.
Mike Pollard
1
@MikePollard efekt nie jest cichy tak samo, targetContentOffset nie pasuje dobrze do tej sytuacji.
Kyle Fang,
70

Powyższe ClipViewrozwiązanie działało u mnie, ale musiałem zrobić inną -[UIView hitTest:withEvent:]implementację. Wersja Eda Marty'ego nie miała interakcji użytkownika z pionowymi widokami przewijania, które mam wewnątrz poziomego.

Następująca wersja zadziałała dla mnie:

-(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
źródło
Pomaga to również uzyskać przyciski i inne podglądy podczas interakcji z użytkownikiem. :) dzięki
Thomas Clayson
4
Dziękuję za ten kod, jak mogę użyć tej modyfikacji, aby uzyskać zdarzenia z widoków podrzędnych (przycisków) nie zawartych w granicach widoku przewijania? Działa, jeśli na przykład przycisk znajduje się w granicach centralnego widoku przewijania, ale nie na zewnątrz.
DreamOfMirrors
4
To naprawdę pomogło. Powinien być uwzględniony jako modyfikacja wybranej odpowiedzi.
Pacu
2
Tak! To również sprawia, że ​​interakcja użytkownika w widoku przewijania znów działa! Musiałem ustawić userInteractionEnabled na YES w ClipView, aby to zadziałało.
Tom van Zummeren
Musiałem wykonać iterację przez self.scrollView.subviews i najpierw wykonać hitTest.
Bao Lei
8

Ustaw rozmiar ramki scrollView na następujący rozmiar stron:

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

Teraz możesz przesuwać self.view, a zawartość scrollView będzie przewijana.
Służy również scrollView.clipsToBounds = NO;do zapobiegania przycinaniu zawartości.

Nikolay Tabunchenko
źródło
5

Skończyło się na tym, że sam zdecydowałem się na niestandardowy UIScrollView, ponieważ był to najszybszy i prostszy sposób, jaki mi się wydawał. Jednak nie widziałem żadnego dokładnego kodu, więc pomyślałem, że się nim podzielę. Moje potrzeby dotyczyły UIScrollView, który miał małą zawartość, a zatem sam UIScrollView był mały, aby uzyskać efekt stronicowania. Jak stwierdza post, nie możesz przesuwać palcem. Ale teraz możesz.

Utwórz klasę CustomScrollView i podklasę UIScrollView. Następnie wszystko, co musisz zrobić, to dodać to do pliku .m:

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

Pozwala to na przewijanie z boku na bok (poziomo). Dostosuj odpowiednio granice, aby ustawić obszar dotykowy przesuwania / przewijania. Cieszyć się!

djneely
źródło
4

Zrobiłem kolejną implementację, która może automatycznie zwrócić scrollview. Nie trzeba więc mieć IBOutlet, który ograniczy ponowne wykorzystanie w projekcie.

- (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
źródło
1
To jest ten, którego możesz użyć, jeśli masz już xib / storyboard. W przeciwnym razie nie jestem pewien, jak uzyskać odwołanie do widoku stronicowania / przewijania. Jakieś pomysły?
mskw
3

Mam inną potencjalnie przydatną modyfikację dla implementacji hitTest ClipView. Nie podobało mi się konieczność dostarczania odwołania UIScrollView do ClipView. Poniższa implementacja umożliwia ponowne użycie klasy ClipView w celu rozszerzenia obszaru testów trafień czegokolwiek bez konieczności dostarczania do niego odwołania.

- (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
źródło
3

Zaimplementowałem powyżej przegłosowaną sugestię, ale używałem, aby UICollectionViewwszystko poza ramką znalazło się poza ekranem. Spowodowało to, że pobliskie komórki renderowały się poza granice tylko wtedy, gdy użytkownik przewijał je w ich kierunku, co nie było idealne.

Skończyło się na tym, że emulowałem zachowanie widoku przewijania, dodając poniższą metodę do delegata (lub 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;
}

Pozwala to całkowicie uniknąć delegowania operacji machnięcia, co również było bonusem. UIScrollViewDelegateMa podobną metodę zwaną scrollViewWillEndDragging:withVelocity:targetContentOffset:które mogłyby zostać wykorzystane do UITableViews stron i UIScrollViews.

Kyle
źródło
2
Dave, również starałem się osiągnąć takie zachowanie za pomocą UICollectionView i ostatecznie użyłem tego samego podejścia, które opisałeś tutaj. Jednak przewijanie nie jest tak dobre, jak był to widok przewijania z włączonym stronicowaniem. Kiedy przesunąłem z większą prędkością, przewinęło się kilka stron i zatrzymało się na proponowanej stronie. To, co chcę osiągnąć, to zachowanie zachowania jako normalnego widoku przewijania z włączonym stronicowaniem - niezależnie od szybkości, z której korzystam, uzyskam tylko 1 stronę więcej. Masz pomysł, jak to zrobić?
Peter Stajger,
3

Włącz wyzwalanie zdarzeń dotknięcia w widokach potomnych widoku przewijania, jednocześnie wspierając technikę tego pytania SO. Używa odniesienia do widoku przewijania (self.scrollView) w celu zwiększenia czytelności.

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

Dodaj to do widoku dziecka, aby uchwycić zdarzenie dotykowe:

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

(Jest to odmiana rozwiązania użytkownika 1856273. Oczyszczono ją pod kątem czytelności i uwzględniono poprawkę błędu Bartserka. Myślałem o edytowaniu odpowiedzi użytkownika1856273, ale zmiana była zbyt duża).

David James
źródło
Aby uruchomić stukanie w celu pracy nad UIButtonem osadzonym w podglądzie, zastąpiłem kod hitView = [childViews objectAtIndex: i]; z // znajdź przycisk UIButton for (UIView * paneView w [[childViews objectAtIndex: i] subviews]) {if ([paneView isKindOfClass: [UIButton class]]) return paneView; }
CharlesA
2

moja wersja Naciśnięcie przycisku leżącego na zwoju - praca =)

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

ale tylko [[self subviews] objectAtIndex: 0] musi być zwojem

kolbasek
źródło
To rozwiązało mój problem :) Zwróć uwagę, że nie dodajesz przesunięcia przewijania do point.x, kiedy sprawdzasz granice, a to sprawia, że ​​kod nie działa dobrze na przewijaniach poziomych. Po dodaniu to działało dobrze!
Bartserk
2

Oto szybka odpowiedź oparta na odpowiedzi Eda Marty'ego, ale zawierająca również modyfikację Juri Pakaste, aby umożliwić naciskanie przycisków itp. W widoku przewijania.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}
Ben Packard
źródło
Dzięki za aktualizację tego :)
Liam Bolling