【问题标题】:Paging UICollectionView by cells, not screen按单元格而不是屏幕分页 UICollectionView
【发布时间】:2014-05-18 16:41:19
【问题描述】:

我的UICollectionView 具有水平滚动功能,并且在整个屏幕上总是有 2 个并排的单元格。我需要滚动停止在单元格的开头。启用分页后,collection view 会滚动整个页面,一次滚动 2 个单元格,然后停止。

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

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

【问题讨论】:

  • 你的collectionviewcell宽度必须等于screnn宽度并且collectionView分页启用
  • 但我需要一次显示 2 个单元格。我在 iPad 上,所以 2 个单元格各共享一半屏幕。
  • 使用targetContentOffsetForProposedContentOffset:withScrollingVelocity:并关闭分页
  • 这就是我正在尝试的。有什么例子吗?

标签: ios objective-c uicollectionview uicollectionviewlayout


【解决方案1】:

好的,所以我在这里找到了解决方案:targetContentOffsetForProposedContentOffset:withScrollingVelocity without subclassing UICollectionViewFlowLayout

一开始我应该搜索targetContentOffsetForProposedContentOffset

【讨论】:

  • 对于任何在 swift 5 中询问此问题的人:collectionView.isPagingEnabled = true 这样做!
  • @Mohammad Bashir Sidani 不,我没有。阅读问题。
【解决方案2】:

只需重写方法:

- (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];
}

【讨论】:

  • 那段代码对我帮助很大,但我不得不添加对当前滚动方向的检查,并相应地调整了 +/- 0.5 的值。
  • 可以设置collectionView.pagingEnabled = true
  • @evya 哇,你是对的。 isPagingEnabled 对我有用。
  • @evya 好东西!!
  • pagingEnabled 如何为你们工作?在以原始分页偏移结束之前,我的会出现超级故障
【解决方案3】:

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

【讨论】:

  • 对于 Swift 5:使用 .fast 而不是 UIScollViewDecelerationRateFast
  • 感谢您指出这一点!忘记更新这个答案了!
  • 嗨,@JoniVR 非常好的解释示例,展示了滑动如何垂直工作。非常好心地建议您需要对整体代码进行哪些更改才能使这项工作在水平方向上完美无缺。除了您建议的目标内容偏移功能中的水平滑动代码之外。我认为要横向复制确切的场景需要进行很多更改。如果我错了,请纠正我。
【解决方案4】:

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

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


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

  1. 将以下属性添加到您的类型:private var indexOfCellBeforeDragging = 0
  2. 像这样设置collectionViewdelegatecollectionView.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)
        }
    }
    

【讨论】:

  • 对于任何使用它的人,您需要将Pop back (against velocity) 部分更改为:collectionViewLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)。注意.centeredHorizontally
  • @matthew.kempson 取决于您希望布局的行为方式。对于我使用的布局,.left 很好
  • 我发现.left 没有按预期工作。它似乎把细胞推得太远了@fredpi
【解决方案5】:

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

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

如果您的收藏垂直滚动,只需将x 更改为y 并将width 更改为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 )
}

【讨论】:

  • 能否请您将链接添加到源文章。顺便说一句,答案很好。
  • @Md.IbrahimHassan 没有文章,我是来源。谢谢
  • 可以用,可惜体验不流畅
  • 不流畅是什么意思?对我来说,结果动画非常流畅..看我的结果here
【解决方案6】:

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)


}

【讨论】:

  • 当你点击单元格的一侧时,会有一个奇怪的偏移量
  • 嘿@Maor,我不知道你是否还需要它,但在我的情况下,它已修复在集合视图上禁用分页。
  • 我喜欢这个,但是快速的小滑动感觉有点迟钝,所以我添加了一些东西来考虑速度并使它更平滑:if(velocity.x &gt; 1) { mod = 0.5; } else if(velocity.x &lt; -1) { mod = -0.5; } 然后在+ Double(0.5)之后添加+ mod
【解决方案7】:

部分基于 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)
}

【讨论】:

    【解决方案8】:

    方法一:集合视图

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

    方法 2:页面视图控制器

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

    【讨论】:

    • 为此我必须禁用分页并仅在collectionview中启用滚动?
    • 这不适用于最新的 swift4/Xcode9.3,targetContentOffset 没有内存字段。我实现了滚动,但是当你“轻弹”时它没有调整单元格的位置。
    • 适用于几个单元格,但当我到达单元格 13 时,它开始跳回上一个单元格,您无法继续。
    【解决方案9】:

    修改 Romulo BM 答案以进行速度监听

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

    【讨论】:

      【解决方案10】:

      这是一种直接的方法。

      情况很简单,但最后很常见(典型的缩略图滚动器,具有固定单元格大小和固定单元格之间的间隙)

      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
      }
      

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

      祝大家好运:)

      【讨论】:

      • 您可以选择将collectionView.decelerationRate = .fast 设置为更接近模仿的默认分页。
      • 这真是太好了。 @elfanek 我发现该设置可以正常工作,除非您轻轻滑动一下,然后它似乎会快速闪烁。
      【解决方案11】:

      这是 Swift5 中的优化解决方案,包括处理错误的 indexPath。 - Michael Lin Liu

      • 步骤 1。获取当前单元格的 indexPath。
      • 步骤 2。检测滚动时的速度。
      • 步骤 3。速度增加时增加 indexPath 的行数。
      • 第四步。告诉集合视图滚动到下一个项目
          func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
              
              targetContentOffset.pointee = scrollView.contentOffset
              
              //M: Get the first visiable item's indexPath from visibaleItems.
              var indexPaths = *YOURCOLLECTIONVIEW*.indexPathsForVisibleItems
              indexPaths.sort()
              var indexPath = indexPaths.first!
      
              //M: Use the velocity to detect the paging control movement.
              //M: If the movement is forward, then increase the indexPath.
              if velocity.x > 0{
                  indexPath.row += 1
                  
                  //M: If the movement is in the next section, which means the indexPath's row is out range. We set the indexPath to the first row of the next section.
                  if indexPath.row == *YOURCOLLECTIONVIEW*.numberOfItems(inSection: indexPath.section){
                      indexPath.row = 0
                      indexPath.section += 1
                  }
              }
              else{
                  //M: If the movement is backward, the indexPath will be automatically changed to the first visiable item which is indexPath.row - 1. So there is no need to write the logic.
              }
              
              //M: Tell the collection view to scroll to the next item.
              *YOURCOLLECTIONVIEW*.scrollToItem(at: indexPath, at: .left, animated: true )
       }
      

      【讨论】:

        【解决方案12】:

        有点像 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);
                }
            }
        }
        

        【讨论】:

          【解决方案13】:

          这是我在 Swift 3 中的版本。计算滚动结束后的偏移量并用动画调整偏移量。

          collectionLayoutUICollectionViewFlowLayout()

          func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
              let index = scrollView.contentOffset.x / collectionLayout.itemSize.width
              let fracPart = index.truncatingRemainder(dividingBy: 1)
              let item= Int(fracPart >= 0.5 ? ceil(index) : floor(index))
          
              let indexPath = IndexPath(item: item, section: 0)
              collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
          }
          

          【讨论】:

            【解决方案14】:

            斯威夫特 5

            我找到了一种无需子类化 UICollectionView 的方法,只需计算水平方向的 contentOffset。显然没有 isPagingEnabled 设置为 true。 代码如下:

            var offsetScroll1 : CGFloat = 0
            var offsetScroll2 : CGFloat = 0
            let flowLayout = UICollectionViewFlowLayout()
            let screenSize : CGSize = UIScreen.main.bounds.size
            var items = ["1", "2", "3", "4", "5"]
            
            override func viewDidLoad() {
                super.viewDidLoad()
                flowLayout.scrollDirection = .horizontal
                flowLayout.minimumLineSpacing = 7
                let collectionView = UICollectionView(frame: CGRect(x: 0, y: 590, width: screenSize.width, height: 200), collectionViewLayout: flowLayout)
                collectionView.register(collectionViewCell1.self, forCellWithReuseIdentifier: cellReuseIdentifier)
                collectionView.delegate = self
                collectionView.dataSource = self
                collectionView.backgroundColor = UIColor.clear
                collectionView.showsHorizontalScrollIndicator = false
                self.view.addSubview(collectionView)
            }
            
            func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
                offsetScroll1 = offsetScroll2
            }
            
            func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
                offsetScroll1 = offsetScroll2
            }
            
            func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>){
                let indexOfMajorCell = self.desiredIndex()
                let indexPath = IndexPath(row: indexOfMajorCell, section: 0)
                flowLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
                targetContentOffset.pointee = scrollView.contentOffset
            }
            
            private func desiredIndex() -> Int {
                var integerIndex = 0
                print(flowLayout.collectionView!.contentOffset.x)
                offsetScroll2 = flowLayout.collectionView!.contentOffset.x
                if offsetScroll2 > offsetScroll1 {
                    integerIndex += 1
                    let offset = flowLayout.collectionView!.contentOffset.x / screenSize.width
                    integerIndex = Int(round(offset))
                    if integerIndex < (items.count - 1) {
                        integerIndex += 1
                    }
                }
                if offsetScroll2 < offsetScroll1 {
                    let offset = flowLayout.collectionView!.contentOffset.x / screenSize.width
                    integerIndex = Int(offset.rounded(.towardZero))
                }
                let targetIndex = integerIndex
                return targetIndex
            }
            

            【讨论】:

              【解决方案15】:

              您还可以创建假滚动视图来处理滚动。

              水平或垂直

              // === Defaults ===
              let bannerSize = CGSize(width: 280, height: 170)
              let pageWidth: CGFloat = 290 // ^ + paging
              let insetLeft: CGFloat = 20
              let insetRight: CGFloat = 20
              // ================
              
              var pageScrollView: UIScrollView!
              
              override func viewDidLoad() {
                  super.viewDidLoad()
              
                  // Create fake scrollview to properly handle paging
                  pageScrollView = UIScrollView(frame: CGRect(origin: .zero, size: CGSize(width: pageWidth, height: 100)))
                  pageScrollView.isPagingEnabled = true
                  pageScrollView.alwaysBounceHorizontal = true
                  pageScrollView.showsVerticalScrollIndicator = false
                  pageScrollView.showsHorizontalScrollIndicator = false
                  pageScrollView.delegate = self
                  pageScrollView.isHidden = true
                  view.insertSubview(pageScrollView, belowSubview: collectionView)
              
                  // Set desired gesture recognizers to the collection view
                  for gr in pageScrollView.gestureRecognizers! {
                      collectionView.addGestureRecognizer(gr)
                  }
              }
              
              func scrollViewDidScroll(_ scrollView: UIScrollView) {
                  if scrollView == pageScrollView {
                      // Return scrolling back to the collection view
                      collectionView.contentOffset.x = pageScrollView.contentOffset.x
                  }
              }
              
              func refreshData() {
                  ...
              
                  refreshScroll()
              }
              
              override func viewDidLayoutSubviews() {
                  super.viewDidLayoutSubviews()
              
                  refreshScroll()
              }
              
              /// Refresh fake scrolling view content size if content changes
              func refreshScroll() {
                  let w = collectionView.width - bannerSize.width - insetLeft - insetRight
                  pageScrollView.contentSize = CGSize(width: pageWidth * CGFloat(banners.count) - w, height: 100)
              }
              

              【讨论】:

                【解决方案16】:

                这是我的解决方案,在 Swift 4.2 中,我希望它可以帮助你。

                class SomeViewController: UIViewController {
                
                  private lazy var flowLayout: UICollectionViewFlowLayout = {
                    let layout = UICollectionViewFlowLayout()
                    layout.itemSize = CGSize(width: /* width */, height: /* height */)
                    layout.minimumLineSpacing = // margin
                    layout.minimumInteritemSpacing = 0.0
                    layout.sectionInset = UIEdgeInsets(top: 0.0, left: /* margin */, bottom: 0.0, right: /* margin */)
                    layout.scrollDirection = .horizontal
                    return layout
                  }()
                
                  private lazy var collectionView: UICollectionView = {
                    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
                    collectionView.showsHorizontalScrollIndicator = false
                    collectionView.dataSource = self
                    collectionView.delegate = self
                    // collectionView.register(SomeCell.self)
                    return collectionView
                  }()
                
                  private var currentIndex: Int = 0
                }
                
                // MARK: - UIScrollViewDelegate
                
                extension SomeViewController {
                  func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
                    guard scrollView == collectionView else { return }
                
                    let pageWidth = flowLayout.itemSize.width + flowLayout.minimumLineSpacing
                    currentIndex = Int(scrollView.contentOffset.x / pageWidth)
                  }
                
                  func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
                    guard scrollView == collectionView else { return }
                
                    let pageWidth = flowLayout.itemSize.width + flowLayout.minimumLineSpacing
                    var targetIndex = Int(roundf(Float(targetContentOffset.pointee.x / pageWidth)))
                    if targetIndex > currentIndex {
                      targetIndex = currentIndex + 1
                    } else if targetIndex < currentIndex {
                      targetIndex = currentIndex - 1
                    }
                    let count = collectionView.numberOfItems(inSection: 0)
                    targetIndex = max(min(targetIndex, count - 1), 0)
                    print("targetIndex: \(targetIndex)")
                
                    targetContentOffset.pointee = scrollView.contentOffset
                    var offsetX: CGFloat = 0.0
                    if targetIndex < count - 1 {
                      offsetX = pageWidth * CGFloat(targetIndex)
                    } else {
                      offsetX = scrollView.contentSize.width - scrollView.width
                    }
                    collectionView.setContentOffset(CGPoint(x: offsetX, y: 0.0), animated: true)
                  }
                }
                

                【讨论】:

                  【解决方案17】:

                  好的,所以建议的答案对我不起作用,因为我想按部分滚动,因此页面宽度可变

                  我这样做了(仅限垂直):

                     var pagesSizes = [CGSize]()
                     func scrollViewDidScroll(_ scrollView: UIScrollView) {
                          defer {
                              lastOffsetY = scrollView.contentOffset.y
                          }
                          if collectionView.isDecelerating {
                              var currentPage = 0
                              var currentPageBottom = CGFloat(0)
                              for pagesSize in pagesSizes {
                                  currentPageBottom += pagesSize.height
                                  if currentPageBottom > collectionView!.contentOffset.y {
                                      break
                                  }
                                  currentPage += 1
                              }
                              if collectionView.contentOffset.y > currentPageBottom - pagesSizes[currentPage].height, collectionView.contentOffset.y + collectionView.frame.height < currentPageBottom {
                                  return // 100% of view within bounds
                              }
                              if lastOffsetY < collectionView.contentOffset.y {
                                  if currentPage + 1 != pagesSizes.count {
                                      collectionView.setContentOffset(CGPoint(x: 0, y: currentPageBottom), animated: true)
                                  }
                              } else {
                                  collectionView.setContentOffset(CGPoint(x: 0, y: currentPageBottom - pagesSizes[currentPage].height), animated: true)
                              }
                          }
                      }
                  

                  在这种情况下,我使用节高+页眉+页脚预先计算每个页面大小,并将其存储在数组中。那是pagesSizes 成员

                  【讨论】:

                    【解决方案18】:

                    我创建了一个自定义集合视图布局here,它支持:

                    • 一次分页一个单元格
                    • 根据滑动速度一次分页 2+ 个单元格
                    • 水平或垂直方向

                    很简单:

                    let layout = PagingCollectionViewLayout()
                    
                    layout.itemSize = 
                    layout.minimumLineSpacing = 
                    layout.scrollDirection = 
                    

                    您可以将PagingCollectionViewLayout.swift 添加到您的项目中

                    pod 'PagingCollectionViewLayout' 添加到您的 podfile 中

                    【讨论】:

                    • 它工作得很好,但不确定为什么它在垂直滚动集合视图中只适用于单元格高度 600。尝试了其他高度值,但它会比项目滚动更多或更少。
                    【解决方案19】:
                    final class PagingFlowLayout: UICollectionViewFlowLayout {
                        private var currentIndex = 0
                    
                        override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
                            let count = collectionView!.numberOfItems(inSection: 0)
                            let currentAttribute = layoutAttributesForItem(
                                at: IndexPath(item: currentIndex, section: 0)
                                ) ?? UICollectionViewLayoutAttributes()
                    
                            let direction = proposedContentOffset.x > currentAttribute.frame.minX
                            if collectionView!.contentOffset.x + collectionView!.bounds.width < collectionView!.contentSize.width || currentIndex < count - 1 {
                                currentIndex += direction ? 1 : -1
                                currentIndex = max(min(currentIndex, count - 1), 0)
                            }
                    
                            let indexPath = IndexPath(item: currentIndex, section: 0)
                            let closestAttribute = layoutAttributesForItem(at: indexPath) ?? UICollectionViewLayoutAttributes()
                    
                            let centerOffset = collectionView!.bounds.size.width / 2
                            return CGPoint(x: closestAttribute.center.x - centerOffset, y: 0)
                        }
                    }
                    

                    【讨论】:

                    • 您不应复制/粘贴答案。如果合适,将其标记为重复。
                    【解决方案20】:

                    Олень Безрогий 的原答案有问题,所以最后一个单元格集合视图滚动到开头

                    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
                        targetContentOffset.pointee = scrollView.contentOffset
                        var indexes = yourCollectionView.indexPathsForVisibleItems
                        indexes.sort()
                        var index = indexes.first!
                        // if velocity.x > 0 && (Get the number of items from your data) > index.row + 1 {
                        if velocity.x > 0 && yourCollectionView.numberOfItems(inSection: 0) > index.row + 1 {
                           index.row += 1
                        } else if velocity.x == 0 {
                            let cell = yourCollectionView.cellForItem(at: index)!
                            let position = yourCollectionView.contentOffset.x - cell.frame.origin.x
                            if position > cell.frame.size.width / 2 {
                               index.row += 1
                            }
                        }
                        
                        yourCollectionView.scrollToItem(at: index, at: .centeredHorizontally, animated: true )
                    }
                    

                    【讨论】:

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

                      【讨论】:

                      • 请不要只发布代码作为答案,还要解释您的代码的作用以及它如何解决问题的问题。带有解释的答案通常更有帮助、质量更好,并且更有可能吸引投票。
                      【解决方案22】:

                      这是我使用UICollectionViewFlowLayout 覆盖targetContentOffset 的方法:

                      (虽然最后我没有使用这个,而是使用 UIPageViewController。)

                      /**
                       A UICollectionViewFlowLayout with...
                       - paged horizontal scrolling
                       - itemSize is the same as the collectionView bounds.size
                       */
                      class PagedFlowLayout: UICollectionViewFlowLayout {
                      
                        override init() {
                          super.init()
                          self.scrollDirection = .horizontal
                          self.minimumLineSpacing = 8 // line spacing is the horizontal spacing in horizontal scrollDirection
                          self.minimumInteritemSpacing = 0
                          if #available(iOS 11.0, *) {
                            self.sectionInsetReference = .fromSafeArea // for iPhone X
                          }
                        }
                      
                        required init?(coder aDecoder: NSCoder) {
                          fatalError("not implemented")
                        }
                      
                        // Note: Setting `minimumInteritemSpacing` here will be too late. Don't do it here.
                        override func prepare() {
                          super.prepare()
                          guard let collectionView = collectionView else { return }
                          collectionView.decelerationRate = UIScrollViewDecelerationRateFast // mostly you want it fast!
                      
                          let insetedBounds = UIEdgeInsetsInsetRect(collectionView.bounds, self.sectionInset)
                          self.itemSize = insetedBounds.size
                        }
                      
                        // Table: Possible cases of targetContentOffset calculation
                        // -------------------------
                        // start |          |
                        // near  | velocity | end
                        // page  |          | page
                        // -------------------------
                        //   0   | forward  |  1
                        //   0   | still    |  0
                        //   0   | backward |  0
                        //   1   | forward  |  1
                        //   1   | still    |  1
                        //   1   | backward |  0
                        // -------------------------
                        override func targetContentOffset( //swiftlint:disable:this cyclomatic_complexity
                          forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
                      
                          guard let collectionView = collectionView else { return proposedContentOffset }
                      
                          let pageWidth = itemSize.width + minimumLineSpacing
                          let currentPage: CGFloat = collectionView.contentOffset.x / pageWidth
                          let nearestPage: CGFloat = round(currentPage)
                          let isNearPreviousPage = nearestPage < currentPage
                      
                          var pageDiff: CGFloat = 0
                          let velocityThreshold: CGFloat = 0.5 // can customize this threshold
                          if isNearPreviousPage {
                            if velocity.x > velocityThreshold {
                              pageDiff = 1
                            }
                          } else {
                            if velocity.x < -velocityThreshold {
                              pageDiff = -1
                            }
                          }
                      
                          let x = (nearestPage + pageDiff) * pageWidth
                          let cappedX = max(0, x) // cap to avoid targeting beyond content
                          //print("x:", x, "velocity:", velocity)
                          return CGPoint(x: cappedX, y: proposedContentOffset.y)
                        }
                      
                      }
                      

                      【讨论】:

                        【解决方案23】:

                        您可以使用以下库:https://github.com/ink-spot/UPCarouselFlowLayout

                        它非常简单,您不需要像其他答案那样考虑细节。

                        【讨论】:

                          猜你喜欢
                          • 1970-01-01
                          • 2015-08-31
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          相关资源
                          最近更新 更多