Muszę przyznać, że jesteś w bardzo trudnej sytuacji.
Zauważ, że musisz użyć UIScrollView z, pagingEnabled=YES
aby przełączać się między stronami, ale musisz pagingEnabled=NO
przewijać 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 hitTest
metodę i zawsze zwraca się, więc wszystkie zdarzenia dotyku trafiają do UIScrollView. Następnie wewnątrz touchesBegan
, touchesMoved
czeki 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 touchesCancelled
widok 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
delaysContentTouches
jest 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
cancelsTouches
jest 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
, touchesEnded
a touchesCanceled
wydarzenia z Cocoa Touch (bo jego hitTest
mó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 delaysContentTouches
na 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ść touchesBegan
do 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ł touchesBegan
później wysłać do UIScrollView, ponieważ nie możesz przechowywać touches
argumentu: zawiera obiekty, które zostaną zmutowane przed następnym touchesMoved
zdarzeniem i nie możesz odtworzyć starego stanu.
Musisz więc natychmiast przejść touchesBegan
do UIScrollView, ale będziesz ukrywać wszelkie dalsze touchesMoved
zdarzenia, dopóki nie zdecydujesz się przewijać w poziomie. Nie touchesMoved
oznacza braku przewijania, więc ten inicjał touchesBegan
nie zaszkodzi. Ale ustaw delaysContentTouches
na 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 touchesBegan
póź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ć touchesCancelled
i touchesEnded
. Trzeba skręcić touchesEnded
w touchesCancelled
jednak, ponieważ UIScrollView zinterpretuje touchesBegan
, touchesEnded
sekwencję 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.
@interface RemorsefulScrollView : UIScrollView {
CGPoint _originalPoint;
BOOL _isHorizontalScroll, _isMultitouch;
UIView *_currentChild;
}
@end
#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];
if (_isHorizontalScroll)
return;
if ([touches count] == [[event touchesForView:self] count]) {
_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];
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 touchesXxx
ponownym 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 pagingEnabled
właściwość w locie z scrollViewDidScroll
metody delegata.
Aby zapobiec przewijaniu po przekątnej, należy najpierw spróbować zapamiętać zawartość przesunięcia scrollViewWillBeginDragging
oraz sprawdzić i zresetować zawartość przesunięcia wewnątrz w scrollViewDidScroll
przypadku 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
, touchesMoved
etc, 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 andreyvit@gmail.com, z radością dołączę go do mojej torby ze sztuczkami ScrollingMadness .
PPS Dodanie tego małego eseju do pliku README ScrollingMadness.