UIScrollXem phân trang ngang giống như các tab trên Mobile Safari


88

Mobile Safari cho phép bạn chuyển đổi các trang bằng cách nhập kiểu xem phân trang ngang UIScrollView với điều khiển trang ở dưới cùng.

Tôi đang cố gắng tái tạo hành vi cụ thể này trong đó UIScrollView có thể cuộn theo chiều ngang hiển thị một số nội dung của chế độ xem tiếp theo.

Ví dụ do Apple cung cấp: PageControl cho biết cách sử dụng UIScrollView cho phân trang ngang, nhưng tất cả các chế độ xem chiếm toàn bộ chiều rộng màn hình.

Làm cách nào để có được UIScrollView để hiển thị một số nội dung của chế độ xem tiếp theo giống như Safari trên thiết bị di động?


2
Chỉ là một suy nghĩ ... hãy thử làm cho giới hạn của chế độ xem cuộn nhỏ hơn màn hình và tìm cách làm cho các chế độ xem hiển thị đúng cách. (và clipsToBounds đặt chế độ xem cuộn để NO)
mjhoy

Tôi muốn có các trang lớn hơn chiều rộng của uiscrollview (cuộn ngang). Và suy nghĩ của mjhoy đã thực sự giúp tôi!
Sasho

Câu trả lời:


263

A UIScrollViewcó bật phân trang sẽ dừng lại ở bội số chiều rộng (hoặc chiều cao) của khung. Vì vậy, bước đầu tiên là xác định độ rộng bạn muốn các trang của mình. Làm cho chiều rộng của UIScrollView. Sau đó, đặt kích thước của lượt xem phụ của bạn lớn đến mức nào bạn cần và đặt tâm của chúng dựa trên bội số UIScrollViewcủa chiều rộng.

Sau đó, vì bạn muốn xem các trang khác, tất nhiên, hãy đặt clipsToBoundsthành NOnhư mhjoy đã nêu. Phần thủ thuật bây giờ là làm cho nó cuộn khi người dùng bắt đầu kéo bên ngoài phạm vi của UIScrollViewkhung hình. Giải pháp của tôi (khi tôi phải làm điều này rất gần đây) là như sau:

Tạo một UIViewlớp con (tức là ClipView) sẽ chứa các lượt xem con UIScrollViewvà của nó. Về cơ bản, nó phải có khung của những gì bạn cho là UIScrollViewsẽ có trong các trường hợp bình thường. Đặt UIScrollViewở giữa ClipView. Đảm bảo rằng ClipView's clipsToBoundsđược đặt thành YESnếu chiều rộng của nó nhỏ hơn chiều rộng của chế độ xem gốc. Ngoài ra, ClipViewnhu cầu tham chiếu đếnUIScrollView .

Bước cuối cùng là ghi đè - (UIView *)hitTest:withEvent:bên trong ClipView.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  return [self pointInside:point withEvent:event] ? scrollView : nil;
}

Về cơ bản, điều này mở rộng vùng cảm ứng của UIScrollView khung hình của chế độ xem cha mẹ của nó, chính xác những gì bạn cần.

Một tùy chọn khác sẽ là phân lớp UIScrollViewvà ghi đè - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventphương thức của nó , tuy nhiên bạn vẫn sẽ cần một khung nhìn vùng chứa để thực hiện việc cắt bớt và có thể khó xác định khi nào quay lại YESchỉ dựa trên UIScrollViewkhung của.

LƯU Ý: Bạn cũng nên xem hitTest: withEvent: sửa đổi của Juri Pakaste nếu bạn đang gặp vấn đề với tương tác người dùng subview.


3
Cảm ơn Ed. Tôi đã sử dụng một UIScrollViewlớp con để tải chậm lượt xem (trong layoutSubviews) và sử dụng một clippingRectđể thực hiện pointInside:withEvent:kiểm tra. Hoạt động thực sự tốt và không cần chế độ xem vùng chứa bổ sung.
ohhorob

1
Câu trả lời chính xác. Tôi sử dụng một UIScrollViewlớp con như @ohhorob nhưng với một cái nhìn bình chứa để cắt và trở lại [super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];trong pointInside:withEvent:. Điều này hoạt động rất tốt và không có vấn đề gì với tương tác của người dùng được đề cập trong câu trả lời của @ JuriPakaste.
cduck

1
Wow, đây là một giải pháp thực sự tốt. Tuy nhiên, chỉ một điều, trên thiết lập của tôi, các trang mà tôi đang thêm có UIButtons. Khi tôi ghi đè phương thức hitTest, nó không cho phép tôi nhấn vào các nút đó. Tôi có thể cuộn, nhưng không thể chọn hành động nào bên trong bất kỳ trang nào.
A Salcedo

8
Đối với những người gặp phải vấn đề này gần đây, hãy lưu ý rằng - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetđược thêm vào UIScrollViewDelegate trong iOS5 có nghĩa là bạn không phải trải qua palaver này nữa.
Mike Pollard

1
@MikePollard hiệu ứng không yên tĩnh như nhau, targetContentOffset không phù hợp với tình huống này.
Kyle Fang

70

Các ClipViewgiải pháp làm việc trên đối với tôi, nhưng tôi đã phải làm một khác nhau -[UIView hitTest:withEvent:]thực hiện. Phiên bản của Ed Marty không nhận được sự tương tác của người dùng với các chế độ xem cuộn dọc mà tôi có bên trong chế độ xem ngang.

Phiên bản sau phù hợp với tôi:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}

Điều này cũng giúp các nút và lượt xem phụ khác có tương tác với người dùng hoạt động. :) cảm ơn
Thomas Clayson

4
Cảm ơn bạn về mã này, làm cách nào tôi có thể sử dụng sửa đổi này để nhận các sự kiện từ các lần xem phụ (nút) không có trong ranh giới chế độ xem cuộn? Nó hoạt động nếu một nút chẳng hạn nằm trong ranh giới của chế độ xem cuộn trung tâm nhưng không ở bên ngoài.
DreamOfMirrors

4
Điều này thực sự hữu ích. Nó nên được bao gồm như một sửa đổi của câu trả lời đã chọn.
Pacu

2
Đúng! Điều này cũng làm cho tương tác của người dùng trong chế độ xem cuộn hoạt động trở lại! Tôi đã phải đặt userInteractionEnabled thành CÓ trên ClipView để điều này hoạt động.
Tom van Zummeren

Tôi phải lặp lại self.scrollView.subviews và thực hiện hitTest trước.
Bao Lei

8

Đặt kích thước khung của scrollView như kích thước các trang của bạn sẽ là:

[self.view addSubview:scrollView];
[self.view addGestureRecognizer:mainScrollView.panGestureRecognizer];

Bây giờ bạn có thể xoay self.viewvà nội dung trên scrollView sẽ được cuộn.
Cũng sử dụng scrollView.clipsToBounds = NO;để ngăn chặn việc cắt bớt nội dung.


5

Tôi bắt đầu tự sử dụng UIScrollView tùy chỉnh vì nó là phương pháp nhanh nhất và đơn giản nhất đối với tôi. Tuy nhiên, tôi không thấy bất kỳ mã chính xác nào nên tôi sẽ chia sẻ. Nhu cầu của tôi là về một UIScrollView có nội dung nhỏ và do đó bản thân UIScrollView cũng nhỏ để đạt được hiệu quả phân trang. Như bài đăng nói rằng bạn không thể vuốt qua. Nhưng bây giờ bạn có thể.

Tạo một lớp CustomScrollView và lớp con UIScrollView. Sau đó, tất cả những gì bạn cần làm là thêm tệp này vào tệp .m:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  return (point.y >= 0 && point.y <= self.frame.size.height);
}

Điều này cho phép bạn cuộn từ bên này sang bên kia (ngang). Điều chỉnh giới hạn cho phù hợp để đặt vùng cảm ứng vuốt / cuộn của bạn. Thưởng thức!


4

Tôi đã thực hiện một triển khai khác có thể trả lại chế độ xem cuộn tự động. Vì vậy, nó không cần phải có IBOutlet sẽ hạn chế việc sử dụng lại trong dự án.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self pointInside:point withEvent:event]) {
        for (id view in [self subviews]) {
            if ([view isKindOfClass:[UIScrollView class]]) {
                return view;
            }
        }
    }
    return nil;
}

1
Đây là cái để sử dụng nếu bạn đã có xib / storyboard. Nếu không, tôi không chắc bạn sẽ nhận được tham chiếu đến Paging / Scrollview như thế nào. Bất kỳ ý tưởng?
mskw

3

Tôi có một sửa đổi hữu ích tiềm năng khác cho việc triển khai ClipView hitTest. Tôi không muốn phải cung cấp tham chiếu UIScrollView cho ClipView. Việc triển khai của tôi bên dưới cho phép bạn sử dụng lại lớp ClipView để mở rộng vùng thử nghiệm của bất kỳ thứ gì và không phải cung cấp cho nó một tham chiếu.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (([self pointInside:point withEvent:event]) && (self.subviews.count >= 1))
    {
        // An extended-hit view should only have one sub-view, or make sure the
        // first subview is the one you want to expand the hit test for.
        return [self.subviews objectAtIndex:0];
    }

    return nil;
}

3

Tôi đã triển khai đề xuất được ủng hộ ở trên, nhưng đề xuất UICollectionViewtôi đang sử dụng coi bất kỳ thứ gì ngoài khung hình đều không xuất hiện trên màn hình. Điều này khiến các ô lân cận chỉ hiển thị ngoài giới hạn khi người dùng cuộn về phía chúng, điều này không lý tưởng.

Cuối cùng, những gì tôi đã làm là mô phỏng hành vi của một lần xem cuộn bằng cách thêm phương thức bên dưới vào (hoặc UICollectionViewLayout).

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{    
  if (velocity.x > 0) {
    proposedContentOffset.x = ceilf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }
  else {
    proposedContentOffset.x = floorf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }

  return proposedContentOffset;
}

Điều này tránh hoàn toàn việc ủy ​​quyền hành động vuốt, đây cũng là một phần thưởng. Có UIScrollViewDelegatemột phương thức tương tự được gọi là phương thức scrollViewWillEndDragging:withVelocity:targetContentOffset:có thể được sử dụng để trang UITableViews và UIScrollViews.


2
Dave, tôi cũng đang đấu tranh để đạt được hành vi như vậy bằng cách sử dụng UICollectionView và cuối cùng tôi đã sử dụng cùng một phương pháp mà bạn mô tả ở đây. Tuy nhiên, trải nghiệm cuộn không tốt bằng chế độ xem cuộn có bật phân trang. Khi tôi vuốt với tốc độ lớn hơn, một số trang đã được cuộn và nó dừng lại trên trang được đề xuất. Những gì tôi muốn đạt được là lưu hành vi dưới dạng chế độ xem cuộn bình thường với tính năng phân trang được bật - bất kỳ tốc độ nào tôi sử dụng, tôi sẽ chỉ nhận được thêm 1 trang. Bạn có bất kỳ ý tưởng làm thế nào để làm điều đó?
Peter Stajger

3

Cho phép kích hoạt các sự kiện chạm trên các chế độ xem con của chế độ xem cuộn trong khi hỗ trợ kỹ thuật của câu hỏi SO này. Sử dụng tham chiếu đến dạng xem cuộn (self.scrollView) để dễ đọc.

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = nil;
    NSArray *childViews = [self.scrollView subviews];
    for (NSUInteger i = 0; i < childViews.count; i++) {
        CGRect childFrame = [[childViews objectAtIndex:i] frame];
        CGRect scrollFrame = self.scrollView.frame;
        CGPoint contentOffset = self.scrollView.contentOffset;
        if (childFrame.origin.x + scrollFrame.origin.x < point.x + contentOffset.x &&
            point.x + contentOffset.x < childFrame.origin.x + scrollFrame.origin.x + childFrame.size.width &&
            childFrame.origin.y + scrollFrame.origin.y < point.y + contentOffset.y &&
            point.y + contentOffset.y < childFrame.origin.y + scrollFrame.origin.y + childFrame.size.height
        ){
            hitView = [childViews objectAtIndex:i];
            return hitView;
        }
    }
    hitView = [super hitTest:point withEvent:event];
    if (hitView == self)
        return self.scrollView;
    return hitView;
}

Thêm cái này vào chế độ xem của con bạn để nắm bắt sự kiện chạm:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

(Đây là một biến thể trên giải pháp của user1856273. Đã được hoàn thiện để dễ đọc và kết hợp sửa lỗi của Bartserk. Tôi đã nghĩ đến việc chỉnh sửa câu trả lời của user1856273 nhưng đó là một thay đổi quá lớn để thực hiện.)


Để khai thác hoạt động trên UIButton được nhúng trong chế độ xem phụ, tôi đã thay thế mã hitView = [childViews objectAtIndex: i]; with // tìm UIButton for (UIView * paneView trong [[childViews objectAtIndex: i] subviews]) {if ([paneView isKindOfClass: [UIButton class]]) return paneView; }
CharlesA

2

phiên bản của tôi Nhấn nút nằm trên cuộn - làm việc =)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView* child = nil;
    for (int i=0; i<[[[[self subviews] objectAtIndex:0] subviews] count];i++) {

        CGRect myframe =[[[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i] frame];
        CGRect myframeS =[[[self subviews] objectAtIndex:0] frame];
        CGPoint myscroll =[[[self subviews] objectAtIndex:0] contentOffset];
        if (myframe.origin.x < point.x && point.x < myframe.origin.x+myframe.size.width &&
            myframe.origin.y+myframeS.origin.y < point.y+myscroll.y && point.y+myscroll.y < myframe.origin.y+myframeS.origin.y +myframe.size.height){
            child = [[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i];
            return child;
        }


    }

    child = [super hitTest:point withEvent:event];
    if (child == self)
        return [[self subviews] objectAtIndex:0];
    return child;
    }

nhưng chỉ [[self subviews] objectAtIndex: 0] phải là một cuộn


Điều này đã giải quyết được vấn đề của tôi :) Chỉ cần lưu ý rằng bạn không thêm độ lệch cuộn vào point.x khi bạn đang kiểm tra các giới hạn và điều này làm cho mã không hoạt động tốt trên các cuộn ngang. Sau khi thêm điều đó, nó hoạt động tốt!
Bartserk

2

Đây là câu trả lời Swift dựa trên câu trả lời của Ed Marty nhưng cũng bao gồm sửa đổi của Juri Pakaste để cho phép nhấn nút, v.v. bên trong chế độ xem cuộn.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}

Cảm ơn vì đã cập nhật thông tin này :)
Liam Bolling
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.