UIScrollView: stronicowanie w poziomie, przewijanie w pionie?

88

Jak mogę zmusić stronę, UIScrollVieww której stronicowanie i przewijanie są włączone, w danym momencie poruszają się tylko w pionie lub w poziomie?

Rozumiem, że directionalLockEnabledwłaściwość powinna to osiągnąć, ale ukośne przesunięcie nadal powoduje przewijanie widoku po przekątnej zamiast ograniczania ruchu do pojedynczej osi.

Edycja: aby było jaśniej, chciałbym umożliwić użytkownikowi przewijanie w poziomie LUB w pionie, ale nie w obu jednocześnie.

Cœur
źródło
Czy chcesz ograniczyć przewijanie widoku TYLKO w poziomie / pionie, czy też chcesz, aby użytkownik przewijał widok w poziomie lub pionie, ale nie w obu jednocześnie?
Will Hartung
Czy możesz wyświadczyć mi przysługę i zmienić tytuł, aby wyglądał na nieco ciekawszy i nietrywialniejszy? Coś w rodzaju „UIScrollView: stronicowanie w poziomie, przewijanie w pionie?”
Andrey Tarantsov
Niestety, wysłałem pytanie jako niezarejestrowany użytkownik na komputerze, do którego nie mam już dostępu (przed zarejestrowaniem się pod tym samym nazwiskiem po powrocie do domu), co oznacza, że ​​nie mogę go edytować. To powinno również wyjaśnić mi kontynuację niżej, zamiast edytować oryginalne pytanie. Przepraszamy za złamanie etykiety stosu tym razem!
Nick

Odpowiedzi:

117

Muszę przyznać, że jesteś w bardzo trudnej sytuacji.

Zauważ, że musisz użyć UIScrollView z, pagingEnabled=YESaby przełączać się między stronami, ale musisz pagingEnabled=NOprzewijać w pionie.

Istnieją 2 możliwe strategie. Nie wiem, który będzie działał / jest łatwiejszy do wdrożenia, więc wypróbuj oba.

Po pierwsze: zagnieżdżone UIScrollViews. Szczerze mówiąc, nie widziałem jeszcze osoby, która to zrobiła. Jednak osobiście nie starałem się wystarczająco mocno, a moja praktyka pokazuje, że jeśli wystarczająco się postarasz , możesz sprawić, że UIScrollView zrobi wszystko, co chcesz .

Strategia polega więc na tym, aby zewnętrzny widok przewijania obsługiwał tylko przewijanie w poziomie, a wewnętrzne widoki przewijania tylko w pionie. Aby to osiągnąć, musisz wiedzieć, jak UIScrollView działa wewnętrznie. Zastępuje hitTestmetodę i zawsze zwraca się, więc wszystkie zdarzenia dotyku trafiają do UIScrollView. Następnie wewnątrz touchesBegan, touchesMovedczeki itd to czy jest zainteresowany w wypadku i albo uchwyty lub przekazuje je do wewnętrznych komponentów.

Aby zdecydować, czy dotknięcie ma być obsługiwane, czy przekazywane dalej, UIScrollView uruchamia licznik czasu przy pierwszym dotknięciu:

  • Jeśli nie wykonałeś znacznego ruchu palcem w ciągu 150 ms, zdarzenie to przechodzi do widoku wewnętrznego.

  • Jeśli znacznie przesunąłeś palec w ciągu 150 ms, zaczyna on przewijać (i nigdy nie przekazuje zdarzenia do widoku wewnętrznego).

    Zwróć uwagę, że po dotknięciu tabeli (która jest podklasą widoku przewijania) i natychmiastowym rozpoczęciu przewijania, dotknięty wiersz nigdy nie jest podświetlony.

  • Jeśli nie przesunąłeś palcem znacząco w ciągu 150 ms i UIScrollView zaczął przekazywać zdarzenia do widoku wewnętrznego, ale następnie przesunąłeś palec na tyle daleko, aby rozpocząć przewijanie, UIScrollView wywołuje touchesCancelledwidok wewnętrzny i rozpoczyna przewijanie.

    Zwróć uwagę, że po dotknięciu stołu, przytrzymaniu lekko palca, a następnie rozpoczęciu przewijania, dotknięty wiersz jest najpierw podświetlany, ale później usuwany.

Poniższą sekwencję zdarzeń można zmienić, konfigurując UIScrollView:

  • Jeśli delaysContentTouchesjest NIE, to żaden timer nie jest używany - zdarzenia natychmiast przechodzą do wewnętrznej kontroli (ale potem są anulowane, jeśli przesuniesz palcem wystarczająco daleko)
  • Jeśli cancelsTouchesjest NIE, to po przesłaniu zdarzeń do kontrolki przewijanie nigdy nie nastąpi.

Należy pamiętać, że jest to UIScrollView, który odbiera wszystko touchesBegin , touchesMoved, touchesEndeda touchesCanceledwydarzenia z Cocoa Touch (bo jego hitTestmówi to, aby to zrobić). Następnie przekazuje je do widoku wewnętrznego, jeśli chce, tak długo, jak chce.

Teraz, gdy wiesz już wszystko o UIScrollView, możesz zmienić jego zachowanie. Mogę się założyć, że preferujesz przewijanie w pionie, tak aby po dotknięciu widoku przez użytkownika i rozpoczęciu (nawet nieznacznego) ruchu palcem widok zaczął przewijać się w kierunku pionowym; ale gdy użytkownik przesunie palec wystarczająco daleko w kierunku poziomym, chcesz anulować przewijanie w pionie i rozpocząć przewijanie w poziomie.

Chcesz podklasować swoją zewnętrzną klasę UIScrollView (powiedzmy, nazywasz swoją klasę RemorsefulScrollView), aby zamiast domyślnego zachowania natychmiast przekazywała wszystkie zdarzenia do widoku wewnętrznego i tylko po wykryciu znacznego ruchu poziomego przewijała.

Jak sprawić, by RemorsefulScrollView zachowywał się w ten sposób?

  • Wygląda na to, że wyłączenie przewijania w pionie i ustawienie delaysContentTouchesna NIE powinno sprawić, że zagnieżdżone UIScrollViews będą działać. Niestety tak nie jest; Wydaje się, że UIScrollView wykonuje dodatkowe filtrowanie dla szybkich ruchów (których nie można wyłączyć), więc nawet jeśli UIScrollView można przewijać tylko w poziomie, zawsze pochłania (i ignoruje) wystarczająco szybkie ruchy pionowe.

    Efekt jest tak poważny, że przewijanie w pionie wewnątrz zagnieżdżonego widoku przewijania jest bezużyteczne. (Wygląda na to, że masz dokładnie taką konfigurację, więc spróbuj: przytrzymaj palec przez 150 ms, a następnie przesuń go w kierunku pionowym - zagnieżdżony UIScrollView działa wtedy zgodnie z oczekiwaniami!)

  • Oznacza to, że nie możesz używać kodu UIScrollView do obsługi zdarzeń; musisz zastąpić wszystkie cztery metody obsługi dotyku w RemorsefulScrollView i najpierw wykonać własne przetwarzanie, przekazując zdarzenie tylko do super(UIScrollView), jeśli zdecydowałeś się na przewijanie w poziomie.

  • Musisz jednak przejść touchesBegando UIScrollView, ponieważ chcesz, aby zapamiętał współrzędną podstawową do przyszłego przewijania w poziomie (jeśli później zdecydujesz, że jest to przewijanie w poziomie). Nie będziesz mógł touchesBeganpóźniej wysłać do UIScrollView, ponieważ nie możesz przechowywać touchesargumentu: zawiera obiekty, które zostaną zmutowane przed następnym touchesMovedzdarzeniem i nie możesz odtworzyć starego stanu.

    Musisz więc natychmiast przejść touchesBegando UIScrollView, ale będziesz ukrywać wszelkie dalsze touchesMovedzdarzenia, dopóki nie zdecydujesz się przewijać w poziomie. Nie touchesMovedoznacza braku przewijania, więc ten inicjał touchesBegannie zaszkodzi. Ale ustaw delaysContentTouchesna NIE, aby żadne dodatkowe zegary niespodzianki nie przeszkadzały.

    (Offtopic - w przeciwieństwie do Ciebie, UIScrollView może prawidłowo przechowywać dotknięcia i może touchesBeganpóźniej odtworzyć i przekazać oryginalne zdarzenie. Ma nieuczciwą przewagę w postaci używania niepublikowanych interfejsów API, więc może klonować obiekty dotykowe przed ich mutacją).

  • Biorąc pod uwagę, że zawsze do przodu touchesBegan, musisz także przekazywać touchesCancelledi touchesEnded. Trzeba skręcić touchesEndedw touchesCancelledjednak, ponieważ UIScrollView zinterpretuje touchesBegan, touchesEndedsekwencję jako dotykowym przyciskiem myszy, a będzie przesyła je do wewnętrznej widzenia. Już sam przekazujesz właściwe zdarzenia, więc nigdy nie chcesz, aby UIScrollView niczego przekazywał.

Zasadniczo tutaj jest pseudokod określający to, co musisz zrobić. Dla uproszczenia nigdy nie zezwalam na przewijanie w poziomie po wystąpieniu zdarzenia multitouch.

// RemorsefulScrollView.h

@interface RemorsefulScrollView : UIScrollView {
  CGPoint _originalPoint;
  BOOL _isHorizontalScroll, _isMultitouch;
  UIView *_currentChild;
}
@end

// RemorsefulScrollView.m

// the numbers from an example in Apple docs, may need to tune them
#define kThresholdX 12.0f
#define kThresholdY 4.0f

@implementation RemorsefulScrollView

- (id)initWithFrame:(CGRect)frame {
  if (self = [super initWithFrame:frame]) {
    self.delaysContentTouches = NO;
  }
  return self;
}

- (id)initWithCoder:(NSCoder *)coder {
  if (self = [super initWithCoder:coder]) {
    self.delaysContentTouches = NO;
  }
  return self;
}

- (UIView *)honestHitTest:(CGPoint)point withEvent:(UIEvent *)event {
  UIView *result = nil;
  for (UIView *child in self.subviews)
    if ([child pointInside:point withEvent:event])
      if ((result = [child hitTest:point withEvent:event]) != nil)
        break;
  return result;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event]; // always forward touchesBegan -- there's no way to forward it later
    if (_isHorizontalScroll)
      return; // UIScrollView is in charge now
    if ([touches count] == [[event touchesForView:self] count]) { // initial touch
      _originalPoint = [[touches anyObject] locationInView:self];
    _currentChild = [self honestHitTest:_originalPoint withEvent:event];
    _isMultitouch = NO;
    }
  _isMultitouch |= ([[event touchesForView:self] count] > 1);
  [_currentChild touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  if (!_isHorizontalScroll && !_isMultitouch) {
    CGPoint point = [[touches anyObject] locationInView:self];
    if (fabsf(_originalPoint.x - point.x) > kThresholdX && fabsf(_originalPoint.y - point.y) < kThresholdY) {
      _isHorizontalScroll = YES;
      [_currentChild touchesCancelled:[event touchesForView:self] withEvent:event]
    }
  }
  if (_isHorizontalScroll)
    [super touchesMoved:touches withEvent:event]; // UIScrollView only kicks in on horizontal scroll
  else
    [_currentChild touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  if (_isHorizontalScroll)
    [super touchesEnded:touches withEvent:event];
    else {
    [super touchesCancelled:touches withEvent:event];
    [_currentChild touchesEnded:touches withEvent:event];
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
  [super touchesCancelled:touches withEvent:event];
  if (!_isHorizontalScroll)
    [_currentChild touchesCancelled:touches withEvent:event];
}

@end

Nie próbowałem tego uruchamiać ani nawet kompilować (i napisałem całą klasę w zwykłym edytorze tekstowym), ale możesz zacząć od powyższego i miejmy nadzieję, że zadziała.

Jedynym ukrytym haczykiem, jaki widzę, jest to, że jeśli dodasz jakiekolwiek widoki podrzędne inne niż UIScrollView do RemorsefulScrollView, zdarzenia dotykowe, które przekazujesz do dziecka, mogą wrócić do Ciebie za pośrednictwem łańcucha odpowiedzi, jeśli dziecko nie zawsze obsługuje dotknięcia, tak jak robi to UIScrollView. Kuloodporna implementacja RemorsefulScrollView chroniłaby przed touchesXxxponownym wejściem .

Druga strategia: jeśli z jakiegoś powodu zagnieżdżone UIScrollViews nie działają lub okazują się zbyt trudne do poprawienia, możesz spróbować dogadać się tylko z jednym UIScrollView, przełączając jego pagingEnabledwłaściwość w locie z scrollViewDidScrollmetody delegata.

Aby zapobiec przewijaniu po przekątnej, należy najpierw spróbować zapamiętać zawartość przesunięcia scrollViewWillBeginDraggingoraz sprawdzić i zresetować zawartość przesunięcia wewnątrz w scrollViewDidScrollprzypadku wykrycia ruchu po przekątnej. Inną strategią do wypróbowania jest zresetowanie contentSize, aby umożliwić przewijanie tylko w jednym kierunku, gdy zdecydujesz, w którym kierunku zmierza palec użytkownika. (UIScrollView wydaje się dość wybaczający, jeśli chodzi o manipulowanie przy contentSize i contentOffset z jego metod delegata).

Jeśli to nie działa albo czy rezultaty w niechlujstwa wizualizacje, trzeba ręcznym touchesBegan, touchesMovedetc, a nie do przodu ukośne zdarzenia ruchu do UIScrollView. (W tym przypadku wrażenia użytkownika będą jednak nieoptymalne, ponieważ będziesz musiał zignorować ruchy po przekątnej zamiast wymuszać je w jednym kierunku. Jeśli czujesz się naprawdę odważny, możesz napisać własny lookalike UITouch, coś w rodzaju RevengeTouch. Cel -C to zwykłe stare C i nie ma na świecie nic bardziej dukktypektywnego niż C; tak długo, jak nikt nie sprawdza prawdziwej klasy obiektów, co, jak sądzę, nikt nie robi, możesz sprawić, że każda klasa będzie wyglądać jak każda inna. możliwość syntezy dowolnych akcentów, z dowolnymi współrzędnymi).

Strategia tworzenia kopii zapasowych: istnieje TTScrollView, rozsądna reimplementacja UIScrollView w bibliotece Three20 . Niestety dla użytkownika wydaje się to bardzo nienaturalne i non-ifonish. Ale jeśli każda próba użycia UIScrollView nie powiedzie się, możesz wrócić do niestandardowego widoku przewijania. Odradzam, jeśli to w ogóle możliwe; Korzystanie z UIScrollView zapewnia natywny wygląd i zachowanie, niezależnie od tego, jak ewoluuje w przyszłych wersjach iPhone OS.

Okej, ten krótki esej stał się trochę za długi. Po kilku dniach pracy nad ScrollingMadness nadal interesuję się grami UIScrollView.

PS Jeśli któryś z tych elementów działa i masz ochotę się nim podzielić, wyślij mi e-mail z odpowiednim kodem na adres [email protected], z radością dołączę go do mojej torby ze sztuczkami ScrollingMadness .

PPS Dodanie tego małego eseju do pliku README ScrollingMadness.

Andrey Tarantsov
źródło
7
Zwróć uwagę, że to zagnieżdżone zachowanie scrollview działa teraz od razu w SDK, nie jest potrzebny żaden zabawny biznes
andygeers
1
Dał +1 komentarz Andygeers, a potem zdał sobie sprawę, że nie jest dokładny; nadal możesz „złamać” zwykły widok UIScrollView, przeciągając po przekątnej od punktu początkowego, a następnie „wyciągnąłeś przewijak tracąc” i będzie ignorował blokadę kierunku, dopóki nie zwolnisz i nie będzie wiadomo gdzie.
Kalle,
Dzięki za świetne wyjaśnienie zza kulis! Poszedłem z rozwiązaniem Mattiasa Wadmana poniżej, które wygląda na nieco mniej inwazyjne IMO.
Ortwin Gentz,
Byłoby wspaniale, gdyby ta odpowiedź została zaktualizowana teraz, gdy UIScrollView ujawnia swój mechanizm rozpoznawania gestów przesuwania. (Ale być może jest to poza pierwotnym zakresem.)
jtbandes
Czy istnieje możliwość aktualizacji tego postu? Świetny post i dziękuję za wkład.
Pavan
56

To działa dla mnie za każdym razem ...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);
Tonetel
źródło
20
Dla każdego, kto natknął się na to pytanie: myślę, że ta odpowiedź została opublikowana przed edycją, która wyjaśniła pytanie. Umożliwi to przewijanie tylko w poziomie, nie rozwiązuje problemu możliwości przewijania w pionie lub poziomie, ale nie po przekątnej.
andygeers
U mnie działa jak urok. Mam bardzo długi poziomy scrollView ze stronicowaniem i UIWebView na każdej stronie, który obsługuje przewijanie w pionie, którego potrzebuję.
Tom Redman
Świetny! Ale czy ktoś może wyjaśnić, jak to działa (ustawiając wysokość contentSize na 1)!
Praveen
To naprawdę działa, ale dlaczego i jak działa? Czy ktoś może nadać temu sens?
Marko Francekovic
27

Dla leniwych jak ja:

Ustaw scrollview.directionalLockEnablednaYES

- (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}

- (void) scrollViewDidScroll: (UIScrollView *) scrollView
{
    if (scrollView.contentOffset.x != self.oldContentOffset.x)
    {
        scrollView.pagingEnabled = YES;
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x,
                                               self.oldContentOffset.y);
    }
    else
    {
        scrollView.pagingEnabled = NO;
    }
}

- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}
icompot
źródło
16

Użyłem następującej metody, aby rozwiązać ten problem, mam nadzieję, że ci pomoże.

Najpierw zdefiniuj następujące zmienne w pliku nagłówkowym kontrolerów.

CGPoint startPos;
int     scrollDirection;

pozycja_początkowa zachowa contentOffset wartość, gdy delegat otrzyma scrollViewWillBeginDragging wiadomość. Więc w tej metodzie robimy to;

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    startPos = scrollView.contentOffset;
    scrollDirection=0;
}

następnie używamy tych wartości do określenia zamierzonego przez użytkownika kierunku przewijania w wiadomości scrollViewDidScroll .

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{

   if (scrollDirection==0){//we need to determine direction
       //use the difference between positions to determine the direction.
       if (abs(startPos.x-scrollView.contentOffset.x)<abs(startPos.y-scrollView.contentOffset.y)){          
          NSLog(@"Vertical Scrolling");
          scrollDirection=1;
       } else {
          NSLog(@"Horitonzal Scrolling");
          scrollDirection=2;
       }
    }
//Update scroll position of the scrollview according to detected direction.     
    if (scrollDirection==1) {
       [scrollView setContentOffset:CGPointMake(startPos.x,scrollView.contentOffset.y) animated:NO];
    } else if (scrollDirection==2){
       [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x,startPos.y) animated:NO];
    }
 }

w końcu musimy zatrzymać wszystkie operacje aktualizacji, gdy użytkownik kończy przeciąganie;

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (decelerate) {
       scrollDirection=3;
    }
 }
Yildiray Meric
źródło
Hmm ... Właściwie po dokładniejszym zbadaniu - działa całkiem nieźle, ale problem polega na tym, że traci kontrolę nad kierunkiem podczas fazy zwalniania - więc jeśli zaczniesz poruszać palcem po przekątnej, będzie się wydawać, że porusza się tylko w poziomie lub w pionie dopóki nie puścisz, w którym momencie
zwolni
@andygeers, mogłoby działać lepiej, gdyby - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (decelerate) { scrollDirection=3; } }zostało zmienione na - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (!decelerate) { scrollDirection=0; } }i- (void)scrollViewDidEndDecelerating{ scrollDirection=0; }
cduck
Nie działa dla mnie. Wydaje się, że ustawienie contentOffset w locie niszczy zachowanie stronicowania. Poszedłem z rozwiązaniem Mattiasa Wadmana.
Ortwin Gentz,
13

Mogę tu grzebać, ale natknąłem się na ten post dzisiaj, próbując rozwiązać ten sam problem. Oto moje rozwiązanie, wygląda na to, że działa świetnie.

Chcę wyświetlić ekran pełen treści, ale pozwolę użytkownikowi przewijać do jednego z 4 innych ekranów, w górę iw dół w lewo iw prawo. Mam jeden UIScrollView o rozmiarze 320x480 i contentSize 960x1440. Przesunięcie zawartości zaczyna się od 320,480. W scrollViewDidEndDecelerating :, przerysowuję widoki 5 (widoki środkowe i 4 wokół niego) i resetuję przesunięcie zawartości do 320,480.

Oto mięso mojego rozwiązania, metoda scrollViewDidScroll:.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{   
    if (!scrollingVertically && ! scrollingHorizontally)
    {
        int xDiff = abs(scrollView.contentOffset.x - 320);
        int yDiff = abs(scrollView.contentOffset.y - 480);

        if (xDiff > yDiff)
        {
            scrollingHorizontally = YES;
        }
        else if (xDiff < yDiff)
        {
            scrollingVertically = YES;
        }
    }

    if (scrollingHorizontally)
    {
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, 480);
    }
    else if (scrollingVertically)
    {
        scrollView.contentOffset = CGPointMake(320, scrollView.contentOffset.y);
    }
}
James J.
źródło
Prawdopodobnie zauważysz, że ten kod nie „zwalnia” ładnie, gdy użytkownik puści widok przewijania - zatrzyma się. Jeśli nie chcesz spowolnienia, może to być dla ciebie w porządku.
andygeers
4

Chłopaki, jest to tak proste, jak ustawienie wysokości podglądu podrzędnego na taką samą, jak wysokość widoku przewijania i wysokość rozmiaru treści. Następnie przewijanie w pionie jest wyłączone.

julianlubenov
źródło
2
Dla każdego, kto natknął się na to pytanie: myślę, że ta odpowiedź została opublikowana przed edycją, która wyjaśniła pytanie. Umożliwi to przewijanie tylko w poziomie, nie rozwiązuje problemu możliwości przewijania w pionie lub poziomie, ale nie po przekątnej.
andygeers
3

Oto moja implementacja strony, UIScrollViewktóra zezwala tylko na przewijanie ortogonalne. Pamiętaj, aby ustawić pagingEnabledsię YES.

PagedOrthoScrollView.h

@interface PagedOrthoScrollView : UIScrollView
@end

PagedOrthoScrollView.m

#import "PagedOrthoScrollView.h"

typedef enum {
  PagedOrthoScrollViewLockDirectionNone,
  PagedOrthoScrollViewLockDirectionVertical,
  PagedOrthoScrollViewLockDirectionHorizontal
} PagedOrthoScrollViewLockDirection; 

@interface PagedOrthoScrollView ()
@property(nonatomic, assign) PagedOrthoScrollViewLockDirection dirLock;
@property(nonatomic, assign) CGFloat valueLock;
@end

@implementation PagedOrthoScrollView
@synthesize dirLock;
@synthesize valueLock;

- (id)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self == nil) {
    return self;
  }

  self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  self.valueLock = 0;

  return self;
}

- (void)setBounds:(CGRect)bounds {
  int mx, my;

  if (self.dirLock == PagedOrthoScrollViewLockDirectionNone) {
    // is on even page coordinates, set dir lock and lock value to closest page 
    mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
    if (mx != 0) {
      self.dirLock = PagedOrthoScrollViewLockDirectionHorizontal;
      self.valueLock = (round(CGRectGetMinY(bounds) / CGRectGetHeight(self.bounds)) *
                        CGRectGetHeight(self.bounds));
    } else {
      self.dirLock = PagedOrthoScrollViewLockDirectionVertical;
      self.valueLock = (round(CGRectGetMinX(bounds) / CGRectGetWidth(self.bounds)) *
                        CGRectGetWidth(self.bounds));
    }

    // show only possible scroll indicator
    self.showsVerticalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionVertical;
    self.showsHorizontalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionHorizontal;
  }

  if (self.dirLock == PagedOrthoScrollViewLockDirectionHorizontal) {
    bounds.origin.y = self.valueLock;
  } else {
    bounds.origin.x = self.valueLock;
  }

  mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
  my = abs((int)CGRectGetMinY(bounds) % (int)CGRectGetHeight(self.bounds));

  if (mx == 0 && my == 0) {
    // is on even page coordinates, reset lock
    self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  }

  [super setBounds:bounds];
}

@end
Mattias Wadman
źródło
1
Działa świetnie, także na iOS 7. Dzięki!
Ortwin Gentz,
3

Musiałem również rozwiązać ten problem i chociaż odpowiedź Andrieja Tarantowa zdecydowanie zawiera wiele przydatnych informacji, które pomogą zrozumieć, jak UIScrollViewsdziała, uważam, że rozwiązanie jest nieco zbyt skomplikowane. Moje rozwiązanie, które nie obejmuje żadnej podklasy ani przekazywania dotykowego, jest następujące:

  1. Utwórz dwa zagnieżdżone widoki przewijania, po jednym dla każdego kierunku.
  2. Utwórz dwa atrapy UIPanGestureRecognizers, ponownie po jednym dla każdego kierunku.
  3. Utwórz zależności niepowodzenia między UIScrollViewswłaściwościami „panGestureRecognizer i atrapą UIPanGestureRecognizerodpowiadającą kierunkowi prostopadłemu do żądanego kierunku przewijania UIScrollView przy użyciu UIGestureRecognizer's -requireGestureRecognizerToFail:selektora.
  4. W -gestureRecognizerShouldBegin:wywołaniach zwrotnych dla fikcyjnych UIPanGestureRecognizers oblicz początkowy kierunek panoramy za pomocą selektora -translationInView: gestu ( UIPanGestureRecognizersnie wywołasz tego wywołania zwrotnego delegata, dopóki dotyk nie przetłumaczy się wystarczająco, aby zarejestrować się jako panoramowanie). Zezwalaj lub nie zezwalaj na uruchamianie aparatu rozpoznawania gestów w zależności od obliczonego kierunku, który z kolei powinien kontrolować, czy będzie można rozpocząć rozpoznawanie prostopadłych gestów panoramowania UIScrollView.
  5. W -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:, nie pozwól swojemu manekinowi UIPanGestureRecognizersdziałać obok przewijania (chyba że masz jakieś dodatkowe zachowanie, które chciałbyś dodać).
Kurt Horimoto
źródło
2

Stworzyłem stronicowanie UIScrollView z ramką o rozmiarze ekranu widoku i ustawiłem rozmiar zawartości na szerokość potrzebną dla całej mojej zawartości i wysokość 1 (dzięki Tonetel). Następnie stworzyłem strony menu UIScrollView z ramkami ustawionymi na każdą stronę, rozmiar ekranu widoku i ustawiłem rozmiar zawartości każdej z nich na szerokość ekranu i niezbędną wysokość dla każdej z nich. Następnie po prostu dodałem każdą ze stron menu do widoku stronicowania. Upewnij się, że stronicowanie jest włączone w widoku przewijania stronicowania, ale wyłączone w widokach menu (powinno być domyślnie wyłączone) i powinno być dobrze.

Widok przewijania stronicowania jest teraz przewijany w poziomie, ale nie w pionie ze względu na wysokość zawartości. Każda strona jest przewijana w pionie, ale nie w poziomie z powodu ograniczeń rozmiaru zawartości.

Oto kod, jeśli to wyjaśnienie pozostawiło coś do życzenia:

UIScrollView *newPagingScrollView =  [[UIScrollView alloc] initWithFrame:self.view.bounds]; 
[newPagingScrollView setPagingEnabled:YES];
[newPagingScrollView setShowsVerticalScrollIndicator:NO];
[newPagingScrollView setShowsHorizontalScrollIndicator:NO];
[newPagingScrollView setDelegate:self];
[newPagingScrollView setContentSize:CGSizeMake(self.view.bounds.size.width * NumberOfDetailPages, 1)];
[self.view addSubview:newPagingScrollView];

float pageX = 0;

for (int i = 0; i < NumberOfDetailPages; i++)
{               
    CGRect pageFrame = (CGRect) 
    {
        .origin = CGPointMake(pageX, pagingScrollView.bounds.origin.y), 
        .size = pagingScrollView.bounds.size
    };
    UIScrollView *newPage = [self createNewPageFromIndex:i ToPageFrame:pageFrame]; // newPage.contentSize custom set in here        
    [pagingScrollView addSubview:newPage];

    pageX += pageFrame.size.width;  
}           
Jon Conner
źródło
2

Dzięki Tonetel. Nieznacznie zmodyfikowałem Twoje podejście, ale było to dokładnie to, czego potrzebowałem, aby zapobiec przewijaniu w poziomie.

self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 1);
Jeremy
źródło
2

To ulepszone rozwiązanie icompot, które było dla mnie dość niestabilne. Ten działa dobrze i jest łatwy do wdrożenia:

- (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}

- (void) scrollViewDidScroll: (UIScrollView *) scrollView
{    

    float XOffset = fabsf(self.oldContentOffset.x - scrollView.contentOffset.x );
    float YOffset = fabsf(self.oldContentOffset.y - scrollView.contentOffset.y );

        if (scrollView.contentOffset.x != self.oldContentOffset.x && (XOffset >= YOffset) )
        {

            scrollView.pagingEnabled = YES;
            scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x,
                                                   self.oldContentOffset.y);

        }
        else
        {
            scrollView.pagingEnabled = NO;
               scrollView.contentOffset = CGPointMake( self.oldContentOffset.x,
                                                   scrollView.contentOffset.y);
        }

}


- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}
Maciej
źródło
2

Na przyszłość oparłem się na odpowiedzi Andreya Tanasova i znalazłem następujące rozwiązanie, które działa dobrze.

Najpierw zdefiniuj zmienną do przechowywania contentOffset i ustaw ją na współrzędne 0,0

var positionScroll = CGPointMake(0, 0)

Teraz zaimplementuj następujące elementy w delegacie scrollView:

override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    // Note that we are getting a reference to self.scrollView and not the scrollView referenced passed by the method
    // For some reason, not doing this fails to get the logic to work
    self.positionScroll = (self.scrollView?.contentOffset)!
}

override func scrollViewDidScroll(scrollView: UIScrollView) {

    // Check that contentOffset is not nil
    // We do this because the scrollViewDidScroll method is called on viewDidLoad
    if self.scrollView?.contentOffset != nil {

        // The user wants to scroll on the X axis
        if self.scrollView?.contentOffset.x > self.positionScroll.x || self.scrollView?.contentOffset.x < self.positionScroll.x {

            // Reset the Y position of the scrollView to what it was BEFORE scrolling started
            self.scrollView?.contentOffset = CGPointMake((self.scrollView?.contentOffset.x)!, self.positionScroll.y);
            self.scrollView?.pagingEnabled = true
        }

        // The user wants to scroll on the Y axis
        else {
            // Reset the X position of the scrollView to what it was BEFORE scrolling started
            self.scrollView?.contentOffset = CGPointMake(self.positionScroll.x, (self.scrollView?.contentOffset.y)!);
            self.collectionView?.pagingEnabled = false
        }

    }

}

Mam nadzieję że to pomoże.

Benzoes
źródło
1

Z dokumentów:

"wartość domyślna to NIE, co oznacza, że ​​przewijanie jest dozwolone zarówno w kierunku poziomym, jak i pionowym. Jeśli wartość wynosi TAK, a użytkownik rozpocznie przeciąganie w jednym ogólnym kierunku (poziomym lub pionowym), widok przewijania wyłącza przewijanie w drugim kierunku. "

Myślę, że ważną częścią jest „jeśli ... użytkownik zacznie przeciągać w jednym ogólnym kierunku”. Więc jeśli zaczną przeciągać po przekątnej, to nie zadziała. Nie żeby te dokumenty były zawsze wiarygodne, jeśli chodzi o interpretację - ale wydaje się, że pasuje do tego, co widzisz.

Wydaje się to rozsądne. Muszę zapytać - dlaczego w dowolnym momencie chcesz ograniczyć się tylko do poziomu lub tylko do pionu? Być może UIScrollView nie jest narzędziem dla Ciebie?

philsquared
źródło
1

Mój widok składa się z 10 widoków poziomych, a niektóre z tych widoków składają się z wielu widoków pionowych, więc otrzymasz coś takiego:


1.0   2.0   3.1    4.1
      2.1   3.1
      2.2
      2.3

Z włączonym stronicowaniem na osi poziomej i pionowej

Widok ma również następujące atrybuty:

[self setDelaysContentTouches:NO];
[self setMultipleTouchEnabled:NO];
[self setDirectionalLockEnabled:YES];

Teraz, aby zapobiec przewijaniu po przekątnej, wykonaj następujące czynności:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    int pageWidth = 768;
    int pageHeight = 1024;
    int subPage =round(self.contentOffset.y / pageHeight);
    if ((int)self.contentOffset.x % pageWidth != 0 && (int)self.contentOffset.y % pageHeight != 0) {
        [self setContentOffset:CGPointMake(self.contentOffset.x, subPage * pageHeight];
    } 
}

U mnie działa jak urok!

user422756
źródło
1
nie rozumiem, jak to może działać dla Ciebie ... czy mógłbyś przesłać przykładowy projekt?
hfossli
1

Trzeba zresetować _isHorizontalScrolldo NIE w touchesEndedi touchesCancelled.

Praveen Matanam
źródło
0

Jeśli nie próbowałeś tego robić w wersji beta 3.0, polecam spróbować. Obecnie używam serii widoków tabeli w widoku przewijania i działa zgodnie z oczekiwaniami. (Cóż, przewijanie tak czy inaczej. Znalazłem to pytanie, szukając rozwiązania zupełnie innego problemu ...)

Tim Keating
źródło
0

Dla tych, którzy szukają w Swift drugiego rozwiązania @AndreyTarantsov, wygląda to tak, jak w kodzie:

    var positionScroll:CGFloat = 0

    func scrollViewWillBeginDragging(scrollView: UIScrollView) {
        positionScroll = self.theScroll.contentOffset.x
    }

    func scrollViewDidScroll(scrollView: UIScrollView) {
        if self.theScroll.contentOffset.x > self.positionScroll || self.theScroll.contentOffset.x < self.positionScroll{
             self.theScroll.pagingEnabled = true
        }else{
            self.theScroll.pagingEnabled = false
        }
    }

tym, co tutaj robimy, jest utrzymanie bieżącej pozycji tuż przed przeciągnięciem widoku scrollview, a następnie sprawdzenie, czy x zwiększyło się lub zmniejszyło w porównaniu z bieżącą pozycją x. Jeśli tak, ustaw pagingEnabled na true, jeśli nie (y wzrosło / spadło), to ustaw pagingEnabled na false

Mam nadzieję, że jest to przydatne dla nowych przybyszów!

jonprasetyo
źródło
0

Udało mi się to rozwiązać, implementując scrollViewDidBeginDragging (_ :) i patrząc na prędkość na bazowym UIPanGestureRecognizer.

Uwaga: w tym rozwiązaniu każda panorama, która byłaby przekątna, zostanie zignorowana przez scrollView.

override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    super.scrollViewWillBeginDragging(scrollView)
    let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
    if velocity.x != 0 && velocity.y != 0 {
        scrollView.panGestureRecognizer.isEnabled = false
        scrollView.panGestureRecognizer.isEnabled = true
    }
}
Morten Saabye Kristensen
źródło
-3

Jeśli używasz narzędzia do tworzenia interfejsów do projektowania interfejsu - można to zrobić dość łatwo.

W konstruktorze interfejsu wystarczy kliknąć UIScrollView i wybrać opcję (w inspektorze), która ogranicza przewijanie tylko w jedną stronę.

zpesk
źródło
1
W IB nie ma opcji, aby to zrobić. Dwie opcje, do których możesz się odnosić, dotyczą wyświetlania paska przewijania, a nie faktycznego przewijania. Co więcej, blokada kierunku blokuje kierunek dopiero po rozpoczęciu przewijania.
Sophtware