Я должен сказать, что вы в очень сложной ситуации.
Обратите внимание, что вам нужно использовать UIScrollView с 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 вести себя таким образом?
Похоже, что отключение вертикальной прокрутки и установка delaysContentTouches
на NO должны заставить работать вложенные 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
.
Вторая стратегия: Если по какой-то причине вложенные UIScrollViews не работают или оказываются слишком сложными для получения прав, вы можете попробовать обойтись только одним UIScrollView, переключив его свойство pagingEnabled
на вылететь из вашего scrollViewDidScroll
метода делегата.
Чтобы предотвратить прокрутку по диагонали, сначала попробуйте запомнить contentOffset в scrollViewWillBeginDragging
, а также проверить и сбросить contentOffset внутри scrollViewDidScroll
, если вы обнаружите диагональное перемещение. Другая стратегия, которую нужно попробовать, - сбросить contentSize, чтобы включить прокрутку только в одном направлении, как только вы решите, в каком направлении движется палец пользователя. (UIScrollView, похоже, довольно прост, что возится с контентом contentSize и contentOffset из методов его делегатов.)
Если это не сработает или приведет к неаккуратным изображениям, вам придется переопределить touchesBegan
, touchesMoved
и т. Д. И не пересылать события диагонального перемещения в UIScrollView. (Однако пользовательский опыт в этом случае будет неоптимальным, потому что вам придется игнорировать диагональные движения вместо того, чтобы заставлять их двигаться в одном направлении. Если вы чувствуете себя действительно предприимчивым, вы можете написать свой собственный UITouch, похожий на RevengeTouch. Objective -C - обычный старый C, и в мире нет ничего более типичного для утки, чем C: пока никто не проверяет реальный класс объектов, что, как я полагаю, никто не делает, вы можете сделать любой класс похожим на любой другой класс. возможность синтезировать любые касания, которые вы хотите, с любыми желаемыми координатами.)
Стратегия резервного копирования: есть TTScrollView, разумное переопределение UIScrollView в Three20 library . К сожалению, это кажется очень неестественным и неифоничным для пользователя. Но если каждая попытка использования UIScrollView не удалась, вы можете вернуться к пользовательскому представлению прокрутки. Я рекомендую против этого, если это вообще возможно; использование UIScrollView гарантирует, что вы получите собственный внешний вид, независимо от того, как он будет развиваться в будущих версиях iPhone OS.
Хорошо, это маленькое эссе стало слишком длинным. Я все еще в UIScrollView играх после работы над ScrollingMadness несколько дней назад.
P.S. Если у вас получится что-то из этого, и вы захотите поделиться им, отправьте мне соответствующий код по адресу andreyvit@gmail.com, и я с радостью включу его в свой пакет ScrollingMadness трюков .
P.P.S. Добавление этого небольшого эссе в README ScrollingMadness.