UIScrollView: phân trang theo chiều ngang, cuộn theo chiều dọc?


88

Làm cách nào để tôi có thể buộc a UIScrollViewtrong đó phân trang và cuộn chỉ di chuyển theo chiều dọc hoặc chiều ngang tại một thời điểm nhất định?

Tôi hiểu rằng thuộc directionalLockEnabledtính sẽ đạt được điều này, nhưng thao tác vuốt chéo vẫn khiến chế độ xem cuộn theo đường chéo thay vì hạn chế chuyển động trong một trục duy nhất.

Chỉnh sửa: để rõ ràng hơn, tôi muốn cho phép người dùng cuộn theo chiều ngang HOẶC theo chiều dọc, nhưng không phải cả hai đồng thời.


Bạn có muốn giới hạn một chế độ xem CHỈ được cuộn Ngang / Vert hay bạn muốn người dùng cuộn Ngang HOẶC Vert, nhưng không cuộn cả hai đồng thời?
Will Hartung

Bạn có thể giúp tôi một việc và thay đổi tiêu đề để nó trông thú vị hơn một chút và không tầm thường được không? Một cái gì đó như “UIScrollView: phân trang theo chiều ngang, cuộn theo chiều dọc?”
Andrey Tarantsov

Rất tiếc, tôi đã đăng câu hỏi với tư cách là người dùng chưa đăng ký trên máy tính mà tôi không còn quyền truy cập (trước khi đăng ký dưới cùng tên khi tôi về nhà), có nghĩa là tôi không thể chỉnh sửa nó. Điều này cũng nên giải thích cho tôi theo sau từ dưới lên thay vì chỉnh sửa câu hỏi ban đầu. Xin lỗi vì đã vi phạm quy tắc Stack lần này!
Nick

Câu trả lời:


117

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=NOcuộ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 đè hitTestphươ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, touchesMovedv.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 delaysContentToucheslà 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 cancelsToucheslà 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, touchesEndedtouchesCanceledcác sự kiện từ CocoaTouch (vì nó hitTestnó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 delaysContentTouchesthà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 đó cuộn ngang). Bạn sẽ không thể gửi touchesBegantớ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 touchesMovedsự 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ỳ touchesMovedsự 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 touchesMovedcó nghĩa là không cuộn, vì vậy đầu tiên touchesBegannày sẽ không có hại. Nhưng hãy đặt delaysContentTouchesthà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 touchesBegansự 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 touchesCancelledtouchesEnded. Tuy nhiên, bạn phải chuyển touchesEndedsang chế độ xem touchesCancelledvì UIScrollView sẽ diễn giải touchesBegan, touchesEndedtrì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.

// 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

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 touchesXxxlạ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ó pagingEnabledngay lập tức từ scrollViewDidScrollphươ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 scrollViewWillBeginDraggingvà kiểm tra và đặt lại contentOffset bên trong scrollViewDidScrollnế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, touchesMovedv.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.


7
Lưu ý rằng hành vi chế độ xem cuộn lồng nhau này hiện hoạt động ngoài hộp trong SDK, không cần phải làm gì vui
andygeers

1
+1 andygeers nhận xét, sau đó nhận ra nó không chính xác; bạn vẫn có thể "phá vỡ" UIScrollView thông thường bằng cách kéo theo đường chéo từ điểm bắt đầu và sau đó bạn đã "kéo mất cuộn cuộn" và nó sẽ bỏ qua khóa định hướng cho đến khi bạn thả ra và có thần biết ở đâu.
Kalle

Cảm ơn vì lời giải thích hậu trường tuyệt vời! Tôi đã sử dụng giải pháp của Mattias Wadman bên dưới có vẻ ít xâm lấn hơn IMO.
Ortwin Gentz

Sẽ thật tuyệt nếu câu trả lời này được cập nhật ngay bây giờ khi UIScrollView hiển thị trình nhận dạng cử chỉ xoay của nó. (Nhưng có lẽ điều đó nằm ngoài phạm vi ban đầu.)
jtbandes

Có khả năng bài viết này được cập nhật không? Bài viết tuyệt vời và cảm ơn bạn đã đóng góp ý kiến.
Pavan

56

Điều này phù hợp với tôi mọi lúc ...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);

20
Đối với bất kỳ ai khác tình cờ gặp câu hỏi này: Tôi nghĩ câu trả lời này đã được đăng trước khi bản chỉnh sửa làm rõ câu hỏi. Điều này sẽ cho phép bạn chỉ cuộn theo chiều ngang, nó không giải quyết được vấn đề có thể cuộn theo chiều dọc hoặc chiều ngang nhưng không được cuộn theo đường chéo.
andygeers

Hoạt động như một cái duyên đối với tôi. Tôi có một Chế độ xem cuộn ngang rất dài với tính năng phân trang và một UIWebView trên mỗi trang, xử lý cuộn dọc mà tôi cần.
Tom Redman,

Thông minh! Nhưng ai đó có thể giải thích cách hoạt động của điều này không (thiết lập chiều cao contentSize thành 1)!
Praveen

Nó thực sự hoạt động, nhưng tại sao và nó hoạt động như thế nào? Ai có thể làm cho một số ý nghĩa của nó?
Marko Francekovic

27

Đối với những người lười biếng như tôi:

Đặt scrollview.directionalLockEnabledthànhYES

- (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}

- (void) scrollViewDidScroll: (UIScrollView *) scrollView
{
    if (scrollView.contentOffset.x != self.oldContentOffset.x)
    {
        scrollView.pagingEnabled = YES;
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x,
                                               self.oldContentOffset.y);
    }
    else
    {
        scrollView.pagingEnabled = NO;
    }
}

- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}

16

Tôi đã sử dụng phương pháp sau để giải quyết vấn đề này, hy vọng nó sẽ giúp bạn.

Trước tiên, hãy xác định các biến sau trong tệp tiêu đề bộ điều khiển của bạn.

CGPoint startPos;
int     scrollDirection;

startPos sẽ giữ giá trị contentOffset khi đại biểu của bạn nhận được thông báo scrollViewWillBeginDragging . Vì vậy, trong phương pháp này chúng tôi làm điều này;

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    startPos = scrollView.contentOffset;
    scrollDirection=0;
}

thì chúng tôi sử dụng các giá trị này để xác định hướng cuộn dự định của người dùng trong tin nhắn scrollViewDidScroll .

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{

   if (scrollDirection==0){//we need to determine direction
       //use the difference between positions to determine the direction.
       if (abs(startPos.x-scrollView.contentOffset.x)<abs(startPos.y-scrollView.contentOffset.y)){          
          NSLog(@"Vertical Scrolling");
          scrollDirection=1;
       } else {
          NSLog(@"Horitonzal Scrolling");
          scrollDirection=2;
       }
    }
//Update scroll position of the scrollview according to detected direction.     
    if (scrollDirection==1) {
       [scrollView setContentOffset:CGPointMake(startPos.x,scrollView.contentOffset.y) animated:NO];
    } else if (scrollDirection==2){
       [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x,startPos.y) animated:NO];
    }
 }

cuối cùng, chúng tôi phải dừng tất cả các hoạt động cập nhật khi người dùng cuối kéo;

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (decelerate) {
       scrollDirection=3;
    }
 }

Hmm ... Trên thực tế, sau khi điều tra thêm - nó hoạt động khá tốt, nhưng rắc rối là nó mất kiểm soát hướng trong giai đoạn giảm tốc - vì vậy nếu bạn bắt đầu di chuyển ngón tay của mình theo đường chéo thì nó sẽ có vẻ chỉ di chuyển theo chiều ngang hoặc chiều dọc cho đến khi bạn buông tay lúc này nó sẽ giảm tốc theo một hướng đường chéo trong một thời gian
andygeers

@andygeers, nó có thể làm việc tốt hơn nếu - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (decelerate) { scrollDirection=3; } }được đổi thành - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (!decelerate) { scrollDirection=0; } }- (void)scrollViewDidEndDecelerating{ scrollDirection=0; }
cduck

Không làm việc cho tôi. Đặt contentOffset trong chuyến bay dường như phá hủy hành vi phân trang. Tôi đã đi với giải pháp của Mattias Wadman.
Ortwin Gentz

13

Tôi có thể đang tìm kiếm ở đây, nhưng tôi tình cờ gặp bài đăng này hôm nay khi tôi đang cố gắng giải quyết vấn đề tương tự. Đây là giải pháp của tôi, có vẻ đang hoạt động tốt.

Tôi muốn hiển thị màn hình nhiều nội dung nhưng cho phép người dùng cuộn đến một trong 4 màn hình khác, lên xuống trái và phải. Tôi có một UIScrollView duy nhất với kích thước 320x480 và contentSize là 960x1440. Phần bù nội dung bắt đầu từ 320,480. Trong scrollViewDidEndDecelerating :, tôi vẽ lại 5 chế độ xem (chế độ xem trung tâm và 4 chế độ xem xung quanh nó) và đặt lại độ lệch nội dung thành 320.480.

Đây là phần quan trọng của giải pháp của tôi, phương thức scrollViewDidScroll:.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{   
    if (!scrollingVertically && ! scrollingHorizontally)
    {
        int xDiff = abs(scrollView.contentOffset.x - 320);
        int yDiff = abs(scrollView.contentOffset.y - 480);

        if (xDiff > yDiff)
        {
            scrollingHorizontally = YES;
        }
        else if (xDiff < yDiff)
        {
            scrollingVertically = YES;
        }
    }

    if (scrollingHorizontally)
    {
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, 480);
    }
    else if (scrollingVertically)
    {
        scrollView.contentOffset = CGPointMake(320, scrollView.contentOffset.y);
    }
}

Bạn có thể sẽ thấy rằng mã này không "giảm tốc" tốt khi người dùng buông chế độ xem cuộn - nó sẽ ngừng chết. Nếu bạn không muốn giảm tốc, điều này có thể tốt cho bạn.
andygeers

4

Các bạn, điều đó thật đơn giản khi làm cho chiều cao của chế độ xem phụ giống với chiều cao của chế độ xem cuộn và chiều cao của kích thước nội dung. Sau đó, tính năng cuộn dọc bị tắt.


2
Đối với bất kỳ ai khác tình cờ gặp câu hỏi này: Tôi nghĩ câu trả lời này đã được đăng trước khi bản chỉnh sửa làm rõ câu hỏi. Điều này sẽ cho phép bạn chỉ cuộn theo chiều ngang, nó không giải quyết được vấn đề có thể cuộn theo chiều dọc hoặc chiều ngang nhưng không được cuộn theo đường chéo.
andygeers

3

Đây là cách triển khai của tôi về một phân trang UIScrollViewchỉ cho phép cuộn trực giao. Hãy chắc chắn đặt pagingEnabledthành YES.

PagedOrthoScrollView.h

@interface PagedOrthoScrollView : UIScrollView
@end

PagedOrthoScrollView.m

#import "PagedOrthoScrollView.h"

typedef enum {
  PagedOrthoScrollViewLockDirectionNone,
  PagedOrthoScrollViewLockDirectionVertical,
  PagedOrthoScrollViewLockDirectionHorizontal
} PagedOrthoScrollViewLockDirection; 

@interface PagedOrthoScrollView ()
@property(nonatomic, assign) PagedOrthoScrollViewLockDirection dirLock;
@property(nonatomic, assign) CGFloat valueLock;
@end

@implementation PagedOrthoScrollView
@synthesize dirLock;
@synthesize valueLock;

- (id)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self == nil) {
    return self;
  }

  self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  self.valueLock = 0;

  return self;
}

- (void)setBounds:(CGRect)bounds {
  int mx, my;

  if (self.dirLock == PagedOrthoScrollViewLockDirectionNone) {
    // is on even page coordinates, set dir lock and lock value to closest page 
    mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
    if (mx != 0) {
      self.dirLock = PagedOrthoScrollViewLockDirectionHorizontal;
      self.valueLock = (round(CGRectGetMinY(bounds) / CGRectGetHeight(self.bounds)) *
                        CGRectGetHeight(self.bounds));
    } else {
      self.dirLock = PagedOrthoScrollViewLockDirectionVertical;
      self.valueLock = (round(CGRectGetMinX(bounds) / CGRectGetWidth(self.bounds)) *
                        CGRectGetWidth(self.bounds));
    }

    // show only possible scroll indicator
    self.showsVerticalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionVertical;
    self.showsHorizontalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionHorizontal;
  }

  if (self.dirLock == PagedOrthoScrollViewLockDirectionHorizontal) {
    bounds.origin.y = self.valueLock;
  } else {
    bounds.origin.x = self.valueLock;
  }

  mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
  my = abs((int)CGRectGetMinY(bounds) % (int)CGRectGetHeight(self.bounds));

  if (mx == 0 && my == 0) {
    // is on even page coordinates, reset lock
    self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  }

  [super setBounds:bounds];
}

@end

1
Hoạt động tuyệt vời, cũng trên iOS 7. Cảm ơn!
Ortwin Gentz

3

Tôi cũng đã phải giải quyết vấn đề này, và trong khi câu trả lời của Andrey Tarantsov chắc chắn có rất nhiều thông tin hữu ích để hiểu cách UIScrollViewshoạt động, tôi cảm thấy rằng giải pháp này hơi phức tạp. Giải pháp của tôi, không liên quan đến bất kỳ phân lớp con hoặc chuyển tiếp cảm ứng nào, như sau:

  1. Tạo hai dạng xem cuộn lồng nhau, một dạng cho mỗi hướng.
  2. Tạo hai hình nộm UIPanGestureRecognizers, một lần nữa cho mỗi hướng.
  3. Tạo phụ thuộc lỗi giữa thuộc tính UIScrollViews'panGestureRecognizer và giả UIPanGestureRecognizertương ứng với hướng vuông góc với hướng cuộn mong muốn của UIScrollView bằng cách sử dụng UIGestureRecognizer's -requireGestureRecognizerToFail:bộ chọn.
  4. Trong lệnh -gestureRecognizerShouldBegin:gọi lại cho UIPanGestureRecognizers giả của bạn, hãy tính toán hướng ban đầu của chảo bằng cách sử dụng bộ chọn -translationInView: của cử chỉ (của bạn UIPanGestureRecognizerssẽ không gọi lệnh gọi lại ủy quyền này cho đến khi cảm ứng của bạn đã dịch đủ để đăng ký làm pan). Cho phép hoặc không cho phép trình nhận dạng cử chỉ của bạn bắt đầu tùy thuộc vào hướng được tính toán, do đó sẽ kiểm soát xem trình nhận dạng cử chỉ xoay UIScrollView vuông góc có được phép bắt đầu hay không.
  5. Trong -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:, không cho phép hình nộm của bạn UIPanGestureRecognizerschạy dọc theo thao tác cuộn (trừ khi bạn có một số hành vi bổ sung mà bạn muốn thêm).

2

Những gì tôi đã làm là tạo một UIScrollView phân trang với khung có kích thước bằng kích thước của màn hình xem và đặt kích thước nội dung thành chiều rộng cần thiết cho tất cả nội dung của tôi và chiều cao là 1 (cảm ơn Tonetel). Sau đó, tôi tạo các trang UIScrollView menu với các khung được đặt thành từng trang, kích thước của màn hình xem và đặt kích thước nội dung của từng trang theo chiều rộng của màn hình và chiều cao cần thiết cho từng trang. Sau đó, tôi chỉ cần thêm từng trang menu vào chế độ xem phân trang. Đảm bảo tính năng phân trang được bật trên chế độ xem cuộn phân trang nhưng bị vô hiệu hóa trên các chế độ xem menu (nên tắt theo mặc định) và bạn có thể thực hiện được.

Chế độ xem cuộn phân trang hiện cuộn theo chiều ngang, nhưng không cuộn theo chiều dọc vì chiều cao nội dung. Mỗi trang cuộn theo chiều dọc nhưng không cuộn theo chiều ngang vì các hạn chế về kích thước nội dung của nó.

Đây là mã nếu lời giải thích đó để lại điều gì đó mong muốn:

UIScrollView *newPagingScrollView =  [[UIScrollView alloc] initWithFrame:self.view.bounds]; 
[newPagingScrollView setPagingEnabled:YES];
[newPagingScrollView setShowsVerticalScrollIndicator:NO];
[newPagingScrollView setShowsHorizontalScrollIndicator:NO];
[newPagingScrollView setDelegate:self];
[newPagingScrollView setContentSize:CGSizeMake(self.view.bounds.size.width * NumberOfDetailPages, 1)];
[self.view addSubview:newPagingScrollView];

float pageX = 0;

for (int i = 0; i < NumberOfDetailPages; i++)
{               
    CGRect pageFrame = (CGRect) 
    {
        .origin = CGPointMake(pageX, pagingScrollView.bounds.origin.y), 
        .size = pagingScrollView.bounds.size
    };
    UIScrollView *newPage = [self createNewPageFromIndex:i ToPageFrame:pageFrame]; // newPage.contentSize custom set in here        
    [pagingScrollView addSubview:newPage];

    pageX += pageFrame.size.width;  
}           

2

Cảm ơn Tonetel. Tôi đã sửa đổi một chút cách tiếp cận của bạn, nhưng đó chính xác là những gì tôi cần để ngăn việc cuộn ngang.

self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 1);

2

Đây là giải pháp cải tiến của icompot, khá không ổn định đối với tôi. Cái này hoạt động tốt và dễ thực hiện:

- (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}

- (void) scrollViewDidScroll: (UIScrollView *) scrollView
{    

    float XOffset = fabsf(self.oldContentOffset.x - scrollView.contentOffset.x );
    float YOffset = fabsf(self.oldContentOffset.y - scrollView.contentOffset.y );

        if (scrollView.contentOffset.x != self.oldContentOffset.x && (XOffset >= YOffset) )
        {

            scrollView.pagingEnabled = YES;
            scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x,
                                                   self.oldContentOffset.y);

        }
        else
        {
            scrollView.pagingEnabled = NO;
               scrollView.contentOffset = CGPointMake( self.oldContentOffset.x,
                                                   scrollView.contentOffset.y);
        }

}


- (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView
{
    self.oldContentOffset = scrollView.contentOffset;
}

2

Để tham khảo trong tương lai, tôi đã xây dựng dựa trên câu trả lời của Andrey Tanasov và đưa ra giải pháp sau đây hoạt động tốt.

Đầu tiên xác định một biến để lưu trữ contentOffset và đặt nó thành 0,0 tọa độ

var positionScroll = CGPointMake(0, 0)

Bây giờ hãy triển khai những điều sau trong đại biểu scrollView:

override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    // Note that we are getting a reference to self.scrollView and not the scrollView referenced passed by the method
    // For some reason, not doing this fails to get the logic to work
    self.positionScroll = (self.scrollView?.contentOffset)!
}

override func scrollViewDidScroll(scrollView: UIScrollView) {

    // Check that contentOffset is not nil
    // We do this because the scrollViewDidScroll method is called on viewDidLoad
    if self.scrollView?.contentOffset != nil {

        // The user wants to scroll on the X axis
        if self.scrollView?.contentOffset.x > self.positionScroll.x || self.scrollView?.contentOffset.x < self.positionScroll.x {

            // Reset the Y position of the scrollView to what it was BEFORE scrolling started
            self.scrollView?.contentOffset = CGPointMake((self.scrollView?.contentOffset.x)!, self.positionScroll.y);
            self.scrollView?.pagingEnabled = true
        }

        // The user wants to scroll on the Y axis
        else {
            // Reset the X position of the scrollView to what it was BEFORE scrolling started
            self.scrollView?.contentOffset = CGPointMake(self.positionScroll.x, (self.scrollView?.contentOffset.y)!);
            self.collectionView?.pagingEnabled = false
        }

    }

}

Hi vọng điêu nay co ich.


1

Từ các tài liệu:

"giá trị mặc định là KHÔNG, có nghĩa là cho phép cuộn theo cả chiều ngang và chiều dọc. Nếu giá trị là CÓ và người dùng bắt đầu kéo theo một hướng chung (ngang hoặc dọc), chế độ xem cuộn sẽ vô hiệu hóa việc cuộn theo hướng khác. "

Tôi nghĩ phần quan trọng là "nếu ... người dùng bắt đầu kéo theo một hướng chung". Vì vậy, nếu họ bắt đầu kéo theo đường chéo, điều này sẽ không thành công. Không phải những tài liệu này luôn đáng tin cậy khi giải thích - nhưng điều đó dường như phù hợp với những gì bạn đang thấy.

Điều này thực sự có vẻ hợp lý. Tôi phải hỏi - tại sao bạn muốn hạn chế chỉ theo chiều ngang hoặc chỉ theo chiều dọc bất cứ lúc nào? Có lẽ UIScrollView không phải là công cụ dành cho bạn?


1

Chế độ xem của tôi bao gồm 10 chế độ xem ngang và một số chế độ xem đó bao gồm nhiều chế độ xem dọc, vì vậy bạn sẽ nhận được một cái gì đó như sau:


1.0   2.0   3.1    4.1
      2.1   3.1
      2.2
      2.3

Với phân trang được bật trên cả trục ngang và trục dọc

Chế độ xem cũng có các thuộc tính sau:

[self setDelaysContentTouches:NO];
[self setMultipleTouchEnabled:NO];
[self setDirectionalLockEnabled:YES];

Bây giờ để ngăn cuộn chéo, hãy làm như sau:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    int pageWidth = 768;
    int pageHeight = 1024;
    int subPage =round(self.contentOffset.y / pageHeight);
    if ((int)self.contentOffset.x % pageWidth != 0 && (int)self.contentOffset.y % pageHeight != 0) {
        [self setContentOffset:CGPointMake(self.contentOffset.x, subPage * pageHeight];
    } 
}

Hoạt động như một sự quyến rũ đối với tôi!


1
không hiểu cách này có thể làm việc cho bạn ... bạn có thể tải lên một dự án mẫu được không?
hfossli 10/09/10

1

Cần đặt lại _isHorizontalScrollthành KHÔNG trong touchesEndedtouchesCancelled.



0

Nếu bạn chưa thử làm điều này trong phiên bản 3.0 beta, tôi khuyên bạn nên thử. Tôi hiện đang sử dụng một loạt chế độ xem bảng bên trong chế độ xem cuộn và nó hoạt động như mong đợi. (Chà, dù sao thì thao tác cuộn cũng có. Tìm thấy câu hỏi này khi tìm cách khắc phục sự cố hoàn toàn khác ...)


0

Đối với những người đang tìm kiếm Swift tại @AndreyTarantsov, giải pháp thứ hai của nó giống như thế này trong mã:

    var positionScroll:CGFloat = 0

    func scrollViewWillBeginDragging(scrollView: UIScrollView) {
        positionScroll = self.theScroll.contentOffset.x
    }

    func scrollViewDidScroll(scrollView: UIScrollView) {
        if self.theScroll.contentOffset.x > self.positionScroll || self.theScroll.contentOffset.x < self.positionScroll{
             self.theScroll.pagingEnabled = true
        }else{
            self.theScroll.pagingEnabled = false
        }
    }

những gì chúng tôi đang làm ở đây là giữ vị trí hiện tại ngay trước khi lượt xem cuộn được kéo và sau đó kiểm tra xem x đã tăng hay giảm so với vị trí hiện tại của x. Nếu có, hãy đặt pagingEnabled thành true, nếu không (y đã tăng / giảm) thì đặt pagingEnabled thành false

Hy vọng điều đó hữu ích cho những người mới đến!


0

Tôi đã giải quyết được điều này bằng cách triển khai scrollViewDidBeginDragging (_ :) và xem xét tốc độ trên UIPanGestureRecognizer bên dưới.

NB: Với giải pháp này, mọi chuyển động theo đường chéo sẽ bị scrollView bỏ qua.

override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    super.scrollViewWillBeginDragging(scrollView)
    let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
    if velocity.x != 0 && velocity.y != 0 {
        scrollView.panGestureRecognizer.isEnabled = false
        scrollView.panGestureRecognizer.isEnabled = true
    }
}

-3

Nếu bạn đang sử dụng trình tạo giao diện để thiết kế giao diện của mình - điều này có thể được thực hiện khá dễ dàng.

Trong trình tạo giao diện, chỉ cần nhấp vào UIScrollView và chọn tùy chọn (trong trình kiểm tra) giới hạn nó chỉ cuộn một chiều.


1
Không có tùy chọn nào trong IB để làm điều này. Hai tùy chọn bạn có thể đề cập đến liên quan đến hiển thị thanh cuộn, không phải cuộn thực. Hơn nữa, Khóa hướng chỉ khóa hướng sau khi quá trình cuộn bắt đầu.
Sophtware
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.