ios 按单元格分页 UICollectionView,而不是屏幕

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/22895465/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 23:55:26  来源:igfitidea点击:

Paging UICollectionView by cells, not screen

iosobjective-cuicollectionviewuicollectionviewlayout

提问by Martin Koles

I have UICollectionViewwith horizontal scrolling and there are always 2 cells side-by-side per the entire screen. I need the scrolling to stop at the begining of a cell. With paging enabled, the collection view scrolls the whole page, which is 2 cells at once, and then it stops.

我有UICollectionView水平滚动,并且每个屏幕总是并排有 2 个单元格。我需要滚动停止在单元格的开头。启用分页后,集合视图会滚动整个页面,即一次 2 个单元格,然后停止。

I need to enable scrolling by a single cell, or scrolling by multiple cells with stopping at the edge of the cell.

我需要启用单个单元格滚动,或者在单元格边缘停止滚动多个单元格。

I tried to subclass UICollectionViewFlowLayoutand to implement the method targetContentOffsetForProposedContentOffset, but so far I was only able to break my collection view and it stopped scrolling. Is there any easier way to achieve this and how, or do I really need to implement all methods of UICollectionViewFlowLayoutsubclass? Thanks.

我试图子类化UICollectionViewFlowLayout并实现该方法targetContentOffsetForProposedContentOffset,但到目前为止我只能打破我的集合视图并且它停止滚动。有没有更简单的方法来实现这一点以及如何实现,或者我真的需要实现UICollectionViewFlowLayout子类的所有方法吗?谢谢。

采纳答案by Martin Koles

OK, so I found the solution here: targetContentOffsetForProposedContentOffset:withScrollingVelocity without subclassing UICollectionViewFlowLayout

好的,所以我在这里找到了解决方案:targetContentOffsetForProposedContentOffset:withScrollingVelocity 没有子类化 UICollectionViewFlowLayout

I should have searched for targetContentOffsetForProposedContentOffsetin the begining.

我应该targetContentOffsetForProposedContentOffset在一开始就搜索。

回答by evya

just override the method:

只需覆盖该方法:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    *targetContentOffset = scrollView.contentOffset; // set acceleration to 0.0
    float pageWidth = (float)self.articlesCollectionView.bounds.size.width;
    int minSpace = 10;

    int cellToSwipe = (scrollView.contentOffset.x)/(pageWidth + minSpace) + 0.5; // cell width + min spacing for lines
    if (cellToSwipe < 0) {
        cellToSwipe = 0;
    } else if (cellToSwipe >= self.articles.count) {
        cellToSwipe = self.articles.count - 1;
    }
    [self.articlesCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:cellToSwipe inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}

回答by fredpi

Horizontal Paging With Custom Page Width (Swift 4 & 5)

具有自定义页面宽度的水平分页(Swift 4 & 5)

Many solutions presented here result in some weird behaviour that doesn't feel like properly implemented paging.

这里介绍的许多解决方案会导致一些奇怪的行为,感觉不像是正确实现的分页。



The solution presented in this tutorial, however, doesn't seem to have any issues. It just feels like a perfectly working paging algorithm. You can implement it in 5 simple steps:

但是,本教程中提供的解决方案似乎没有任何问题。感觉就像一个完美的分页算法。您可以通过 5 个简单的步骤来实现它:

  1. Add the following property to your type: private var indexOfCellBeforeDragging = 0
  2. Set the collectionViewdelegatelike this: collectionView.delegate = self
  3. Add conformance to UICollectionViewDelegatevia an extension: extension YourType: UICollectionViewDelegate { }
  4. Add the following method to the extension implementing the UICollectionViewDelegateconformance and set a value for pageWidth:

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        let pageWidth = // The width your page should have (plus a possible margin)
        let proportionalOffset = collectionView.contentOffset.x / pageWidth
        indexOfCellBeforeDragging = Int(round(proportionalOffset))
    }
    
  5. Add the following method to the extension implementing the UICollectionViewDelegateconformance, set the same value for pageWidth(you may also store this value at a central place) and set a value for collectionViewItemCount:

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        // Stop scrolling
        targetContentOffset.pointee = scrollView.contentOffset
    
        // Calculate conditions
        let pageWidth = // The width your page should have (plus a possible margin)
        let collectionViewItemCount = // The number of items in this section
        let proportionalOffset = collectionView.contentOffset.x / pageWidth
        let indexOfMajorCell = Int(round(proportionalOffset))
        let swipeVelocityThreshold: CGFloat = 0.5
        let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < collectionViewItemCount && velocity.x > swipeVelocityThreshold
        let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold
        let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging
        let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell)
    
        if didUseSwipeToSkipCell {
            // Animate so that swipe is just continued
            let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1)
            let toValue = pageWidth * CGFloat(snapToIndex)
            UIView.animate(
                withDuration: 0.3,
                delay: 0,
                usingSpringWithDamping: 1,
                initialSpringVelocity: velocity.x,
                options: .allowUserInteraction,
                animations: {
                    scrollView.contentOffset = CGPoint(x: toValue, y: 0)
                    scrollView.layoutIfNeeded()
                },
                completion: nil
            )
        } else {
            // Pop back (against velocity)
            let indexPath = IndexPath(row: indexOfMajorCell, section: 0)
            collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
        }
    }
    
  1. 将以下属性添加到您的类型: private var indexOfCellBeforeDragging = 0
  2. collectionViewdelegate像这样设置:collectionView.delegate = self
  3. UICollectionViewDelegate通过扩展添加一致性:extension YourType: UICollectionViewDelegate { }
  4. 将以下方法添加到实现UICollectionViewDelegate一致性的扩展中,并为 设置一个值pageWidth

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        let pageWidth = // The width your page should have (plus a possible margin)
        let proportionalOffset = collectionView.contentOffset.x / pageWidth
        indexOfCellBeforeDragging = Int(round(proportionalOffset))
    }
    
  5. 将以下方法添加到实现UICollectionViewDelegate一致性的扩展中,为 设置相同的值pageWidth(您也可以将此值存储在中央位置)并为 设置一个值collectionViewItemCount

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        // Stop scrolling
        targetContentOffset.pointee = scrollView.contentOffset
    
        // Calculate conditions
        let pageWidth = // The width your page should have (plus a possible margin)
        let collectionViewItemCount = // The number of items in this section
        let proportionalOffset = collectionView.contentOffset.x / pageWidth
        let indexOfMajorCell = Int(round(proportionalOffset))
        let swipeVelocityThreshold: CGFloat = 0.5
        let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < collectionViewItemCount && velocity.x > swipeVelocityThreshold
        let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold
        let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging
        let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell)
    
        if didUseSwipeToSkipCell {
            // Animate so that swipe is just continued
            let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1)
            let toValue = pageWidth * CGFloat(snapToIndex)
            UIView.animate(
                withDuration: 0.3,
                delay: 0,
                usingSpringWithDamping: 1,
                initialSpringVelocity: velocity.x,
                options: .allowUserInteraction,
                animations: {
                    scrollView.contentOffset = CGPoint(x: toValue, y: 0)
                    scrollView.layoutIfNeeded()
                },
                completion: nil
            )
        } else {
            // Pop back (against velocity)
            let indexPath = IndexPath(row: indexOfMajorCell, section: 0)
            collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
        }
    }
    

回答by StevenOjo

Swift 3 version of Evya's answer:

Evya 答案的 Swift 3 版本:

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
  targetContentOffset.pointee = scrollView.contentOffset
    let pageWidth:Float = Float(self.view.bounds.width)
    let minSpace:Float = 10.0
    var cellToSwipe:Double = Double(Float((scrollView.contentOffset.x))/Float((pageWidth+minSpace))) + Double(0.5)
    if cellToSwipe < 0 {
        cellToSwipe = 0
    } else if cellToSwipe >= Double(self.articles.count) {
        cellToSwipe = Double(self.articles.count) - Double(1)
    }
    let indexPath:IndexPath = IndexPath(row: Int(cellToSwipe), section:0)
    self.collectionView.scrollToItem(at:indexPath, at: UICollectionViewScrollPosition.left, animated: true)


}

回答by Romulo BM

Here's the easiest way that i found to do that in Swift 4.2for horinzontalscroll:

这是我在Swift 4.2 中发现的用于水平滚动的最简单方法:

I'm using the first cell on visibleCellsand scrolling to then, if the first visible cell are showing less of the half of it's width i'm scrolling to the next one.

我正在使用第一个单元格visibleCells并滚动到然后,如果第一个可见单元格显示的宽度小于其宽度的一半,我将滚动到下一个单元格。

If your collection scroll vertically, simply change xby yand widthby height

如果您收藏滚动垂直,只需更改x通过ywidth通过height

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    targetContentOffset.pointee = scrollView.contentOffset
    var indexes = self.collectionView.indexPathsForVisibleItems
    indexes.sort()
    var index = indexes.first!
    let cell = self.collectionView.cellForItem(at: index)!
    let position = self.collectionView.contentOffset.x - cell.frame.origin.x
    if position > cell.frame.size.width/2{
       index.row = index.row+1
    }
    self.collectionView.scrollToItem(at: index, at: .left, animated: true )
}

回答by John Cido

Partly based on StevenOjo's answer. I've tested this using a horizontal scrolling and no Bounce UICollectionView. cellSize is CollectionViewCell size. You can tweak factor to modify scrolling sensitivity.

部分基于 StevenOjo 的回答。我已经使用水平滚动和没有 Bounce UICollectionView 对此进行了测试。cellSize 是 CollectionViewCell 大小。您可以调整因子来修改滚动灵敏度。

override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    targetContentOffset.pointee = scrollView.contentOffset
    var factor: CGFloat = 0.5
    if velocity.x < 0 {
        factor = -factor
    }
    let indexPath = IndexPath(row: (scrollView.contentOffset.x/cellSize.width + factor).int, section: 0)
    collectionView?.scrollToItem(at: indexPath, at: .left, animated: true)
}

回答by JoniVR

Here's my implementation in Swift 5for verticalcell-based paging:

这是我在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)
}

Some notes:

一些注意事项:

  • Doesn't glitch
  • SET PAGING TO FALSE! (otherwise this won't work)
  • Allows you to set your own flickvelocityeasily.
  • If something is still not working after trying this, check if your itemSizeactually matches the size of the item as that's often a problem, especially when using collectionView(_:layout:sizeForItemAt:), use a custom variable with the itemSize instead.
  • This works best when you set self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.
  • 不会出现故障
  • 将分页设置为假!(否则这将不起作用)
  • 允许您轻松设置自己的轻弹速度
  • 如果尝试此操作后仍然无法正常工作,请检查您的itemSize实际大小是否与项目的大小匹配,因为这通常是一个问题,尤其是在使用时collectionView(_:layout:sizeForItemAt:),请改用带有 itemSize 的自定义变量。
  • 当您设置self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Here's a horizontalversion (haven't tested it thoroughly so please forgive any mistakes):

这是一个横向版本(尚未彻底测试,因此请原谅任何错误):

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)
}

This code is based on the code I use in my personal project, you can check it out hereby downloading it and running the Example target.

此代码基于我在个人项目中使用的代码,您可以在此处下载并运行 Example 目标来查看它。

回答by user1046037

Approach 1: Collection View

方法 1:集合视图

flowLayoutis UICollectionViewFlowLayoutproperty

flowLayoutUICollectionViewFlowLayout财产

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

    if let collectionView = collectionView {

        targetContentOffset.memory = scrollView.contentOffset
        let pageWidth = CGRectGetWidth(scrollView.frame) + flowLayout.minimumInteritemSpacing

        var assistanceOffset : CGFloat = pageWidth / 3.0

        if velocity.x < 0 {
            assistanceOffset = -assistanceOffset
        }

        let assistedScrollPosition = (scrollView.contentOffset.x + assistanceOffset) / pageWidth

        var targetIndex = Int(round(assistedScrollPosition))


        if targetIndex < 0 {
            targetIndex = 0
        }
        else if targetIndex >= collectionView.numberOfItemsInSection(0) {
            targetIndex = collectionView.numberOfItemsInSection(0) - 1
        }

        print("targetIndex = \(targetIndex)")

        let indexPath = NSIndexPath(forItem: targetIndex, inSection: 0)

        collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Left, animated: true)
    }
}

Approach 2: Page View Controller

方法 2:页面视图控制器

You could use UIPageViewControllerif it meets your requirements, each page would have a separate view controller.

UIPageViewController如果它满足您的要求,您可以使用,每个页面都有一个单独的视图控制器。

回答by Moose

This is a straight way to do this.

这是执行此操作的直接方法。

The case is simple, but finally quite common ( typical thumbnails scroller with fixed cell size and fixed gap between cells )

案例很简单,但最终很常见(典型的缩略图滚动条具有固定的单元格大小和固定的单元格之间的间隙)

var itemCellSize: CGSize = <your cell size>
var itemCellsGap: CGFloat = <gap in between>

override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let pageWidth = (itemCellSize.width + itemCellsGap)
    let itemIndex = (targetContentOffset.pointee.x) / pageWidth
    targetContentOffset.pointee.x = round(itemIndex) * pageWidth - (itemCellsGap / 2)
}

// CollectionViewFlowLayoutDelegate

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return itemCellSize
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return itemCellsGap
}

Note that there is no reason to call a scrollToOffset or dive into layouts. The native scrolling behaviour already does everything.

请注意,没有理由调用 scrollToOffset 或深入布局。本机滚动行为已经完成了一切。

Cheers All :)

干杯 :)

回答by skensell

Kind of like evya's answer, but a little smoother because it doesn't set the targetContentOffset to zero.

有点像 evya 的回答,但更流畅一点,因为它没有将 targetContentOffset 设置为零。

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    if ([scrollView isKindOfClass:[UICollectionView class]]) {
        UICollectionView* collectionView = (UICollectionView*)scrollView;
        if ([collectionView.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]]) {
            UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout;

            CGFloat pageWidth = layout.itemSize.width + layout.minimumInteritemSpacing;
            CGFloat usualSideOverhang = (scrollView.bounds.size.width - pageWidth)/2.0;
            // k*pageWidth - usualSideOverhang = contentOffset for page at index k if k >= 1, 0 if k = 0
            // -> (contentOffset + usualSideOverhang)/pageWidth = k at page stops

            NSInteger targetPage = 0;
            CGFloat currentOffsetInPages = (scrollView.contentOffset.x + usualSideOverhang)/pageWidth;
            targetPage = velocity.x < 0 ? floor(currentOffsetInPages) : ceil(currentOffsetInPages);
            targetPage = MAX(0,MIN(self.projects.count - 1,targetPage));

            *targetContentOffset = CGPointMake(MAX(targetPage*pageWidth - usualSideOverhang,0), 0);
        }
    }
}