Треба сказати, що у вас дуже важка ситуація.
Зверніть увагу, що 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.