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

Posted

技术标签:

【中文标题】带有分页的 UICollectionView - 设置页面宽度【英文标题】:UICollectionView with paging - setting page width 【发布时间】:2013-12-10 14:17:35 【问题描述】:

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

【问题讨论】:

【参考方案1】:

另一种方法是创建一个自定义 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);


如果您正在寻找 Swift 解决方案,请查看此Gist

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

【讨论】:

一般来说,您应该在答案正文中勾勒出您的解决方案,而不是仅仅包含一个链接,这样如果链接断开,答案仍然有用。 这是我能找到的唯一解决方案,可以在 UICollectionView 中按单元格成功实现垂直分页。显然,您必须修改代码以使用 y 值而不是 x 值,但它可以完美运行并且感觉就像正常的 pagingEnabled。谢谢迈克! @MikeM - 一段很棒的代码人!至少测试了其中的十几个,但都失败了。唯一的问题发生在用户快速轻弹时,这将导致最近的单元格被跳过。知道如何解决这个问题吗?谢谢! @Thomas - 我做到了,只需设置 [yourScrollView].decelerationRate = UIScrollViewDecelerationRateFast 就可以了! @trdavidson 我在我的博文中加入了您的解决方案并引用了您的名字。谢谢!【参考方案2】:

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

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

    guard let collectionView = self.collectionView else 
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    

    // 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 = collectionView.contentOffset.y/pageHeight

    // Determine the current page based on velocity.
    let currentPage = velocity.y == 0 ? round(approximatePage) : (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) - collectionView.contentInset.top

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

一些注意事项:

不会出现故障 将分页设置为假! (否则这将不起作用) 允许您轻松设置您自己的轻弹速度。 如果尝试此操作后仍有问题,请检查您的 itemSize 是否确实与项目的大小匹配,因为这通常是一个问题,尤其是在使用 collectionView(_:layout:sizeForItemAt:) 时,请改用带有 itemSize 的自定义变量。李> 设置self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast 时效果最佳。

这是一个横向版本(未彻底测试,如有错误请见谅):

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

    guard let collectionView = self.collectionView else 
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    

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

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

    // Determine the current page based on velocity.
    let currentPage = velocity.x == 0 ? round(approximatePage) : (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) - collectionView.contentInset.left

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

此代码基于我在个人项目中使用的代码,您可以通过下载并运行示例目标来查看here。

【讨论】:

谢谢!您的解决方案是唯一有效的解决方案!我不得不用自定义计算替换 itemSize,因为 itemSize 仅在您不使用 collectionView(_:layout:sizeForItemAt:) 时才使用 developer.apple.com/documentation/uikit/… 由于人们在使用此方法时可能会使用collectionView(_:layout:sizeForItemAt:),因此将itemSize的使用替换为其他东西可能会更好。 如果可以的话,我会多次投票,我已经为此苦苦挣扎了很长时间,其他答案都没有为我工作。 很好的解决方案!虽然水平滚动的答案也应该使用 self.minimumLineSpacing 而不是 self.minimumInteritemSpacing。还结合这个答案并翻转那些最小值,我通过设置 self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast 获得了感觉更好的滚动结果。 在没有速度的情况下释放拖动时发现了一个小问题 - 基本上,集合视图将捕捉到最近的下一个项目。用let currentPage = velocity.x == 0 ? round(approximatePage) : (velocity.x &lt; 0.0 ? floor(approximatePage) : ceil(approximatePage)) 替换let currentPage = (velocity.x &lt; 0.0) ? floor(approximatePage) : ceil(approximatePage) 似乎会产生预期的捕捉到最近的项目。 (注意:这是针对水平版本的。垂直等效应该很容易解决)【参考方案3】:

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

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

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

【讨论】:

我认为上面的方法不允许你改变targetContentOffset... @MikeM 是的,这就是为什么它是一个 inout 参数;你可以使用*targetContentOffset = CGPointMake(...)之类的东西。 @JesseRusak - 我真的很喜欢这个想法,但是当我尝试实现它时,collectionview 会快速滚动。关于如何阻止这种情况的任何建议?谢谢! @trdavidson 我不确定;这取决于您如何实现该方法。也许发布一个新问题并将其链接到此处? @JesseRusak 我最终使用了 MikeM 他的自定义 FlowLayout 实现,因为它工作得非常好。谢谢你回来,我可能会在以后摆弄这个,看看我能不能让它工作 - 干杯【参考方案4】:

在查看此处的解决方案之前,我开发了我的解决方案。我还创建了一个自定义 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)

【讨论】:

我无法编译,因为我的视图控制器中缺少self.layoutAttributesForElements 花了 2 天时间试图找到一个解决方案,其中一些工作几乎完美,但最终还是不够好。我拿了这个并对其进行了调整,结果很完美。谢谢【参考方案5】:

Mike M. 之前在帖子中提出的解决方案对我有用,但在我的情况下,我希望第一个单元格从 collectionView 的中间开始。所以我使用集合流委托方法来定义一个插入(collectionView:layout:insetForSectionAtIndex:)。这使得第一个单元格和第二个单元格之间的滚动被卡住并且无法正确滚动到第一个单元格。

原因是candidateAttributes.center.x - halfWidth 的值为负。解决方案是获取绝对值,所以我将 fabs 添加到这一行 return CGPointMake(fabs(candidateAttributes.center.x - halfWidth), offset.y);

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

【讨论】:

【参考方案6】:

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

设置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];

【讨论】:

以上是关于带有分页的 UICollectionView - 设置页面宽度的主要内容,如果未能解决你的问题,请参考以下文章

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

在启用分页的情况下使两个 UICollectionView 同步滚动

iOS 中带有分页的 UICollectionView 的页数

滚动到 UICollectionView 中启用分页的页面

使用 Swift 创建分页 UICollectionView

UICollectionView 在启用分页的情况下不会滚动完整的 320 点