Вы должны в очень сложной ситуации, я должен сказать.
Обратите внимание, что для переключения между страницами необходимо использовать UIScrollView с pagingEnabled=YES
, но вам нужно, чтобы вы могли прокручивать по вертикали в pagingEnabled=NO
.
Существует 2 возможных стратегии. Я не знаю, какой из них будет работать/проще реализовать, поэтому попробуйте оба.
Первый: Вложенные UIScrollViews. Честно говоря, я еще должен увидеть человека, у которого есть это, чтобы работать. Однако я не очень старался лично, и моя практика показывает, что, когда вы достаточно стараетесь, вы можете make UIScrollView do anything you want.
Таким образом, стратегия заключается в том, чтобы внешний вид прокрутки обрабатывал только горизонтальную прокрутку, а внутренние виды прокрутки обрабатывали только вертикальную прокрутку. Для этого вы должны знать, как работает UIScrollView. Он переопределяет метод hitTest
и всегда возвращает себя, так что все события касания переходят в UIScrollView. Затем внутри touchesBegan
, touchesMoved
и т. Д. Он проверяет, заинтересован ли он в событии, и либо обрабатывает, либо передает его внутренним компонентам.
Чтобы решить, если на ощупь будет обрабатываться или быть перенаправлены, UIScrollView запускает таймер, когда вы первый потрогать:
Если вы не переместили свой палец значительно в 150ms, он передает событие на внутренний вид.
Если вы перенесли палец значительно в течение 150 мс, он начинает прокрутку (и никогда не передает событие во внутренний вид).
Обратите внимание, что когда вы касаетесь таблицы (которая является подклассом прокрутки) и начинайте прокрутку сразу, строка, которую вы коснулись, никогда не выделяется.
Если у вас есть не переместили свой палец значительно в 150ms и 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
, а также проверить и сбросить содержимоеОфис внутри scrollViewDidScroll
, если вы обнаружите диагональное движение. Еще одна стратегия - сбросить contentSize, чтобы включить только прокрутку в одном направлении, как только вы решите, в каком направлении движется палец пользователя.(UIScrollView, кажется, довольно снисходителен о возился с contentSize и contentOffset из его методов делегата.)
Если это не работает, либо или приводит к неаккуратным визуальным, вы должны переопределить touchesBegan
, touchesMoved
и т.д., а не вперед диагональные события движения в UIScrollView. (В этом случае пользовательский опыт будет субоптимальным, потому что вам придется игнорировать диагональные движения, а не форсировать их в одном направлении. Если вы чувствуете себя очень авантюрно, вы можете написать свой собственный UITouch-подобный, что-то вроде RevengeTouch. -C - простой старый C, и в мире нет ничего глупого, кроме C, пока никто не проверяет реальный класс объектов, который, как я полагаю, никто не делает, вы можете сделать любой класс похожим на любой другой класс. возможность синтезировать любые штрихи, которые вы хотите, с любой координаты вы хотите)
стратегии резервного копирования:. есть TTScrollView, здравомыслящий перевыполнение UIScrollView в Three20 library. К сожалению, он чувствует себя очень неестественным и не-iphonish для пользователя. Но если каждая попытка использования UIScrollView терпит неудачу, вы можете вернуться к пользовательскому кодированному представлению прокрутки. Я рекомендую против этого, если это вообще возможно; использование UIScrollView гарантирует, что вы получаете естественный внешний вид, независимо от того, как он развивается в будущих версиях ОС для iPhone.
Хорошо, это небольшое эссе получилось немного слишком длинным. Несколько дней назад я просто побывал в играх UIScrollView после работы над ScrollingMadness.
P.S. Если вы получите какое-либо из этих действий и почувствуете, что хотите поделиться, напишите мне соответствующий код на [email protected], я бы с радостью включил его в мой ScrollingMadness bag of tricks.
P.P.S. Добавление этого небольшого эссе в ScrollingMadness README.
Вы хотите ограничить просмотр, который будет прокручиваться ТОЛЬКО Horiz/Vert, или вы хотите, чтобы пользователь прокрутил Horiz OR Vert, но не оба одновременно? –
Можете ли вы сделать мне одолжение и изменить название, чтобы оно выглядело немного интереснее и нетривиально? Что-то вроде «UIScrollView: подкачка по горизонтали, прокрутка по вертикали?» –
К сожалению, я разместил вопрос как незарегистрированный пользователь на компьютере, к которому у меня больше нет доступа (до регистрации под тем же именем, когда я вернулся домой), что означает, t отредактируйте его. Это также должно объяснять мне следующий снизу, вместо редактирования исходного вопроса. Извините за нарушение стэкового этикета на этот раз! – Nick