UIScrollView: листание по горизонтали, прокрутка по вертикали?

88

Как я могу заставить объект, UIScrollViewв котором включены листание и прокрутка, перемещаться только вертикально или горизонтально в данный момент?

Насколько я понимаю, это directionalLockEnabledсвойство должно обеспечивать это, но диагональное смахивание по-прежнему заставляет представление прокручиваться по диагонали вместо ограничения движения по одной оси.

Изменить: чтобы было понятнее, я хотел бы разрешить пользователю прокручивать по горизонтали ИЛИ по вертикали, но не то и другое одновременно.

Cœur
источник
Вы хотите ограничить прокрутку вида ТОЛЬКО по горизонтали / вертикали или вы хотите, чтобы пользователь прокручивал по горизонтали ИЛИ по вертикали, но не по обоим одновременно?
Уилл Хартунг
Не могли бы вы сделать мне одолжение и изменить название, чтобы оно выглядело немного интереснее и нетривиально? Что-то вроде «UIScrollView: листание по горизонтали, прокрутка по вертикали?»
Андрей Таранцов
К сожалению, я разместил вопрос как незарегистрированный пользователь на компьютере, к которому у меня больше нет доступа (до регистрации под тем же именем, когда я вернулся домой), что означает, что я не могу его редактировать. Это также должно объяснить мне, что я должен следовать ниже, а не редактировать исходный вопрос. Приносим извинения за нарушение этикета Stack на этот раз!
Ник

Ответы:

117

Должен сказать, вы в очень сложной ситуации.

Обратите внимание, что вам нужно использовать UIScrollView with pagingEnabled=YESдля переключения между страницами, но вам нужно pagingEnabled=NOпрокручивать по вертикали.

Есть 2 возможные стратегии. Я не знаю, какой из них будет работать / проще реализовать, поэтому попробуйте оба.

Во-первых: вложенные UIScrollViews. Честно говоря, я еще не видел человека, у которого это работало бы. Однако лично я недостаточно старался, и моя практика показывает, что когда вы действительно стараетесь, вы можете заставить UIScrollView делать все, что захотите .

Таким образом, стратегия состоит в том, чтобы позволить внешнему виду прокрутки обрабатывать только горизонтальную прокрутку, а внутренним видам прокрутки - только вертикальной прокрутке. Для этого вы должны знать, как UIScrollView работает внутри. Он переопределяет hitTestметод и всегда возвращает себя, так что все события касания попадают в UIScrollView. Затем внутри touchesBeganи touchesMovedт.д. он проверяет, заинтересовано ли событие в событии, и либо обрабатывает, либо передает его внутренним компонентам.

Чтобы решить, обрабатывать или пересылать касание, UIScrollView запускает таймер при первом касании:

  • Если вы не переместили палец значительно в течение 150 мс, он передает событие во внутреннее представление.

  • Если вы значительно переместили палец в течение 150 мс, он начинает прокрутку (и никогда не передает событие во внутреннее представление).

    Обратите внимание: когда вы касаетесь таблицы (которая является подклассом режима прокрутки) и сразу начинаете прокрутку, строка, к которой вы прикоснулись, никогда не выделяется.

  • Если вы не переместили палец значительно в течение 150 мс, и UIScrollView начал передавать события во внутреннее представление, но затем вы переместили палец достаточно далеко, чтобы началась прокрутка, UIScrollView вызывает touchesCancelledвнутреннее представление и начинает прокрутку.

    Обратите внимание: когда вы касаетесь таблицы, немного удерживаете палец и затем начинаете прокрутку, строка, к которой вы прикоснулись, выделяется первой, но затем снимается.

Эта последовательность событий может быть изменена конфигурацией UIScrollView:

  • Если delaysContentTouchesНЕТ, то таймер не используется - события сразу переходят во внутренний элемент управления (но затем отменяются, если вы перемещаете палец достаточно далеко)
  • Если cancelsTouchesНЕТ, то после отправки событий в элемент управления прокрутка никогда не будет.

Обратите внимание , что UIScrollView , который получает все touchesBegin , touchesMoved, touchesEndedи touchesCanceledсобытия из CocoaTouch (потому что его hitTestговорит , что это делать). Затем он направляет их к внутреннему взгляду, если хочет, пока хочет.

Теперь, когда вы знаете все о UIScrollView, вы можете изменить его поведение. Могу поспорить, вы хотите отдать предпочтение вертикальной прокрутке, чтобы, как только пользователь коснулся представления и начал двигать пальцем (даже немного), представление начало прокручиваться в вертикальном направлении; но когда пользователь перемещает палец в горизонтальном направлении достаточно далеко, вы хотите отменить вертикальную прокрутку и начать горизонтальную прокрутку.

Вы хотите создать подкласс своего внешнего UIScrollView (скажем, вы называете свой класс RemorsefulScrollView), чтобы вместо поведения по умолчанию он немедленно перенаправлял все события во внутреннее представление и прокручивался только при обнаружении значительного горизонтального движения.

Как заставить RemorsefulScrollView вести себя таким образом?

  • Похоже, отключение вертикальной прокрутки и установка значения delaysContentTouchesNO должны заставить работать вложенные UIScrollViews. К сожалению, это не так; UIScrollView, похоже, выполняет некоторую дополнительную фильтрацию для быстрых движений (которые нельзя отключить), так что даже если UIScrollView можно прокручивать только по горизонтали, он всегда будет поглощать (и игнорировать) достаточно быстрые вертикальные движения.

    Эффект настолько серьезен, что вертикальную прокрутку внутри вложенного представления прокрутки невозможно использовать. (Похоже, у вас есть именно такая настройка, поэтому попробуйте: удерживайте палец в течение 150 мс, а затем перемещайте его в вертикальном направлении - тогда вложенный UIScrollView работает так, как ожидалось!)

  • Это означает, что вы не можете использовать код UIScrollView для обработки событий; вам нужно переопределить все четыре метода обработки касания в RemorsefulScrollView и сначала выполнить свою собственную обработку, перенаправляя событие только в super(UIScrollView), если вы решили использовать горизонтальную прокрутку.

  • Однако вы должны пройти touchesBeganк UIScrollView, потому что вы хотите, чтобы помнить основание координат для будущей горизонтальной прокрутки (если позже вы решите , что это горизонтальная прокрутка). Вы не сможете отправить touchesBeganв UIScrollView позже, потому что вы не можете сохранить touchesаргумент: он содержит объекты, которые будут изменены до следующего touchesMovedсобытия, и вы не можете воспроизвести старое состояние.

    Таким образом, вы должны touchesBeganнемедленно перейти к UIScrollView, но вы будете скрывать touchesMovedот него любые дальнейшие события, пока не решите прокрутить по горизонтали. Нет touchesMovedозначает отсутствие прокрутки, поэтому начальная touchesBeganбуква не повредит. Но установите delaysContentTouchesзначение NO, чтобы никакие дополнительные таймеры неожиданности не мешали.

    (Offtopic - в отличие от вас, UIScrollView может правильно сохранять касания и может воспроизводить и пересылать исходное touchesBeganсобытие позже. Он имеет несправедливое преимущество использования неопубликованных API-интерфейсов, поэтому может клонировать сенсорные объекты до того, как они будут видоизменены.)

  • Учитывая, что вы всегда вперед touchesBegan, вам также нужно вперед touchesCancelledи touchesEnded. Вы должны включить touchesEndedв touchesCancelled, однако, поскольку UIScrollView будет интерпретировать touchesBegan, touchesEndedпоследовательность как сенсорную мышь, и направите его на внутренний взгляд. Вы уже сами пересылаете нужные события, поэтому вы никогда не хотите, чтобы UIScrollView что-либо пересылал.

В основном это псевдокод того, что вам нужно сделать. Для простоты я никогда не разрешаю горизонтальную прокрутку после того, как произошло событие мультитач.

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

Я не пытался запустить или даже скомпилировать это (и набрал весь класс в текстовом редакторе), но вы можете начать с вышеизложенного и, надеюсь, заставить его работать.

Единственная скрытая уловка, которую я вижу, заключается в том, что если вы добавляете какие-либо дочерние представления, не относящиеся к UIScrollView, в RemorsefulScrollView, события касания, которые вы пересылаете дочернему элементу, могут возвращаться к вам через цепочку респондентов, если ребенок не всегда обрабатывает касания, как это делает UIScrollView. Пуленепробиваемая реализация RemorsefulScrollView защитит от touchesXxxповторного входа.

Вторая стратегия: если по какой-то причине вложенные UIScrollView не работают или оказываются слишком сложными для правильного выполнения, вы можете попробовать обойтись только одним UIScrollView, переключая его pagingEnabledсвойство на лету из вашего scrollViewDidScrollметода делегата.

Чтобы предотвратить диагональную прокрутку, вы должны сначала попытаться запомнить contentOffset in scrollViewWillBeginDragging, а также проверить и сбросить contentOffset внутри, scrollViewDidScrollесли вы обнаружите диагональное движение. Еще одна стратегия, которую следует попробовать, - сбросить contentSize, чтобы разрешить прокрутку только в одном направлении, после того как вы решите, в каком направлении движется палец пользователя. (UIScrollView кажется довольно снисходительным к возням с contentSize и contentOffset из его методов делегата.)

Если это не работает или приводит к неаккуратным визуальным эффектам, вам необходимо переопределить touchesBeganи touchesMovedт. Д., А не пересылать события диагонального движения в UIScrollView. (Однако в этом случае пользовательский опыт будет неоптимальным, потому что вам придется игнорировать диагональные движения вместо того, чтобы направлять их в одном направлении. Если вы действительно любите приключения, вы можете написать свой собственный UITouch-аналог, что-то вроде RevengeTouch. Цель -C - это старый добрый C, и в мире нет ничего более утиного типа, чем C; пока никто не проверяет реальный класс объектов, что, как я полагаю, никто не делает, вы можете сделать любой класс похожим на любой другой класс. Это открывает возможность синтезировать любые прикосновения, какие захотите, с любыми координатами.)

Стратегия резервного копирования: есть TTScrollView, разумная реализация UIScrollView в библиотеке Three20 . К сожалению, пользователю это кажется очень неестественным и неуместным. Но если каждая попытка использования UIScrollView терпит неудачу, вы можете вернуться к просмотру прокрутки с настраиваемым кодом. Я действительно не рекомендую этого делать, если это вообще возможно; использование UIScrollView гарантирует, что вы получите естественный внешний вид, независимо от того, как он будет развиваться в будущих версиях iPhone OS.

Хорошо, это небольшое эссе получилось слишком длинным. Я просто все еще увлекаюсь играми UIScrollView после работы над ScrollingMadness несколько дней назад.

PS Если у вас что-то из этого работает и вы захотите поделиться, пришлите мне соответствующий код по электронной почте на адрес andreyvit@gmail.com, я с радостью добавлю его в свой набор трюков ScrollingMadness .

PPS Добавление этого небольшого эссе в README ScrollingMadness.

Андрей Таранцов
источник
7
Обратите внимание, что это вложенное поведение прокрутки теперь работает прямо из коробки в SDK, никаких забавных дел не требуется
andygeers
1
+1 комментарий andygeers, потом понял, что это не так; вы все еще можете «сломать» обычный UIScrollView, перетащив его по диагонали от начальной точки, а затем вы «потянули скроллер, чтобы потерять», и он будет игнорировать блокировку направления, пока вы не отпустите и не закончите бог знает где.
Kalle
Спасибо за отличное закулисное объяснение! Я пошел с решением Маттиаса Вадмана ниже, которое выглядит немного менее инвазивным IMO.
Ортвин Генц
Было бы здорово, если бы этот ответ был обновлен сейчас, когда UIScrollView предоставляет свой распознаватель жестов панорамирования. (Но, возможно, это выходит за рамки первоначального объема.)
jtbandes
Есть ли возможность обновить этот пост? Отличный пост и спасибо за ваш вклад.
Паван
56

Это работает для меня каждый раз ...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);
Tonetel
источник
20
Для всех, кто сталкивается с этим вопросом: я думаю, что этот ответ был опубликован до редактирования, которое прояснило вопрос. Это позволит вам прокручивать только по горизонтали, это не решает проблему возможности прокрутки по вертикали или горизонтали, но не по диагонали.
Andygeers
Для меня работает как оберег. У меня есть очень длинный горизонтальный scrollView с разбиением на страницы и UIWebView на каждой странице, который обрабатывает нужную мне вертикальную прокрутку.
Tom Redman
Превосходно! Но может кто-нибудь объяснить, как это работает (установив высоту contentSize на 1)!
Praveen
Это действительно работает, но почему и как это работает? Кто-нибудь может понять это?
Марко Франсекович
27

Для таких ленивых, как я:

Установить scrollview.directionalLockEnabledнаYES

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

Я использовал следующий метод для решения этой проблемы, надеюсь, он вам поможет.

Сначала определите следующие переменные в файле заголовка вашего контроллера.

CGPoint startPos;
int     scrollDirection;

startPos сохранит значение contentOffset, когда ваш делегат получит сообщение scrollViewWillBeginDragging . Итак, в этом методе мы делаем следующее;

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

затем мы используем эти значения для определения предполагаемого пользователем направления прокрутки в сообщении 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];
    }
 }

наконец, мы должны остановить все операции обновления, когда пользователь завершает перетаскивание;

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (decelerate) {
       scrollDirection=3;
    }
 }
Йилдирай Мерич
источник
Хм ... На самом деле, после дополнительного исследования - он работает довольно хорошо, но проблема в том, что он теряет контроль над направлением во время фазы замедления - поэтому, если вы начнете двигать пальцем по диагонали, он будет двигаться только по горизонтали или вертикали. пока вы не отпустите этот момент, он на некоторое время будет замедляться в диагональном направлении
Andygeers
@andygeers, это могло бы работать лучше, если бы - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (decelerate) { scrollDirection=3; } }было изменено на - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (!decelerate) { scrollDirection=0; } }и- (void)scrollViewDidEndDecelerating{ scrollDirection=0; }
cduck
У меня не получилось. Установка contentOffset в полете, похоже, разрушает поведение разбиения на страницы. Я пошел с решением Маттиаса Вадмана.
Ортвин Генц
13

Я мог бы здесь копать, но сегодня я наткнулся на этот пост, когда пытался решить ту же проблему. Вот мое решение, похоже, отлично работает.

Я хочу отобразить полный экран содержимого, но разрешить пользователю прокручивать до одного из 4 других экранов, вверх вниз влево и вправо. У меня есть один UIScrollView размером 320x480 и contentSize 960x1440. Смещение содержимого начинается с 320 480. В scrollViewDidEndDecelerating: я перерисовываю 5 представлений (центральные представления и 4 вокруг него) и сбрасываю смещение содержимого на 320 480.

Вот суть моего решения - метод 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);
    }
}
Джеймс Дж.
источник
Вы, вероятно, обнаружите, что этот код не "замедляется", когда пользователь отпускает режим прокрутки - он останавливается. Если вы не хотите замедления, это может вам подойти.
Andygeers
4

Ребята, это так же просто, как сделать высоту подпредставления такой же, как высоту прокрутки и высоту размера содержимого. Тогда вертикальная прокрутка отключена.

Юлианлубенов
источник
2
Для всех, кто сталкивается с этим вопросом: я думаю, что этот ответ был опубликован до редактирования, которое прояснило вопрос. Это позволит вам прокручивать только по горизонтали, это не решает проблему возможности прокрутки по вертикали или горизонтали, но не по диагонали.
Andygeers
3

Вот моя реализация страничной страницы, UIScrollViewкоторая разрешает только ортогональные прокрутки. Обязательно установитеpagingEnabled на 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
Отлично работает, в том числе на iOS 7. Спасибо!
Ортвин Генц
3

Мне также приходилось решать эту проблему, и хотя ответ Андрея Таранцова определенно содержит много полезной информации для понимания того UIScrollViews, как работает, я считаю, что решение немного сложнее. Мое решение, которое не включает подклассы или переадресацию касаний, выглядит следующим образом:

  1. Создайте два вложенных представления прокрутки, по одному для каждого направления.
  2. Создайте два манекена UIPanGestureRecognizers, снова по одному для каждого направления.
  3. Создайте зависимости сбоев между UIScrollViewsсвойствами panGestureRecognizer и фиктивным элементом, UIPanGestureRecognizerсоответствующим направлению, перпендикулярному желаемому направлению прокрутки UIScrollView, с помощью UIGestureRecognizer's -requireGestureRecognizerToFail:селектора.
  4. В -gestureRecognizerShouldBegin:обратных вызовах для ваших фиктивных UIPanGestureRecognizers вычислите начальное направление панорамирования с помощью селектора -translationInView: для жеста (вы UIPanGestureRecognizersне будете вызывать этот обратный вызов делегата, пока ваше прикосновение не переведет достаточно, чтобы зарегистрироваться как панорама). Разрешить или запретить запуск распознавателя жестов в зависимости от рассчитанного направления, которое, в свою очередь, должно определять, разрешено ли запускать перпендикулярный распознаватель жестов панорамирования UIScrollView.
  5. В -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:, не позволяют вашему манекену , UIPanGestureRecognizersчтобы работать вместе с прокруткой (если у вас есть некоторое дополнительное поведение , которое вы хотите добавить).
Курт Хоримото
источник
2

Что я сделал, так это создал страницу UIScrollView с рамкой размером с экран просмотра и установил размер содержимого равным ширине, необходимой для всего моего содержимого, и высоте 1 (спасибо Tonetel). Затем я создал меню UIScrollView pages с фреймами, установленными для каждой страницы, размером экрана просмотра, и установил размер содержимого каждой из них равным ширине экрана и необходимой высоте для каждой. Затем я просто добавил каждую страницу меню к просмотру страниц. Убедитесь, что разбиение на страницы включено в просмотре прокрутки, но отключено в представлениях меню (по умолчанию должно быть отключено), и все должно быть в порядке.

Вид прокрутки страницы теперь прокручивается по горизонтали, но не по вертикали из-за высоты содержимого. Каждая страница прокручивается по вертикали, но не по горизонтали из-за ограничений размера содержимого.

Вот код, если это объяснение оставляло желать лучшего:

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

Спасибо, Tonetel. Я немного изменил ваш подход, но это было именно то, что мне нужно, чтобы предотвратить горизонтальную прокрутку.

self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 1);
Джереми
источник
2

Это улучшенное решение icompot, которое было для меня довольно нестабильным. Этот отлично работает, и его легко реализовать:

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

На будущее я основывался на ответе Андрея Танасова и придумал следующее решение, которое отлично работает.

Сначала определите переменную для хранения contentOffset и установите для нее координаты 0,0

var positionScroll = CGPointMake(0, 0)

Теперь реализуйте в делегате 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
        }

    }

}

Надеюсь это поможет.

Бенджамин
источник
1

Из документов:

"значение по умолчанию - НЕТ, что означает, что прокрутка разрешена как в горизонтальном, так и в вертикальном направлениях. Если значение ДА и пользователь начинает перетаскивание в одном общем направлении (горизонтальном или вертикальном), вид прокрутки отключает прокрутку в другом направлении. "

Я думаю, что важная часть: «если ... пользователь начинает перетаскивать в одном общем направлении». Так что, если они начнут перетаскивать по диагонали, это не сработает. Не то чтобы эти документы всегда надежны, когда дело доходит до интерпретации, но похоже, что это соответствует тому, что вы видите.

Это действительно кажется разумным. Я должен спросить - почему вы хотите ограничиться только горизонтальным или только вертикальным в любое время? Возможно, UIScrollView не для вас?

Philsquared
источник
1

Мое представление состоит из 10 горизонтальных представлений, а некоторые из этих представлений состоят из нескольких вертикальных представлений, поэтому вы получите что-то вроде этого:


1.0   2.0   3.1    4.1
      2.1   3.1
      2.2
      2.3

При включенном разбиении по горизонтальной и вертикальной оси

Представление также имеет следующие атрибуты:

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

Теперь, чтобы предотвратить диагональную прокрутку, сделайте следующее:

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

Для меня работает как шарм!

user422756
источник
1
не понимаю, как это могло сработать ... не могли бы вы загрузить образец проекта?
hfossli
1

Нужно сбросить _isHorizontalScrollна НЕТ в touchesEndedи touchesCancelled.

Правин Матанам
источник
0

Если вы не пробовали сделать это в бета-версии 3.0, я рекомендую вам попробовать. В настоящее время я использую серию табличных представлений внутри представления прокрутки, и он работает так, как ожидалось. (Во всяком случае, прокрутка есть. Обнаружил этот вопрос, когда искал решение совершенно другой проблемы ...)

Тим Китинг
источник
0

Для тех, кто ищет в Swift во втором решении @AndreyTarantsov, его код выглядит следующим образом:

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

здесь мы сохраняем текущую позицию непосредственно перед перетаскиванием scrollview, а затем проверяем, увеличился или уменьшился x по сравнению с текущей позицией x. Если да, то установите для pagingEnabled значение true, если нет (y увеличилось / уменьшилось), установите для pagingEnabled значение false.

Надеюсь, это пригодится новичкам!

jonprasetyo
источник
0

Мне удалось решить эту проблему, реализовав scrollViewDidBeginDragging (_ :) и посмотрев на скорость базового UIPanGestureRecognizer.

NB: с этим решением каждая панорама, которая была бы диагональной, будет игнорироваться 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
    }
}
Мортен Саабье Кристенсен
источник
-3

Если вы используете конструктор интерфейсов для разработки своего интерфейса - это можно сделать довольно легко.

В построителе интерфейса просто щелкните UIScrollView и выберите параметр (в инспекторе), который ограничивает его только односторонней прокруткой.

Zpesk
источник
1
В IB нет возможности сделать это. Возможно, вы имеете в виду два параметра, которые связаны с отображением полосы прокрутки, а не с самой прокруткой. Кроме того, функция фиксации направления блокирует направление только после начала прокрутки.
Sophtware