Paging UIScrollView theo gia số nhỏ hơn kích thước khung hình


87

Tôi có chế độ xem cuộn là chiều rộng của màn hình nhưng chỉ cao khoảng 70 pixel. Nó chứa nhiều biểu tượng 50 x 50 (với khoảng trống xung quanh chúng) mà tôi muốn người dùng có thể lựa chọn. Nhưng tôi luôn muốn chế độ xem cuộn hoạt động theo cách phân trang, luôn dừng lại với một biểu tượng ở chính giữa.

Nếu các biểu tượng có chiều rộng của màn hình thì điều này sẽ không thành vấn đề vì phân trang của UIScrollView sẽ xử lý nó. Nhưng vì các biểu tượng nhỏ của tôi nhỏ hơn nhiều so với kích thước nội dung, nó không hoạt động.

Tôi đã thấy hành vi này trước đây trong một cuộc gọi ứng dụng AllRecipes. Tôi chỉ không biết làm thế nào để làm điều đó.

Làm cách nào để phân trang trên cơ sở kích thước từng biểu tượng hoạt động?

Câu trả lời:


123

Hãy thử làm cho lượt xem cuộn của bạn nhỏ hơn kích thước của màn hình (theo chiều rộng), nhưng bỏ chọn hộp kiểm "Lượt xem phụ của Clip" trong IB. Sau đó, phủ một chế độ xem userInteractionEnabled = NO trong suốt lên trên nó (ở toàn bộ chiều rộng), sẽ ghi đè hitTest: withEvent: để trả lại chế độ xem cuộn của bạn. Điều đó sẽ cung cấp cho bạn những gì bạn đang tìm kiếm. Xem câu trả lời này để biết thêm chi tiết.


1
Tôi không chắc bạn muốn nói gì, tôi sẽ xem xét câu trả lời mà bạn chỉ ra.
Henning

8
Đây là một giải pháp đẹp. Tôi đã không nghĩ đến việc ghi đè hàm hitTest: và điều đó cắt bỏ khá nhiều nhược điểm duy nhất của cách tiếp cận này. Tốt lắm.
Ben Gotow

Giải pháp tốt! Thực sự đã giúp tôi ra ngoài.
DevDevDev

8
Điều này thực sự hoạt động tốt, nhưng tôi khuyên bạn nên thay vì chỉ trả lại scrollView, hãy trả về [scrollView hitTest: point withEvent: event] để các sự kiện đi qua đúng cách. Ví dụ: trong chế độ xem cuộn đầy UIButtons, các nút sẽ vẫn hoạt động theo cách này.
greenisus

2
lưu ý, điều này KHÔNG hoạt động với tableview hoặc collectionView vì nó sẽ không hiển thị những thứ bên ngoài khung. Xem câu trả lời của tôi bên dưới
vish

63

Cũng có một giải pháp khác có lẽ tốt hơn một chút là phủ chế độ xem cuộn bằng một chế độ xem khác và ghi đè hitTest.

Bạn có thể phân lớp UIScrollView và ghi đè pointInside của nó. Sau đó, chế độ xem cuộn có thể phản hồi các lần chạm bên ngoài khung của nó. Tất nhiên những thứ còn lại đều giống nhau.

@interface PagingScrollView : UIScrollView {

    UIEdgeInsets responseInsets;
}

@property (nonatomic, assign) UIEdgeInsets responseInsets;

@end


@implementation PagingScrollView

@synthesize responseInsets;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint parentLocation = [self convertPoint:point toView:[self superview]];
    CGRect responseRect = self.frame;
    responseRect.origin.x -= responseInsets.left;
    responseRect.origin.y -= responseInsets.top;
    responseRect.size.width += (responseInsets.left + responseInsets.right);
    responseRect.size.height += (responseInsets.top + responseInsets.bottom);

    return CGRectContainsPoint(responseRect, parentLocation);
}

@end

1
Đúng! hơn nữa, nếu giới hạn của scrollview của bạn giống với cha của nó thì bạn chỉ có thể trả về có từ phương thức pointInside. cảm ơn
cocoatoucher 13/10/10

5
Tôi thích giải pháp này, mặc dù tôi phải thay đổi parentLocation thành "[self convertPoint: point toView: self.superview]" để nó hoạt động bình thường.
khoảng

Bạn đúng convertPoint tốt hơn nhiều so với những gì tôi đã viết ở đó.
Chia

6
trực tiếp return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];@amrox @Split bạn không cần phản hồi Đặt lại nếu bạn có chế độ xem siêu cấp hoạt động như chế độ xem cắt.
Cœur

Giải pháp này dường như bị phá vỡ khi tôi đi đến cuối danh sách các mục của mình nếu trang cuối cùng của các mục không hoàn toàn lấp đầy trang. Thật khó để giải thích, nhưng nếu tôi đang hiển thị 3,5 ô trên mỗi trang và tôi có 5 mục thì trang thứ hai sẽ hiển thị cho tôi 2 ô với không gian trống ở bên phải hoặc nó sẽ bắt đầu hiển thị 3 ô với phần đầu một phần ô. Vấn đề là nếu tôi ở trạng thái mà nó đang hiển thị cho tôi không gian trống và tôi đã chọn một ô thì phân trang sẽ tự sửa trong quá trình chuyển đổi điều hướng ... Tôi sẽ đăng giải pháp của mình bên dưới.
tyler

18

Tôi thấy rất nhiều giải pháp, nhưng chúng rất phức tạp. Một cách dễ dàng hơn nhiều để có các trang nhỏ nhưng vẫn giữ được tất cả các vùng có thể cuộn được, là làm cho cuộn nhỏ hơn và di chuyển scrollView.panGestureRecognizerđến chế độ xem chính của bạn. Đây là các bước:

  1. Giảm kích thước Chế độ xem cuộn của bạnKích thước ScrollView nhỏ hơn kích thước chính

  2. Đảm bảo chế độ xem cuộn của bạn được phân trang và không cắt chế độ xem phụ nhập mô tả hình ảnh ở đây

  3. Trong mã, hãy di chuyển cử chỉ xoay chế độ xem cuộn sang chế độ xem vùng chứa chính có chiều rộng đầy đủ:

    override func viewDidLoad() {
        super.viewDidLoad()
        statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer)
    }

Đây là một giải pháp greeeeeeat. Rất thông minh.
superpuccio

Đây thực sự là những gì tôi muốn!
LYM

Rất thông minh! Bạn đã tiết kiệm cho tôi hàng giờ. Cảm ơn bạn!
Erik

6

Câu trả lời được chấp nhận là rất tốt, nhưng nó sẽ chỉ có hiệu quả với UIScrollViewlớp học, và không có con cháu nào của nó. Ví dụ: nếu bạn có nhiều chế độ xem và chuyển đổi thành a UICollectionView, bạn sẽ không thể sử dụng phương pháp này, bởi vì chế độ xem bộ sưu tập sẽ xóa các chế độ xem mà nó cho là "không hiển thị" (vì vậy mặc dù chúng không được cắt bớt, chúng sẽ biến mất).

Nhận xét về đề cập đó scrollViewWillEndDragging:withVelocity:targetContentOffset:, theo quan điểm của tôi, là câu trả lời chính xác.

Những gì bạn có thể làm là, bên trong phương pháp ủy quyền này, bạn tính toán trang / chỉ mục hiện tại. Sau đó, bạn quyết định xem vận tốc và mục tiêu có bù đắp xứng đáng cho chuyển động "trang tiếp theo" hay không. Bạn có thể đến khá gần với pagingEnabledhành vi.

lưu ý: Tôi thường là một nhà phát triển RubyMotion ngày nay, vì vậy ai đó vui lòng chứng minh mã Obj-C này cho đúng. Xin lỗi vì sự kết hợp giữa camelCase và solid_case, tôi đã sao chép và dán nhiều đoạn mã này.

- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView
         withVelocity:(CGPoint)velocity
         targetContentOffset:(inout CGPoint *)targetOffset
{
    CGFloat x = targetOffset->x;
    int index = [self convertXToIndex: x];
    CGFloat w = 300f;  // this is your custom page width
    CGFloat current_x = w * [self convertXToIndex: scrollView.contentOffset.x];

    // even if the velocity is low, if the offset is more than 50% past the halfway
    // point, proceed to the next item.
    if ( velocity.x < -0.5 || (current_x - x) > w / 2 ) {
      index -= 1
    } 
    else if ( velocity.x > 0.5 || (x - current_x) > w / 2 ) {
      index += 1;
    }

    if ( index >= 0 || index < self.items.length ) {
      CGFloat new_x = [self convertIndexToX: index];
      targetOffset->x = new_x;
    }
} 

haha bây giờ tôi tự hỏi nếu tôi không nên đề cập đến RubyMotion - nó khiến tôi bị đánh giá thấp! không thực sự tôi muốn những phiếu bầu giảm giá cũng bao gồm một số bình luận, tôi rất muốn biết những gì mọi người thấy sai với lời giải thích của tôi.
colinta

Bạn có thể bao gồm định nghĩa cho convertXToIndex:convertIndexToX:?
mr5

Không hoàn thành. Khi bạn kéo từ từ và nhấc tay lên với velocitysố 0, nó sẽ bật trở lại, điều này thật kỳ lạ. Vì vậy, tôi có một câu trả lời tốt hơn.
DawnSong

4
- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView
         withVelocity:(CGPoint)velocity
         targetContentOffset:(inout CGPoint *)targetOffset
{
    static CGFloat previousIndex;
    CGFloat x = targetOffset->x + kPageOffset;
    int index = (x + kPageWidth/2)/kPageWidth;
    if(index<previousIndex - 1){
        index = previousIndex - 1;
    }else if(index > previousIndex + 1){
        index = previousIndex + 1;
    }

    CGFloat newTarget = index * kPageWidth;
    targetOffset->x = newTarget - kPageOffset;
    previousIndex = index;
} 

kPageWidth là chiều rộng bạn muốn cho trang của mình. kPageOffset là nếu bạn không muốn các ô được căn trái (tức là nếu bạn muốn chúng được căn giữa, hãy đặt giá trị này bằng một nửa chiều rộng ô của bạn). Nếu không, nó phải bằng không.

Điều này cũng sẽ chỉ cho phép cuộn từng trang một.


3

Hãy xem -scrollView:didEndDragging:willDecelerate:phương pháp trên UIScrollViewDelegate. Cái gì đó như:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    int x = scrollView.contentOffset.x;
    int xOff = x % 50;
    if(xOff < 25)
        x -= xOff;
    else
        x += 50 - xOff;

    int halfW = scrollView.contentSize.width / 2; // the width of the whole content view, not just the scroll view
    if(x > halfW)
        x = halfW;

    [scrollView setContentOffset:CGPointMake(x,scrollView.contentOffset.y)];
}

Nó không hoàn hảo — lần cuối cùng tôi thử mã này, tôi đã có một số hành vi xấu (nhảy, như tôi nhớ lại) khi quay lại từ một cuộn dây cao su. Bạn có thể tránh điều đó bằng cách chỉ cần đặt thuộc bouncestính của chế độ xem cuộn thành NO.


3

Vì tôi dường như không được phép bình luận nên tôi sẽ thêm ý kiến ​​của mình vào câu trả lời của Noah ở đây.

Tôi đã đạt được điều này thành công bằng phương pháp mà Noah Witherspoon đã mô tả. Tôi đã làm việc xung quanh hành vi nhảy bằng cách đơn giản là không gọi setContentOffset:phương thức khi chế độ xem cuộn qua các cạnh của nó.

         - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
         {      
             // Don't snap when at the edges because that will override the bounce mechanic
             if (self.contentOffset.x < 0 || self.contentOffset.x + self.bounds.size.width > self.contentSize.width)
                 return;

             ...
         }

Tôi cũng thấy rằng tôi cần triển khai -scrollViewWillBeginDecelerating:phương pháp UIScrollViewDelegateđể bắt tất cả các trường hợp.


Cần bắt thêm những trường hợp nào?
chrish

trường hợp mà không có kéo ở tất cả. Người dùng nhấc ngón tay lên và việc cuộn sẽ dừng ngay lập tức.
Jess Bowers

3

Tôi đã thử giải pháp ở trên phủ một chế độ xem trong suốt với pointInside: withEvent: overridden. Điều này hoạt động khá tốt đối với tôi, nhưng bị hỏng đối với một số trường hợp - hãy xem nhận xét của tôi. Cuối cùng, tôi chỉ tự mình triển khai phân trang với sự kết hợp của scrollViewDidScroll để theo dõi chỉ mục trang hiện tại và scrollViewWillEndDragging: withVelocity: targetContentOffset và scrollViewDidEndDragging: willDecelerate để bám vào trang thích hợp. Lưu ý, phương pháp will-end chỉ có sẵn trên iOS5 +, nhưng khá hiệu quả khi nhắm mục tiêu một khoảng chênh lệch cụ thể nếu vận tốc! = 0. Cụ thể, bạn có thể cho người gọi biết nơi bạn muốn chế độ xem cuộn tiếp đất với hoạt ảnh nếu có vận tốc trong hướng cụ thể.


0

Khi tạo chế độ xem cuộn, hãy đảm bảo bạn đặt điều này:

scrollView.showsHorizontalScrollIndicator = false;
scrollView.showsVerticalScrollIndicator = false;
scrollView.pagingEnabled = true;

Sau đó, thêm các lượt xem phụ của bạn vào cuộn ở độ lệch bằng chỉ số * chiều cao của cuộn. Đây là cho một cuộn dọc:

UIView * sub = [UIView new];
sub.frame = CGRectMake(0, index * h, w, subViewHeight);
[scrollView addSubview:sub];

Nếu bạn chạy nó ngay bây giờ, các dạng xem sẽ được giãn ra và khi bật phân trang, chúng sẽ cuộn từng cái một.

Vì vậy, sau đó đặt nó trong phương thức viewDidScroll của bạn:

    //set vars
    int index = scrollView.contentOffset.y / h; //current index
    float y = scrollView.contentOffset.y; //actual offset
    float p = (y / h)-index; //percentage of page scroll complete (0.0-1.0)
    int subViewHeight = h-240; //height of the view
    int spacing = 30; //preferred spacing between views (if any)

    NSArray * array = scrollView.subviews;

    //cycle through array
    for (UIView * sub in array){

        //subview index in array
        int subIndex = (int)[array indexOfObject:sub];

        //moves the subs up to the top (on top of each other)
        float transform = (-h * subIndex);

        //moves them back down with spacing
        transform += (subViewHeight + spacing) * subIndex;

        //adjusts the offset during scroll
        transform += (h - subViewHeight - spacing) * p;

        //adjusts the offset for the index
        transform += index * (h - subViewHeight - spacing);

        //apply transform
        sub.transform = CGAffineTransformMakeTranslation(0, transform);
    }

Các khung của các lần xem phụ vẫn được đặt cách nhau, chúng tôi chỉ di chuyển chúng lại với nhau thông qua một biến đổi khi người dùng cuộn.

Ngoài ra, bạn có quyền truy cập vào biến p ở trên, bạn có thể sử dụng biến này cho những thứ khác, như alpha hoặc biến đổi trong các lần xem phụ. Khi p == 1, trang đó đang được hiển thị đầy đủ, hay đúng hơn là nó có xu hướng về 1.


0

Hãy thử sử dụng thuộc tính contentInset của scrollView:

scrollView.pagingEnabled = YES;

[scrollView setContentSize:CGSizeMake(height, pageWidth * 3)];
double leftContentOffset = pageWidth - kSomeOffset;
scrollView.contentInset = UIEdgeInsetsMake(0, leftContentOffset, 0, 0);

Có thể mất một số lần chơi với các giá trị của bạn để đạt được phân trang mong muốn.

Tôi đã thấy điều này hoạt động rõ ràng hơn so với các lựa chọn thay thế đã đăng. Vấn đề với việc sử dụng scrollViewWillEndDragging:phương pháp đại biểu là việc tăng tốc cho các nhấp nháy chậm là không tự nhiên.


0

Đây là giải pháp thực sự duy nhất cho vấn đề.

import UIKit

class TestScrollViewController: UIViewController, UIScrollViewDelegate {

    var scrollView: UIScrollView!

    var cellSize:CGFloat!
    var inset:CGFloat!
    var preX:CGFloat=0
    let pages = 8

    override func viewDidLoad() {
        super.viewDidLoad()

        cellSize = (self.view.bounds.width-180)
        inset=(self.view.bounds.width-cellSize)/2
        scrollView=UIScrollView(frame: self.view.bounds)
        self.view.addSubview(scrollView)

        for i in 0..<pages {
            let v = UIView(frame: self.view.bounds)
            v.backgroundColor=UIColor(red: CGFloat(CGFloat(i)/CGFloat(pages)), green: CGFloat(1 - CGFloat(i)/CGFloat(pages)), blue: CGFloat(CGFloat(i)/CGFloat(pages)), alpha: 1)
            v.frame.origin.x=CGFloat(i)*cellSize
            v.frame.size.width=cellSize
            scrollView.addSubview(v)
        }

        scrollView.contentSize.width=cellSize*CGFloat(pages)
        scrollView.isPagingEnabled=false
        scrollView.delegate=self
        scrollView.contentInset.left=inset
        scrollView.contentOffset.x = -inset
        scrollView.contentInset.right=inset

    }

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        preX = scrollView.contentOffset.x
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

        let originalIndex = Int((preX+cellSize/2)/cellSize)

        let targetX = targetContentOffset.pointee.x
        var targetIndex = Int((targetX+cellSize/2)/cellSize)

        if targetIndex > originalIndex + 1 {
            targetIndex=originalIndex+1
        }
        if targetIndex < originalIndex - 1 {
            targetIndex=originalIndex - 1
        }

        if velocity.x == 0 {
            let currentIndex = Int((scrollView.contentOffset.x+self.view.bounds.width/2)/cellSize)
            let tx=CGFloat(currentIndex)*cellSize-(self.view.bounds.width-cellSize)/2
            scrollView.setContentOffset(CGPoint(x:tx,y:0), animated: true)
            return
        }

        let tx=CGFloat(targetIndex)*cellSize-(self.view.bounds.width-cellSize)/2
        targetContentOffset.pointee.x=scrollView.contentOffset.x

        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: [UIViewAnimationOptions.curveEaseOut, UIViewAnimationOptions.allowUserInteraction], animations: {
            scrollView.contentOffset=CGPoint(x:tx,y:0)
        }) { (b:Bool) in

        }

    }


}

0

Đây là câu trả lời của tôi. Trong ví dụ của tôi, một collectionViewcó tiêu đề phần là scrollView mà chúng tôi muốn làm cho nó có isPagingEnabledhiệu ứng tùy chỉnh và chiều cao của ô là một giá trị không đổi.

var isScrollingDown = false // in my example, scrollDirection is vertical
var lastScrollOffset = CGPoint.zero

func scrollViewDidScroll(_ sv: UIScrollView) {
    isScrollingDown = sv.contentOffset.y > lastScrollOffset.y
    lastScrollOffset = sv.contentOffset
}

// 实现 isPagingEnabled 效果
func scrollViewWillEndDragging(_ sv: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    let realHeaderHeight = headerHeight + collectionViewLayout.sectionInset.top
    guard realHeaderHeight < targetContentOffset.pointee.y else {
        // make sure that user can scroll to make header visible.
        return // 否则无法手动滚到顶部
    }

    let realFooterHeight: CGFloat = 0
    let realCellHeight = cellHeight + collectionViewLayout.minimumLineSpacing
    guard targetContentOffset.pointee.y < sv.contentSize.height - realFooterHeight else {
        // make sure that user can scroll to make footer visible
        return // 若有footer,不在此处 return 会导致无法手动滚动到底部
    }

    let indexOfCell = (targetContentOffset.pointee.y - realHeaderHeight) / realCellHeight
    // velocity.y can be 0 when lifting your hand slowly
    let roundedIndex = isScrollingDown ? ceil(indexOfCell) : floor(indexOfCell) // 如果松手时滚动速度为 0,则 velocity.y == 0,且 sv.contentOffset == targetContentOffset.pointee
    let y = realHeaderHeight + realCellHeight * roundedIndex - collectionViewLayout.minimumLineSpacing
    targetContentOffset.pointee.y = y
}

0

Phiên bản Swift tinh chỉnh của UICollectionViewgiải pháp:

  • Giới hạn một trang cho mỗi lần vuốt
  • Đảm bảo truy cập nhanh vào trang, ngay cả khi cuộn của bạn chậm
override func viewDidLoad() {
    super.viewDidLoad()
    collectionView.decelerationRate = .fast
}

private var dragStartPage: CGPoint = .zero

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    dragStartOffset = scrollView.contentOffset
}

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    // Snap target offset to current or adjacent page
    let currentIndex = pageIndexForContentOffset(dragStartOffset)
    var targetIndex = pageIndexForContentOffset(targetContentOffset.pointee)
    if targetIndex != currentIndex {
        targetIndex = currentIndex + (targetIndex - currentIndex).signum()
    } else if abs(velocity.x) > 0.25 {
        targetIndex = currentIndex + (velocity.x > 0 ? 1 : 0)
    }
    // Constrain to valid indices
    if targetIndex < 0 { targetIndex = 0 }
    if targetIndex >= items.count { targetIndex = max(items.count-1, 0) }
    // Set new target offset
    targetContentOffset.pointee.x = contentOffsetForCardIndex(targetIndex)
}

0

Chủ đề cũ, nhưng đáng nói là tôi đảm nhận điều này:

import Foundation
import UIKit

class PaginatedCardScrollView: UIScrollView {

    convenience init() {
        self.init(frame: CGRect.zero)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        _setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        _setup()
    }

    private func _setup() {
        isPagingEnabled = true
        isScrollEnabled = true
        clipsToBounds = false
        showsHorizontalScrollIndicator = false
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        // Asume the scrollview extends uses the entire width of the screen
        return point.y >= frame.origin.y && point.y <= frame.origin.y + frame.size.height
    }
}

Bằng cách này, bạn có thể a) sử dụng toàn bộ chiều rộng của chế độ xem cuộn để xoay / vuốt và b) có thể tương tác với các phần tử nằm ngoài giới hạn ban đầu của chế độ xem cuộn


0

Đối với vấn đề UICollectionView (đối với tôi là UITableViewCell gồm một bộ sưu tập các thẻ cuộn theo chiều ngang với "mã" của thẻ sắp có / trước đó), tôi chỉ cần từ bỏ việc sử dụng phân trang gốc của Apple. Giải pháp github của Damien rất hiệu quả đối với tôi. Bạn có thể điều chỉnh kích thước tickler bằng cách tăng chiều rộng tiêu đề và tự động định kích thước nó thành 0 khi ở chỉ mục đầu tiên để bạn không kết thúc với một lề trống lớn

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.