带分页的UICollectionView - 设置页面宽度

时间:2013-12-10 14:17:35

标签: ios uicollectionview

我有一个Collection View,一次可以显示大约3.5个单元格,我希望它能够启用分页。但我希望它能够捕捉到每个单元格(就像App Store应用程序一样),而不是滚动视图的整个宽度。我怎么能这样做?

6 个答案:

答案 0 :(得分:50)

另一种方法是创建一个自定义的UICollectionViewFlowLayout并覆盖这样的方法:

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset 
                                 withScrollingVelocity:(CGPoint)velocity {

    CGRect cvBounds = self.collectionView.bounds;
    CGFloat halfWidth = cvBounds.size.width * 0.5f;
    CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;

    NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];

    UICollectionViewLayoutAttributes* candidateAttributes;
    for (UICollectionViewLayoutAttributes* attributes in attributesArray) {

        // == Skip comparison with non-cell items (headers and footers) == //
        if (attributes.representedElementCategory != 
            UICollectionElementCategoryCell) {
            continue;
        }

        // == First time in the loop == //
        if(!candidateAttributes) {
            candidateAttributes = attributes;
            continue;
        }

        if (fabsf(attributes.center.x - proposedContentOffsetCenterX) < 
            fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
            candidateAttributes = attributes;
        }
    }

    return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);

}

https://gist.github.com/mmick66/9812223

  • 注意:当我们真正显示预览单元格时(即使它们的alpha值为0.0f),这将有效。这是因为如果预览单元格在滚动时不可用,则它们的属性对象将不会在循环中传递...

如果您正在寻找 Swift 解决方案,我还创建了一点tutorial that includes code

答案 1 :(得分:16)

您可以通过成为集合视图的委托并实现方法来捕捉单元格:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset

这告诉您用户已完成拖动,并允许您修改targetContentOffset以与您的单元格对齐(即舍入到最近的单元格)。请注意,您需要注意如何修改targetContentOffset;特别是,你需要避免更改它,以便视图需要在传递速度的相反方向滚动,否则你会得到动画故障。如果你谷歌获取该方法名称,你可能会发现很多这样的例子。

答案 2 :(得分:9)

这是我在垂直基于单元格的分页的 Swift 5 中的实现:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    // Page height used for estimating and calculating paging.
    let pageHeight = self.itemSize.height + self.minimumLineSpacing

    // Make an estimation of the current page position.
    let approximatePage = self.collectionView!.contentOffset.y/pageHeight

    // Determine the current page based on velocity.
    let currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage)

    // Create custom flickVelocity.
    let flickVelocity = velocity.y * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - self.collectionView!.contentInset.top

    return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}

一些说明:

  • 不会出现故障
  • 不要忘记将默认分页设置为false
  • 允许您轻松设置您自己的flickvelocity
  • 如果在尝试此操作后某些内容仍然无效,请检查您的itemSize是否与项目的大小相符,因为这通常是个问题。
  • 设置self.collectionView.decelerationRate = UIScollViewDecelerationRateFast
  • 时效果最佳

这是横向版本(未经过彻底测试,请原谅任何错误):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    // Page width used for estimating and calculating paging.
    let pageWidth = self.itemSize.width + self.minimumLineSpacing

    // Make an estimation of the current page position.
    let approximatePage = self.collectionView!.contentOffset.x/pageWidth

    // Determine the current page based on velocity.
    let currentPage = (velocity.x < 0.0) ? floor(approximatePage) : ceil(approximatePage)

    // Create custom flickVelocity.
    let flickVelocity = velocity.x * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    // Calculate newHorizontalOffset.
    let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - self.collectionView!.contentInset.left

    return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y)
}

答案 3 :(得分:3)

在查看这里的解决方案之前,我开发了我的解决方案。我还创建了一个自定义的UICollectionViewFlowLayout并覆盖了targetContentOffset方法。

它似乎对我很好(即我得到与AppStore相同的行为),即使我的代码少得多。在这里,请随意指出我能想到的任何缺点:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    let inset: Int = 10
    let vcBounds = self.collectionView!.bounds
    var candidateContentOffsetX: CGFloat = proposedContentOffset.x

    for attributes in self.layoutAttributesForElements(in: vcBounds)! as [UICollectionViewLayoutAttributes] {

        if vcBounds.origin.x < attributes.center.x {
            candidateContentOffsetX = attributes.frame.origin.x - CGFloat(inset)
            break
        }

    }

    return CGPoint(x: candidateContentOffsetX, y: proposedContentOffset.y)
}

答案 4 :(得分:0)

解决方案Mike M.在为我工作之前在帖子中提出但是在我的情况下我想让第一个单元格从collectionView中间开始。所以我使用了集合流委托方法来定义一个插入(collectionView:layout:insetForSectionAtIndex:)。这使得第一个单元格和第二个单元格之间的滚动被卡住而不能正确滚动到第一个单元格。

原因是candidateAttributes.center.x - halfWidth具有负值。解决方案是获得绝对值,因此我将fab添加到此行return CGPointMake(fabs(candidateAttributes.center.x - halfWidth), offset.y);

默认情况下应添加Fab以涵盖所有情况。

答案 5 :(得分:0)

这是我的解决方案。适用于任何页面宽度。

设置self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast以感受真正的分页。

解决方案基于一个部分来滚动按项目分页。

- (CGFloat)pageWidth {

    return self.itemSize.width + self.minimumLineSpacing;
}

- (CGPoint)offsetAtCurrentPage {

    CGFloat width = -self.collectionView.contentInset.left - self.sectionInset.left;
    for (int i = 0; i < self.currentPage; i++)
        width += [self pageWidth];

    return CGPointMake(width, 0);
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset {

    return [self offsetAtCurrentPage];
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {

    // To scroll paginated
    /*
    if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfItemsInSection:0]-1) self.currentPage += 1;
    else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;

    return  [self offsetAtCurrentPage];
    */

    // To scroll and stop always at the center of a page
    CGRect proposedRect = CGRectMake(proposedContentOffset.x+self.collectionView.bounds.size.width/2 - self.pageWidth/2, 0, self.pageWidth, self.collectionView.bounds.size.height);
    NSMutableArray <__kindof UICollectionViewLayoutAttributes *> *allAttributes = [[self layoutAttributesForElementsInRect:proposedRect] mutableCopy];
    __block UICollectionViewLayoutAttributes *proposedAttributes = nil;
    __block CGFloat minDistance = CGFLOAT_MAX;
    [allAttributes enumerateObjectsUsingBlock:^(__kindof UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

        CGFloat distance = CGRectGetMidX(proposedRect) - obj.center.x;

        if (ABS(distance) < minDistance) {
            proposedAttributes = obj;
            minDistance = distance;
        }
    }];


    // Scroll always
    if (self.currentPage == proposedAttributes.indexPath.row) {
        if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfItemsInSection:0]-1) self.currentPage += 1;
        else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;
    }
    else  {
        self.currentPage = proposedAttributes.indexPath.row;
    }

    return  [self offsetAtCurrentPage];
}

这是按部分分页的。

- (CGPoint)offsetAtCurrentPage {

    CGFloat width = -self.collectionView.contentInset.leff;
    for (int i = 0; i < self.currentPage; i++)
        width += [self sectionWidth:i];
    return CGPointMake(width, 0);
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
    return [self offsetAtCurrentPage];
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {

    // To scroll paginated
    /*
    if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfSections]-1) self.currentPage += 1;
    else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;

    return  [self offsetAtCurrentPage];
    */

    // To scroll and stop always at the center of a page
    CGRect proposedRect = CGRectMake(proposedContentOffset.x+self.collectionView.bounds.size.width/2 - [self sectionWidth:0]/2, 0, [self sectionWidth:0], self.collectionView.bounds.size.height);
    NSMutableArray <__kindof UICollectionViewLayoutAttributes *> *allAttributes = [[self layoutAttributesForElementsInRect:proposedRect] mutableCopy];
    __block UICollectionViewLayoutAttributes *proposedAttributes = nil;
    __block CGFloat minDistance = CGFLOAT_MAX;
    [allAttributes enumerateObjectsUsingBlock:^(__kindof UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

        CGFloat distance = CGRectGetMidX(proposedRect) - obj.center.x;

        if (ABS(distance) < minDistance) {
            proposedAttributes = obj;
            minDistance = distance;
        }
    }];

    // Scroll always
    if (self.currentPage == proposedAttributes.indexPath.section) {
        if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfSections]-1) self.currentPage += 1;
        else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;
    }
    else  {
        self.currentPage = proposedAttributes.indexPath.section;
    }

    return  [self offsetAtCurrentPage];
}