Tôi phải nói rằng bạn đang ở trong một tình huống rất khó khăn.
Lưu ý rằng bạn cần sử dụng UIScrollView pagingEnabled=YES
để chuyển đổi giữa các trang, nhưng bạn cần pagingEnabled=NO
cuộn theo chiều dọc.
Có 2 chiến lược khả thi. Tôi không biết cái nào sẽ hoạt động / dễ thực hiện hơn, vì vậy hãy thử cả hai.
Đầu tiên: UIScrollViews lồng nhau. Thành thật mà nói, tôi vẫn chưa thấy một người nào có thể làm việc này. Tuy nhiên, cá nhân tôi vẫn chưa cố gắng đủ và thực tế của tôi cho thấy rằng khi bạn cố gắng đủ nhiều, bạn có thể khiến UIScrollView làm được bất cứ điều gì bạn muốn .
Vì vậy, chiến lược là để chế độ xem cuộn bên ngoài chỉ xử lý cuộn ngang và chế độ xem cuộn bên trong chỉ xử lý cuộn dọc. Để thực hiện được điều đó, bạn phải biết cách UIScrollView hoạt động nội bộ. Nó ghi đè hitTest
phương thức và luôn trả về chính nó, để tất cả các sự kiện chạm đều được đưa vào UIScrollView. Sau đó, bên trong touchesBegan
, touchesMoved
v.v. nó kiểm tra xem nó có quan tâm đến sự kiện hay không và xử lý hoặc chuyển nó cho các thành phần bên trong.
Để quyết định xem thao tác chạm sẽ được xử lý hay được chuyển tiếp, UIScrollView bắt đầu hẹn giờ khi bạn chạm vào nó lần đầu tiên:
Nếu bạn không di chuyển ngón tay đáng kể trong vòng 150 mili giây, nó sẽ chuyển sự kiện sang chế độ xem bên trong.
Nếu bạn đã di chuyển ngón tay đáng kể trong vòng 150 mili giây, nó sẽ bắt đầu cuộn (và không bao giờ chuyển sự kiện sang chế độ xem bên trong).
Lưu ý cách khi bạn chạm vào bảng (là lớp con của chế độ xem cuộn) và bắt đầu cuộn ngay lập tức, hàng bạn đã chạm sẽ không bao giờ được đánh dấu.
Nếu bạn chưa di chuyển ngón tay đáng kể trong vòng 150 mili giây và UIScrollView bắt đầu chuyển các sự kiện vào chế độ xem bên trong, nhưng sau đó bạn đã di chuyển ngón tay đủ xa để quá trình cuộn bắt đầu, UIScrollView sẽ gọi touchesCancelled
đến chế độ xem bên trong và bắt đầu cuộn.
Lưu ý cách khi bạn chạm vào bảng, giữ ngón tay của bạn một chút và sau đó bắt đầu cuộn, hàng mà bạn chạm vào sẽ được đánh dấu đầu tiên, nhưng được đánh dấu sau đó.
Chuỗi sự kiện này có thể được thay đổi bằng cấu hình của UIScrollView:
- Nếu
delaysContentTouches
là KHÔNG, thì không có bộ đếm thời gian nào được sử dụng - các sự kiện ngay lập tức chuyển đến bộ điều khiển bên trong (nhưng sau đó sẽ bị hủy nếu bạn di chuyển ngón tay đủ xa)
- Nếu
cancelsTouches
là KHÔNG, thì khi các sự kiện được gửi đến một điều khiển, việc cuộn sẽ không bao giờ xảy ra.
Lưu ý rằng nó là UIScrollView tiếp nhận tất cả touchesBegin
, touchesMoved
, touchesEnded
và touchesCanceled
các sự kiện từ CocoaTouch (vì nó hitTest
nói nó làm như vậy). Sau đó, nó chuyển tiếp họ đến chế độ xem bên trong nếu nó muốn, miễn là nó muốn.
Bây giờ bạn đã biết mọi thứ về UIScrollView, bạn có thể thay đổi hành vi của nó. Tôi có thể cá là bạn muốn ưu tiên cho cuộn dọc, để khi người dùng chạm vào chế độ xem và bắt đầu di chuyển ngón tay của mình (thậm chí một chút), chế độ xem bắt đầu cuộn theo hướng dọc; nhưng khi người dùng di chuyển ngón tay theo hướng ngang đủ xa, bạn muốn hủy thao tác cuộn dọc và bắt đầu cuộn ngang.
Bạn muốn phân lớp UIScrollView bên ngoài của mình (giả sử bạn đặt tên cho lớp của mình là RemorsefulScrollView), để thay vì hành vi mặc định, nó ngay lập tức chuyển tiếp tất cả các sự kiện đến chế độ xem bên trong và chỉ khi phát hiện chuyển động ngang đáng kể, nó mới cuộn.
Làm thế nào để làm cho RemorsefulScrollView hoạt động theo cách đó?
Có vẻ như việc tắt tính năng cuộn dọc và đặt delaysContentTouches
thành KHÔNG sẽ làm cho các UIScrollView lồng nhau hoạt động. Thật không may, nó không; UIScrollView dường như thực hiện một số lọc bổ sung cho các chuyển động nhanh (không thể tắt được), do đó, ngay cả khi UIScrollView chỉ có thể được cuộn theo chiều ngang, nó sẽ luôn sử dụng (và bỏ qua) các chuyển động dọc đủ nhanh.
Ảnh hưởng nghiêm trọng đến mức không thể sử dụng cuộn dọc bên trong chế độ xem cuộn lồng nhau. (Có vẻ như bạn đã có chính xác thiết lập này, vì vậy hãy thử nó: giữ một ngón tay trong 150ms, sau đó di chuyển ngón tay theo hướng thẳng đứng - UIScrollView lồng nhau hoạt động như mong đợi sau đó!)
Điều này có nghĩa là bạn không thể sử dụng mã của UIScrollView để xử lý sự kiện; bạn phải ghi đè tất cả bốn phương pháp xử lý chạm trong RemorsefulScrollView và tự xử lý trước, chỉ chuyển tiếp sự kiện tới super
(UIScrollView) nếu bạn quyết định sử dụng thao tác cuộn ngang.
Tuy nhiên, bạn phải chuyển touchesBegan
đến UIScrollView, vì bạn muốn nó ghi nhớ một tọa độ cơ sở để cuộn ngang trong tương lai (nếu sau này bạn quyết định đó là cuộn ngang). Bạn sẽ không thể gửi touchesBegan
tới UIScrollView sau, vì bạn không thể lưu trữ touches
đối số: đối số này chứa các đối tượng sẽ bị thay đổi trước touchesMoved
sự kiện tiếp theo và bạn không thể tạo lại trạng thái cũ.
Vì vậy, bạn phải chuyển touchesBegan
đến UIScrollView ngay lập tức, nhưng bạn sẽ ẩn bất kỳ touchesMoved
sự kiện nào khác khỏi nó cho đến khi bạn quyết định cuộn theo chiều ngang. Không touchesMoved
có nghĩa là không cuộn, vì vậy đầu tiên touchesBegan
này sẽ không có hại. Nhưng hãy đặt delaysContentTouches
thành KHÔNG, để không có thêm bộ hẹn giờ bất ngờ nào khác.
(Ngoại tuyến - không giống như bạn, UIScrollView có thể lưu trữ các lần chạm đúng cách và có thể tái tạo và chuyển tiếp touchesBegan
sự kiện gốc sau đó. Nó có một lợi thế không công bằng là sử dụng các API chưa được xuất bản, vì vậy có thể sao chép các đối tượng cảm ứng trước khi chúng bị đột biến.)
Cho rằng bạn luôn hướng về phía trước touchesBegan
, bạn cũng phải chuyển tiếp touchesCancelled
và touchesEnded
. Tuy nhiên, bạn phải chuyển touchesEnded
sang chế độ xem touchesCancelled
vì UIScrollView sẽ diễn giải touchesBegan
, touchesEnded
trình tự dưới dạng một cú nhấp chuột và sẽ chuyển tiếp nó đến chế độ xem bên trong. Bạn đã tự mình chuyển tiếp các sự kiện thích hợp, vì vậy bạn không bao giờ muốn UIScrollView chuyển tiếp bất cứ điều gì.
Về cơ bản đây là mã giả cho những gì bạn cần làm. Để đơn giản, tôi không bao giờ cho phép cuộn ngang sau khi sự kiện cảm ứng đa điểm đã xảy ra.
@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
Tôi chưa thử chạy hoặc thậm chí biên dịch cái này (và nhập cả lớp vào trình soạn thảo văn bản thuần túy), nhưng bạn có thể bắt đầu với phần trên và hy vọng nó hoạt động.
Bí quyết ẩn duy nhất mà tôi thấy là nếu bạn thêm bất kỳ lượt xem con nào không phải UIScrollView vào RemorsefulScrollView, thì các sự kiện chạm mà bạn chuyển tiếp đến một đứa trẻ có thể quay trở lại bạn qua chuỗi phản hồi, nếu đứa trẻ không luôn xử lý các thao tác chạm như UIScrollView. Việc triển khai RemorsefulScrollView chống đạn sẽ bảo vệ khỏi việc nhập touchesXxx
lại.
Chiến lược thứ hai: Nếu vì lý do nào đó các UIScrollView lồng nhau không hoạt động hoặc quá khó để làm đúng, bạn có thể cố gắng kết hợp với chỉ một UIScrollView, chuyển thuộc tính của nó pagingEnabled
ngay lập tức từ scrollViewDidScroll
phương thức ủy quyền của bạn .
Để ngăn việc cuộn theo đường chéo, trước tiên bạn nên thử ghi nhớ contentOffset trong scrollViewWillBeginDragging
và kiểm tra và đặt lại contentOffset bên trong scrollViewDidScroll
nếu bạn phát hiện thấy một chuyển động chéo. Một chiến lược khác để thử là đặt lại contentSize để chỉ cho phép cuộn theo một hướng, sau khi bạn quyết định hướng ngón tay của người dùng đi. (UIScrollView có vẻ khá tha thứ về việc loay hoay với contentSize và contentOffset từ các phương thức ủy quyền của nó.)
Nếu điều đó không hoạt động hoặc dẫn đến hình ảnh cẩu thả, bạn phải ghi đè touchesBegan
, touchesMoved
v.v. và không chuyển tiếp các sự kiện chuyển động theo đường chéo tới UIScrollView. (Tuy nhiên, trải nghiệm người dùng sẽ không tối ưu trong trường hợp này, bởi vì bạn sẽ phải bỏ qua các chuyển động chéo thay vì ép chúng vào một hướng duy nhất. Nếu cảm thấy thực sự mạo hiểm, bạn có thể viết giao diện UITouch của riêng mình, giống như RevengeTouch. Mục tiêu -C hoàn toàn là C cũ, và không có gì đáng chú ý trên thế giới hơn C; miễn là không ai kiểm tra lớp thực của các đối tượng, điều mà tôi tin rằng không ai làm được, bạn có thể làm cho bất kỳ lớp nào giống bất kỳ lớp nào khác. Điều này mở ra khả năng tổng hợp bất kỳ lần chạm nào bạn muốn, với bất kỳ tọa độ nào bạn muốn.)
Chiến lược sao lưu: có TTScrollView, một bản tái thực hiện lành mạnh của UIScrollView trong thư viện Three20 . Thật không may, nó tạo cảm giác rất không tự nhiên và không bắt mắt cho người dùng. Nhưng nếu mọi nỗ lực sử dụng UIScrollView không thành công, bạn có thể quay trở lại chế độ xem cuộn được mã hóa tùy chỉnh. Tôi khuyên bạn nên chống lại nó nếu có thể; bằng cách sử dụng UIScrollView đảm bảo bạn đang có được giao diện nguyên bản, bất kể nó phát triển như thế nào trong các phiên bản hệ điều hành iPhone trong tương lai.
Được rồi, bài luận nhỏ này hơi dài. Tôi vẫn chỉ chơi trò chơi UIScrollView sau khi làm việc trên ScrollingMadness vài ngày trước.
Tái bút Nếu bạn nhận được bất kỳ mã nào trong số này hiệu quả và muốn chia sẻ, vui lòng gửi email cho tôi mã liên quan tại andreyvit@gmail.com, tôi rất vui khi đưa nó vào túi thủ thuật ScrollingMadness của mình .
PPS Thêm bài luận nhỏ này vào ScrollingMadness README.