Du bist in einer sehr schwierigen Situation, muss ich sagen.
Beachten Sie, dass Sie eine UIScrollView mit verwenden müssen pagingEnabled=YES
, um zwischen Seiten zu wechseln, aber pagingEnabled=NO
vertikal scrollen müssen.
Es gibt 2 mögliche Strategien. Ich weiß nicht, welches funktionieren wird / einfacher zu implementieren ist, also versuchen Sie beide.
Erstens: verschachtelte UIScrollViews. Ehrlich gesagt, ich muss noch eine Person sehen, die das zum Laufen gebracht hat. Ich habe mich jedoch persönlich nicht genug bemüht, und meine Praxis zeigt, dass Sie UIScrollView dazu bringen können , alles zu tun, was Sie wollen , wenn Sie sich genug anstrengen .
Die Strategie besteht also darin, die äußere Bildlaufansicht nur für das horizontale Bildlauf und die innere Bildlaufansicht nur für das vertikale Bildlauf zu verwenden. Um dies zu erreichen, müssen Sie wissen, wie UIScrollView intern funktioniert. Es überschreibt die hitTest
Methode und gibt sich immer selbst zurück, sodass alle Berührungsereignisse in UIScrollView gespeichert werden. Dann innen touchesBegan
, touchesMoved
etc es überprüft , ob es im Falle interessiert, und entweder Griffe oder leitet sie an den inneren Komponenten.
Um zu entscheiden, ob die Berührung behandelt oder weitergeleitet werden soll, startet UIScrollView einen Timer, wenn Sie sie zum ersten Mal berühren:
Wenn Sie Ihren Finger innerhalb von 150 ms nicht wesentlich bewegt haben, wird das Ereignis an die Innenansicht weitergeleitet.
Wenn Sie Ihren Finger innerhalb von 150 ms erheblich bewegt haben, beginnt er mit dem Scrollen (und leitet das Ereignis niemals an die innere Ansicht weiter).
Beachten Sie, dass beim Berühren einer Tabelle (die eine Unterklasse der Bildlaufansicht ist) und beim sofortigen Bildlauf die von Ihnen berührte Zeile niemals hervorgehoben wird.
Wenn Sie nicht Ihren Finger bewegt deutlich innerhalb 150ms und UIScrollView begann an der Innenansicht die Ereignisse vorbei, aber dann haben Sie den Finger weit genug für das Scrollen bewegt zu beginnen, UIScrollView ruft touchesCancelled
auf der Innenansicht und Scrollen beginnt.
Beachten Sie, dass beim Berühren einer Tabelle, halten Sie den Finger etwas gedrückt und beginnen Sie mit dem Scrollen. Die Zeile, die Sie berührt haben, wird zuerst hervorgehoben, anschließend jedoch deaktiviert.
Diese Ereignisfolge kann durch Konfiguration von UIScrollView geändert werden:
- Wenn
delaysContentTouches
NEIN ist, wird kein Timer verwendet - die Ereignisse gehen sofort an die innere Steuerung (werden jedoch abgebrochen, wenn Sie Ihren Finger weit genug bewegen).
- Wenn
cancelsTouches
NEIN ist, wird nach dem Senden der Ereignisse an ein Steuerelement kein Bildlauf mehr ausgeführt.
Beachten Sie, dass es UIScrollView ist , das erhält alle touchesBegin
, touchesMoved
, touchesEnded
und touchesCanceled
Ereignisse von Cocoatouch (weil sein hitTest
es so zu tun , erzählt). Es leitet sie dann an die innere Ansicht weiter, wenn es will, solange es will.
Nachdem Sie alles über UIScrollView wissen, können Sie dessen Verhalten ändern. Ich wette, Sie möchten dem vertikalen Scrollen den Vorzug geben, sodass die Ansicht in vertikaler Richtung rollt, sobald der Benutzer die Ansicht berührt und seinen Finger bewegt (auch nur geringfügig). Wenn der Benutzer seinen Finger jedoch weit genug in horizontaler Richtung bewegt, möchten Sie das vertikale Scrollen abbrechen und das horizontale Scrollen starten.
Sie möchten Ihre äußere UIScrollView in Unterklassen unterteilen (z. B. nennen Sie Ihre Klasse RemorsefulScrollView), damit anstelle des Standardverhaltens alle Ereignisse sofort an die innere Ansicht weitergeleitet werden und nur dann gescrollt wird, wenn eine signifikante horizontale Bewegung erkannt wird.
Wie kann RemorsefulScrollView so verhalten?
Es sieht so aus, als ob das Deaktivieren des vertikalen Bildlaufs und das Setzen delaysContentTouches
auf NO dazu führen sollte, dass verschachtelte UIScrollViews funktionieren. Leider nicht; UIScrollView scheint einige zusätzliche Filter für schnelle Bewegungen durchzuführen (die nicht deaktiviert werden können), so dass UIScrollView, selbst wenn es nur horizontal gescrollt werden kann, immer schnell genug vertikale Bewegungen aufnimmt (und ignoriert).
Der Effekt ist so stark, dass vertikales Scrollen in einer verschachtelten Bildlaufansicht unbrauchbar wird. (Es scheint, dass Sie genau dieses Setup haben, also versuchen Sie es: Halten Sie einen Finger 150 ms lang gedrückt und bewegen Sie ihn dann in vertikaler Richtung - verschachteltes UIScrollView funktioniert dann wie erwartet!)
Dies bedeutet, dass Sie den UIScrollView-Code nicht für die Ereignisbehandlung verwenden können. Sie müssen alle vier Berührungsbehandlungsmethoden in RemorsefulScrollView überschreiben und zuerst Ihre eigene Verarbeitung durchführen. Das Ereignis wird nur an super
(UIScrollView) weitergeleitet, wenn Sie sich für das horizontale Scrollen entschieden haben.
Sie müssen jedoch touchesBegan
an UIScrollView übergeben, da es eine Basiskoordinate für das zukünftige horizontale Scrollen speichern soll (wenn Sie später entscheiden, dass es sich um ein horizontales Scrollen handelt). Sie können später nicht touchesBegan
an UIScrollView senden , da Sie das touches
Argument nicht speichern können : Es enthält Objekte, die vor dem nächsten touchesMoved
Ereignis mutiert werden , und Sie können den alten Status nicht reproduzieren.
Sie müssen touchesBegan
also sofort an UIScrollView übergeben, aber Sie werden alle weiteren touchesMoved
Ereignisse ausblenden , bis Sie sich entscheiden, horizontal zu scrollen. Nein touchesMoved
bedeutet kein Scrollen, daher touchesBegan
schadet diese Initiale nicht. Stellen Sie delaysContentTouches
jedoch NO ein, damit keine zusätzlichen Überraschungstimer stören.
(Offtopic - Im Gegensatz zu Ihnen kann UIScrollView Berührungen ordnungsgemäß speichern und das ursprüngliche Ereignis später reproduzieren und weiterleiten . Es hat den unfairen Vorteil, unveröffentlichte APIs zu verwenden, sodass Berührungsobjekte geklont werden können , touchesBegan
bevor sie mutiert werden.)
Da Sie immer weiterleiten touchesBegan
, müssen Sie auch weiterleiten touchesCancelled
und touchesEnded
. Sie haben zu drehen , touchesEnded
in touchesCancelled
, aber, weil UIScrollView würde interpretieren touchesBegan
, touchesEnded
Sequenz als Touch-Klick, und es an die Innenansicht weiterleiten. Sie leiten die richtigen Ereignisse bereits selbst weiter, sodass UIScrollView niemals etwas weiterleiten soll.
Grundsätzlich ist hier Pseudocode für das, was Sie tun müssen. Der Einfachheit halber erlaube ich niemals horizontales Scrollen, nachdem ein Multitouch-Ereignis aufgetreten ist.
// 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
Ich habe nicht versucht, dies auszuführen oder sogar zu kompilieren (und die gesamte Klasse in einen Nur-Text-Editor eingegeben), aber Sie können mit dem oben Gesagten beginnen und es hoffentlich zum Laufen bringen.
Der einzige versteckte Haken, den ich sehe, ist, dass, wenn Sie RemorsefulScrollView untergeordnete Ansichten hinzufügen, die nicht zu UIScrollView gehören, die Berührungsereignisse, die Sie an ein untergeordnetes Element weiterleiten, möglicherweise über die Antwortkette zu Ihnen zurückkehren, wenn das untergeordnete Element nicht immer wie UIScrollView mit Berührungen umgeht. Eine kugelsichere RemorsefulScrollView-Implementierung würde vor touchesXxx
Wiedereintritt schützen .
Zweite Strategie: Wenn verschachtelte UIScrollViews aus irgendeinem Grund nicht funktionieren oder sich als zu schwierig erweisen, um sie richtig zu machen, können Sie versuchen, mit nur einer UIScrollView auszukommen und ihre pagingEnabled
Eigenschaft im Handumdrehen von Ihrer scrollViewDidScroll
Delegatenmethode zu ändern .
Um ein diagonales Scrollen zu verhindern, sollten Sie zunächst versuchen, sich an contentOffset zu erinnern scrollViewWillBeginDragging
und contentOffset im Inneren zu überprüfen und zurückzusetzen, scrollViewDidScroll
wenn Sie eine diagonale Bewegung feststellen. Eine andere Strategie besteht darin, contentSize zurückzusetzen, um das Scrollen nur in eine Richtung zu ermöglichen, sobald Sie entschieden haben, in welche Richtung der Finger des Benutzers geht. (UIScrollView scheint ziemlich verzeihend zu sein, wenn es darum geht, mit contentSize und contentOffset aus seinen Delegatenmethoden herumzuspielen.)
Wenn das nicht funktioniert entweder oder Ergebnisse in schlampig Visuals, haben Sie außer Kraft zu setzen touchesBegan
, touchesMoved
etc und nicht vorwärts diagonale Bewegung Ereignisse zu UIScrollView. (Die Benutzererfahrung ist in diesem Fall jedoch nicht optimal, da Sie diagonale Bewegungen ignorieren müssen, anstatt sie in eine einzige Richtung zu zwingen. Wenn Sie sich wirklich abenteuerlustig fühlen, können Sie Ihr eigenes UITouch-Lookalike schreiben, etwa RevengeTouch. Ziel -C ist einfach altes C, und es gibt nichts Entenartigeres auf der Welt als C: Solange niemand die reale Klasse der Objekte überprüft, was meiner Meinung nach niemand tut, können Sie jede Klasse wie jede andere Klasse aussehen lassen. Dies öffnet sich eine Möglichkeit, beliebige Berührungen mit beliebigen Koordinaten zu synthetisieren.)
Sicherungsstrategie: Es gibt TTScrollView, eine vernünftige Neuimplementierung von UIScrollView in der Three20-Bibliothek . Leider fühlt es sich für den Benutzer sehr unnatürlich und nicht iphonisch an. Wenn jedoch jeder Versuch, UIScrollView zu verwenden, fehlschlägt, können Sie auf eine benutzerdefinierte Bildlaufansicht zurückgreifen. Ich empfehle dagegen, wenn es überhaupt möglich ist; Durch die Verwendung von UIScrollView wird sichergestellt, dass Sie das native Erscheinungsbild erhalten, unabhängig davon, wie es sich in zukünftigen iPhone OS-Versionen entwickelt.
Okay, dieser kleine Aufsatz wurde etwas zu lang. Nach der Arbeit an ScrollingMadness vor einigen Tagen beschäftige ich mich immer noch mit UIScrollView-Spielen.
PS Wenn Sie eines davon zum Laufen bringen und Lust haben, es zu teilen, senden Sie mir bitte eine E-Mail mit dem entsprechenden Code an andreyvit@gmail.com. Ich würde ihn gerne in meine ScrollingMadness- Trickkiste aufnehmen .
PPS Hinzufügen dieses kleinen Aufsatzes zu ScrollingMadness README.