UIScrollView: horizontal blättern, vertikal scrollen?


88

Wie kann ich ein erzwingen, UIScrollViewbei dem Paging und Scrollen aktiviert sind, um sich zu einem bestimmten Zeitpunkt nur vertikal oder horizontal zu bewegen?

Ich verstehe, dass die directionalLockEnabledEigenschaft dies erreichen sollte, aber ein diagonales Wischen bewirkt immer noch, dass die Ansicht diagonal scrollt, anstatt die Bewegung auf eine einzelne Achse zu beschränken.

Bearbeiten: Um klarer zu sein, möchte ich dem Benutzer erlauben, horizontal ODER vertikal zu scrollen, aber nicht beide gleichzeitig.


Möchten Sie einschränken, dass eine Ansicht NUR durch Horiz / Vert gescrollt wird, oder möchten Sie, dass der Benutzer Horiz ODER Vert scrollt, aber nicht beide gleichzeitig?
Will Hartung

Können Sie mir einen Gefallen tun und den Titel so ändern, dass er etwas interessanter und nicht trivialer aussieht? So etwas wie "UIScrollView: horizontal blättern, vertikal scrollen?"
Andrey Tarantsov

Leider habe ich die Frage als nicht registrierter Benutzer auf einem Computer gepostet, auf den ich keinen Zugriff mehr habe (bevor ich mich bei meiner Ankunft unter demselben Namen registriert habe), was bedeutet, dass ich sie nicht bearbeiten kann. Dies sollte mir auch erklären, wie ich weiter unten nachverfolge, anstatt die ursprüngliche Frage zu bearbeiten. Entschuldigen Sie, dass Sie diesmal die Stapeletikette gebrochen haben!
Nick

Antworten:


117

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=NOvertikal 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 hitTestMethode und gibt sich immer selbst zurück, sodass alle Berührungsereignisse in UIScrollView gespeichert werden. Dann innen touchesBegan, touchesMovedetc 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 touchesCancelledauf 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 delaysContentTouchesNEIN ist, wird kein Timer verwendet - die Ereignisse gehen sofort an die innere Steuerung (werden jedoch abgebrochen, wenn Sie Ihren Finger weit genug bewegen).
  • Wenn cancelsTouchesNEIN 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, touchesEndedund touchesCanceledEreignisse von Cocoatouch (weil sein hitTestes 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 delaysContentTouchesauf 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 touchesBeganan 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 touchesBeganan UIScrollView senden , da Sie das touchesArgument nicht speichern können : Es enthält Objekte, die vor dem nächsten touchesMovedEreignis mutiert werden , und Sie können den alten Status nicht reproduzieren.

    Sie müssen touchesBeganalso sofort an UIScrollView übergeben, aber Sie werden alle weiteren touchesMovedEreignisse ausblenden , bis Sie sich entscheiden, horizontal zu scrollen. Nein touchesMovedbedeutet kein Scrollen, daher touchesBeganschadet diese Initiale nicht. Stellen Sie delaysContentTouchesjedoch 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 , touchesBeganbevor sie mutiert werden.)

  • Da Sie immer weiterleiten touchesBegan, müssen Sie auch weiterleiten touchesCancelledund touchesEnded. Sie haben zu drehen , touchesEndedin touchesCancelled, aber, weil UIScrollView würde interpretieren touchesBegan, touchesEndedSequenz 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 touchesXxxWiedereintritt 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 pagingEnabledEigenschaft im Handumdrehen von Ihrer scrollViewDidScrollDelegatenmethode zu ändern .

Um ein diagonales Scrollen zu verhindern, sollten Sie zunächst versuchen, sich an contentOffset zu erinnern scrollViewWillBeginDraggingund contentOffset im Inneren zu überprüfen und zurückzusetzen, scrollViewDidScrollwenn 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, touchesMovedetc 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.


7
Beachten Sie, dass dieses verschachtelte Scrollview-Verhalten jetzt im SDK sofort funktioniert. Es ist kein lustiges Geschäft erforderlich
andygeers

1
+ 1'd andygeers Kommentar, dann festgestellt, dass es nicht genau ist; Sie können die reguläre UIScrollView immer noch "brechen", indem Sie diagonal vom Startpunkt ziehen und dann "den Scroller verlieren". Die Richtungssperre wird ignoriert, bis Sie loslassen und am Ende wissen, wo.
Kalle

Vielen Dank für die großartige Erklärung hinter den Kulissen! Ich habe mich für die unten stehende Lösung von Mattias Wadman entschieden, die etwas weniger invasiv aussieht.
Ortwin Gentz

Es wäre großartig, wenn diese Antwort jetzt aktualisiert würde, da UIScrollView seinen Pan-Gesten-Erkenner verfügbar macht. (Aber vielleicht liegt das außerhalb des ursprünglichen Bereichs.)
jtbandes

Gibt es eine Möglichkeit, diesen Beitrag zu aktualisieren? Toller Beitrag und vielen Dank für Ihre Eingabe.
Pavan

56

Das funktioniert bei mir jedes Mal ...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);

20
Für alle anderen, die über diese Frage stolpern: Ich denke, diese Antwort wurde vor der Bearbeitung veröffentlicht, die die Frage geklärt hat. Auf diese Weise können Sie nur horizontal scrollen. Es wird nicht das Problem behoben, dass Sie vertikal oder horizontal, aber nicht diagonal scrollen können.
Andygeers

Funktioniert wie ein Zauber für mich. Ich habe eine sehr lange horizontale ScrollView mit Paging und eine UIWebView auf jeder Seite, die das vertikale Scrollen übernimmt, das ich benötige.
Tom Redman

Ausgezeichnet! Aber kann jemand erklären, wie das funktioniert (ContentSize-Höhe auf 1 setzen)!
Praveen

Es funktioniert wirklich, aber warum und wie funktioniert es? Kann jemand einen Sinn daraus machen?
Marko Francekovic

26

Für die faulen Leute wie mich:

Set scrollview.directionalLockEnabledzuYES

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

16

Ich habe die folgende Methode verwendet, um dieses Problem zu lösen. Ich hoffe, es hilft Ihnen.

Definieren Sie zunächst die folgenden Variablen in der Header-Datei Ihres Controllers.

CGPoint startPos;
int     scrollDirection;

startPos behält den contentOffset- Wert bei, wenn Ihr Delegat die scrollViewWillBeginDragging- Nachricht empfängt . Bei dieser Methode machen wir das also;

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

Anschließend verwenden wir diese Werte, um die vom Benutzer beabsichtigte Bildlaufrichtung in der Nachricht scrollViewDidScroll zu bestimmen .

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

Schließlich müssen wir alle Aktualisierungsvorgänge stoppen, wenn der Benutzer das Ziehen beendet.

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (decelerate) {
       scrollDirection=3;
    }
 }

Hmm ... Eigentlich funktioniert es nach weiteren Untersuchungen ziemlich gut - aber das Problem ist, dass es während der Verzögerungsphase die Kontrolle über die Richtung verliert. Wenn Sie also anfangen, Ihren Finger diagonal zu bewegen, scheint er sich nur horizontal oder vertikal zu bewegen bis Sie loslassen, an welchem ​​Punkt es für eine Weile in diagonaler Richtung abgebremst wird
andygeers

@andygeers, es könnte besser funktionieren, wenn - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (decelerate) { scrollDirection=3; } }geändert zu - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (!decelerate) { scrollDirection=0; } }und- (void)scrollViewDidEndDecelerating{ scrollDirection=0; }
cduck

Hat bei mir nicht funktioniert. Das Festlegen von contentOffset im Flug scheint das Paging-Verhalten zu zerstören. Ich habe mich für Mattias Wadmans Lösung entschieden.
Ortwin Gentz

13

Ich bin hier vielleicht am Grab, aber ich bin heute auf diesen Beitrag gestoßen, als ich versuchte, das gleiche Problem zu lösen. Hier ist meine Lösung, scheint großartig zu funktionieren.

Ich möchte einen Bildschirm mit Inhalten anzeigen, aber dem Benutzer erlauben, zu einem von vier anderen Bildschirmen zu scrollen, oben links und rechts. Ich habe eine einzelne UIScrollView mit der Größe 320x480 und einer Inhaltsgröße von 960x1440. Der Inhaltsoffset beginnt bei 320.480. In scrollViewDidEndDecelerating: zeichne ich die 5 Ansichten (mittlere Ansichten und die 4 um sie herum) neu und setze den Inhaltsversatz auf 320.480 zurück.

Hier ist das Fleisch meiner Lösung, die scrollViewDidScroll: -Methode.

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

Sie werden wahrscheinlich feststellen, dass dieser Code nicht gut "verlangsamt" wird, wenn der Benutzer die Bildlaufansicht loslässt - er stoppt tot. Wenn Sie keine Verzögerung wünschen, ist dies möglicherweise in Ordnung für Sie.
andygeers

4

Leute, es ist so einfach, die Höhe der Unteransicht mit der Höhe der Bildlaufansicht und der Höhe der Inhaltsgröße gleichzusetzen. Dann ist das vertikale Scrollen deaktiviert.


2
Für alle anderen, die über diese Frage stolpern: Ich denke, diese Antwort wurde vor der Bearbeitung veröffentlicht, die die Frage geklärt hat. Auf diese Weise können Sie nur horizontal scrollen. Es wird nicht das Problem behoben, dass Sie vertikal oder horizontal, aber nicht diagonal scrollen können.
Andygeers

3

Hier ist meine Implementierung einer Seite UIScrollView, die nur orthogonale Schriftrollen zulässt. Achten Sie darauf einstellen pagingEnabledzu 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

1
Funktioniert auch unter iOS 7. Danke!
Ortwin Gentz

3

Ich musste auch dieses Problem lösen, und obwohl Andrey Tarantsovs Antwort definitiv viele nützliche Informationen enthält, um zu verstehen, wie es UIScrollViewsfunktioniert, halte ich die Lösung für etwas überkompliziert . Meine Lösung, die keine Unterklassen oder Touch-Weiterleitung umfasst, lautet wie folgt:

  1. Erstellen Sie zwei verschachtelte Bildlaufansichten, eine für jede Richtung.
  2. Erstellen Sie zwei Dummy UIPanGestureRecognizers, wieder einen für jede Richtung.
  3. Erstellen Sie mithilfe des Selektors UIScrollViewsFehlerabhängigkeiten zwischen den Eigenschaften 'panGestureRecognizer' und dem Dummy UIPanGestureRecognizer, der der Richtung senkrecht zur gewünschten Bildlaufrichtung Ihres UIScrollView entspricht UIGestureRecognizer's -requireGestureRecognizerToFail:.
  4. -gestureRecognizerShouldBegin:Berechnen Sie in den Rückrufen für Ihre Dummy-UIPanGestureRecognizers die Anfangsrichtung des Schwenks mit dem Selektor -translationInView: der Geste (IhrUIPanGestureRecognizers rufen diesen Delegaten-Rückruf erst auf, wenn Ihre Berührung genug übersetzt hat, um sich als Pfanne zu registrieren). Ermöglichen oder verbieten Sie den Beginn Ihrer Gestenerkennung in Abhängigkeit von der berechneten Richtung, die wiederum steuern soll, ob der senkrechte UIScrollView-Pan-Gestenerkenner beginnen darf oder nicht.
  5. In -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:nicht erlauben Dummy UIPanGestureRecognizersneben Scrollen (es sei denn , Sie haben einige zusätzliche Verhalten , das Sie hinzufügen wollten) zu laufen.

2

Ich habe ein Paging-UIScrollView mit einem Rahmen erstellt, der der Größe des Ansichtsbildschirms entspricht, und die Inhaltsgröße auf die für alle meine Inhalte erforderliche Breite und eine Höhe von 1 festgelegt (danke Tonetel). Dann habe ich Menü-UIScrollView-Seiten mit Frames erstellt, die auf jede Seite, die Größe des Ansichtsbildschirms und die Inhaltsgröße der einzelnen Seiten auf die Breite des Bildschirms und die erforderliche Höhe für jede Seite eingestellt sind. Dann habe ich einfach jede der Menüseiten zur Paging-Ansicht hinzugefügt. Stellen Sie sicher, dass das Paging in der Paging-Bildlaufansicht aktiviert, in den Menüansichten jedoch deaktiviert ist (sollte standardmäßig deaktiviert sein), und Sie können loslegen.

Die Paging-Bildlaufansicht scrollt jetzt horizontal, jedoch aufgrund der Inhaltshöhe nicht vertikal. Jede Seite wird aufgrund der Einschränkungen der Inhaltsgröße vertikal, aber nicht horizontal gescrollt.

Hier ist der Code, wenn diese Erklärung zu wünschen übrig lässt:

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

2

Danke Tonetel. Ich habe Ihren Ansatz leicht geändert, aber es war genau das, was ich brauchte, um horizontales Scrollen zu verhindern.

self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 1);

2

Dies ist eine verbesserte Lösung von icompot, die für mich ziemlich instabil war. Dieser funktioniert gut und ist einfach zu implementieren:

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

2

Zum späteren Nachschlagen habe ich auf der Antwort von Andrey Tanasov aufgebaut und die folgende Lösung gefunden, die gut funktioniert.

Definieren Sie zunächst eine Variable zum Speichern des contentOffset und setzen Sie sie auf 0,0 Koordinaten

var positionScroll = CGPointMake(0, 0)

Implementieren Sie nun Folgendes in den scrollView-Delegaten:

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
        }

    }

}

Hoffe das hilft.


1

Aus den Dokumenten:

"Der Standardwert ist NEIN. Dies bedeutet, dass das Scrollen sowohl in horizontaler als auch in vertikaler Richtung zulässig ist. Wenn der Wert JA lautet und der Benutzer mit dem Ziehen in eine allgemeine Richtung (horizontal oder vertikal) beginnt, deaktiviert die Bildlaufansicht das Scrollen in die andere Richtung. ""

Ich denke, der wichtige Teil ist "wenn ... der Benutzer beginnt , in eine allgemeine Richtung zu ziehen". Wenn sie also anfangen, diagonal zu ziehen, ist dies kein Problem. Nicht, dass diese Dokumente bei der Interpretation immer zuverlässig sind - aber das scheint zu dem zu passen, was Sie sehen.

Das scheint eigentlich sinnvoll. Ich muss fragen - warum wollen Sie sich zu jeder Zeit nur auf horizontal oder nur vertikal beschränken? Vielleicht ist UIScrollView nicht das richtige Tool für Sie?


1

Meine Ansicht besteht aus 10 horizontalen Ansichten, und einige dieser Ansichten bestehen aus mehreren vertikalen Ansichten, sodass Sie ungefähr Folgendes erhalten:


1.0   2.0   3.1    4.1
      2.1   3.1
      2.2
      2.3

Mit aktiviertem Paging sowohl auf der horizontalen als auch auf der vertikalen Achse

Die Ansicht weist außerdem die folgenden Attribute auf:

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

Gehen Sie nun folgendermaßen vor, um ein diagonales Scrollen zu verhindern:

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

Funktioniert wie ein Charme für mich!


1
Ich verstehe nicht, wie das für Sie funktionieren könnte ... könnten Sie ein Beispielprojekt hochladen?
Hfossli

1

_isHorizontalScrollMüssen in touchesEndedund auf NEIN zurückgesetzt werden touchesCancelled.



0

Wenn Sie dies in der Beta-Version 3.0 noch nicht versucht haben, empfehlen wir Ihnen, es einmal auszuprobieren. Ich verwende derzeit eine Reihe von Tabellenansichten in einer Bildlaufansicht und es funktioniert genau wie erwartet. (Nun, das Scrollen funktioniert sowieso. Diese Frage wurde gefunden, als nach einer Lösung für ein völlig anderes Problem gesucht wurde ...)


0

Für diejenigen, die in Swift die zweite Lösung von @AndreyTarantsov suchen, ist dies im Code wie folgt:

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

Was wir hier tun, ist, die aktuelle Position kurz vor dem Ziehen der Bildlaufansicht beizubehalten und dann zu überprüfen, ob x im Vergleich zur aktuellen Position von x zu- oder abgenommen hat. Wenn ja, setzen Sie pagingEnabled auf true, wenn nein (y hat zugenommen / abgenommen), setzen Sie pagingEnabled auf false

Hoffe das ist nützlich für Neuankömmlinge!


0

Ich habe es geschafft, dieses Problem zu lösen, indem ich scrollViewDidBeginDragging (_ :) implementiert und die Geschwindigkeit auf dem zugrunde liegenden UIPanGestureRecognizer untersucht habe.

NB: Bei dieser Lösung wird jede Pfanne, die diagonal gewesen wäre, von der scrollView ignoriert.

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

-3

Wenn Sie den Interface Builder zum Entwerfen Ihrer Schnittstelle verwenden, ist dies recht einfach.

Klicken Sie im Interface Builder einfach auf UIScrollView und wählen Sie die Option (im Inspektor) aus, die das Scrollen nur in eine Richtung beschränkt.


1
In IB gibt es hierfür keine Optionen. Die beiden Optionen, auf die Sie sich möglicherweise beziehen, haben mit der Anzeige der Bildlaufleiste zu tun, nicht mit dem eigentlichen Bildlauf. Darüber hinaus sperrt die Richtungssperre die Richtung erst, wenn der Bildlauf begonnen hat.
Sophtware
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.