Должен сказать, вы в очень сложной ситуации.
Обратите внимание, что вам нужно использовать 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 вести себя таким образом?
Похоже, отключение вертикальной прокрутки и установка значения 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 что-либо пересылал.
В основном это псевдокод того, что вам нужно сделать. Для простоты я никогда не разрешаю горизонтальную прокрутку после того, как произошло событие мультитач.
@interface RemorsefulScrollView : UIScrollView {
CGPoint _originalPoint;
BOOL _isHorizontalScroll, _isMultitouch;
UIView *_currentChild;
}
@end
#define kThresholdX 12.0f
#define kThresholdY 4.0f
@implementation RemorsefulScrollView
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.delaysContentTouches = NO;
}
return self;
}
- (id)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
self.delaysContentTouches = NO;
}
return self;
}
- (UIView *)honestHitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = nil;
for (UIView *child in self.subviews)
if ([child pointInside:point withEvent:event])
if ((result = [child hitTest:point withEvent:event]) != nil)
break;
return result;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
if (_isHorizontalScroll)
return;
if ([touches count] == [[event touchesForView:self] count]) {
_originalPoint = [[touches anyObject] locationInView:self];
_currentChild = [self honestHitTest:_originalPoint withEvent:event];
_isMultitouch = NO;
}
_isMultitouch |= ([[event touchesForView:self] count] > 1);
[_currentChild touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (!_isHorizontalScroll && !_isMultitouch) {
CGPoint point = [[touches anyObject] locationInView:self];
if (fabsf(_originalPoint.x - point.x) > kThresholdX && fabsf(_originalPoint.y - point.y) < kThresholdY) {
_isHorizontalScroll = YES;
[_currentChild touchesCancelled:[event touchesForView:self] withEvent:event]
}
}
if (_isHorizontalScroll)
[super touchesMoved:touches withEvent:event];
else
[_currentChild touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (_isHorizontalScroll)
[super touchesEnded:touches withEvent:event];
else {
[super touchesCancelled:touches withEvent:event];
[_currentChild touchesEnded:touches withEvent:event];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
if (!_isHorizontalScroll)
[_currentChild touchesCancelled:touches withEvent:event];
}
@end
Я не пытался запустить или даже скомпилировать это (и набрал весь класс в текстовом редакторе), но вы можете начать с вышеизложенного и, надеюсь, заставить его работать.
Единственная скрытая уловка, которую я вижу, заключается в том, что если вы добавляете какие-либо дочерние представления, не относящиеся к 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.