Треба сказати, що у вас дуже важка ситуація.
Зверніть увагу, що pagingEnabled=YESдля перемикання між сторінками вам потрібно використовувати UIScrollView , але вам потрібно 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значення НІ повинно змусити вкладені UIScrollViews працювати. На жаль, це не так; Здається, UIScrollView виконує додаткову фільтрацію для швидких рухів (які неможливо відключити), так що навіть якщо UIScrollView можна прокручувати лише горизонтально, він завжди буде з'їдати (і ігнорувати) досить швидкі вертикальні рухи.
Ефект настільки сильний, що вертикальна прокрутка всередині вкладеного подання прокрутки непридатна. (Здається, у вас точно така настройка, тому спробуйте: утримуйте палець протягом 150 мс, а потім рухайте його у вертикальному напрямку - вкладений UIScrollView працює, як очікувалося тоді!)
Це означає, що ви не можете використовувати код UIScrollView для обробки подій; вам потрібно перевизначити всі чотири методи обробки дотиків у RemorsefulScrollView і спочатку виконати власну обробку, переадресувавши подію до super(UIScrollView), якщо ви вирішили використовувати горизонтальну прокрутку.
Однак вам доведеться перейти touchesBeganдо UIScrollView, оскільки ви хочете, щоб він запам'ятав базову координату для майбутньої горизонтальної прокрутки (якщо пізніше ви вирішите, що це горизонтальна прокрутка). touchesBeganПізніше ви не зможете надіслати до UIScrollView, оскільки ви не можете зберегти touchesаргумент: він містить об’єкти, які будуть мутовані до наступної touchesMovedподії, і ви не зможете відтворити старий стан.
Тому вам слід негайно перейти touchesBeganдо UIScrollView, але ви будете приховувати touchesMovedвід нього всі подальші події, поки не вирішите прокрутити горизонтально. Немає touchesMovedні в якому разі НЕ прокрутка, так що початковий touchesBeganне буде ніякого шкоди. Але встановіть delaysContentTouchesзначення НІ, щоб ніякі додаткові таймери сюрпризів не заважали.
(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
Я не намагався запустити або навіть скомпілювати це (і набрав увесь клас у звичайному текстовому редакторі), але ви можете почати з вищезазначеного і, сподіваюся, змусити його працювати.
Єдина прихована фішка, яку я бачу, полягає в тому, що якщо ви додасте до RemorsefulScrollView будь-які дочірні перегляди, які не є UIScrollView, події дотику, які ви пересилаєте дитині, можуть повернутися вам через ланцюжок відповідей, якщо дитина не завжди обробляє дотики, як це робить UIScrollView. Куленепробивна реалізація RemorsefulScrollView захистить від touchesXxxповторного входу .
Друга стратегія: якщо з якихось причин вкладені UIScrollViews не спрацьовують або виявляються занадто важкими, щоб отримати правильне рішення, ви можете спробувати порозумітися лише з одним 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.
Гаразд, цей невеликий нарис став занадто довгим. Я просто все ще користуюся іграми UIScrollView після роботи над ScrollingMadness кілька днів тому.
PS Якщо ви отримуєте якісь - небудь з цих робочих і відчувати себе , як обмін, будь ласка , по електронній пошті мені відповідний код на andreyvit@gmail.com, я б з задоволенням включити його в мою ScrollingMadness хитрощі .
PPS Додавання цього невеликого есе до ScrollingMadness README.