Paging UICollectionView by cells, not screen

前端 未结 22 1941
予麋鹿
予麋鹿 2020-12-04 05:00

I have UICollectionView with horizontal scrolling and there are always 2 cells side-by-side per the entire screen. I need the scrolling to stop at the begining

相关标签:
22条回答
  • 2020-12-04 05:35

    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.

    Cheers All :)

    0 讨论(0)
  • 2020-12-04 05:36

    modify Romulo BM answer for velocity listening

    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 )
    }
    
    0 讨论(0)
  • 2020-12-04 05:36

    Here is my way to do it by using a UICollectionViewFlowLayout to override the targetContentOffset:

    (Although in the end, I end up not using this and use UIPageViewController instead.)

    /**
     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)
      }
    
    }
    
    0 讨论(0)
  • 2020-12-04 05:37

    Also you can create fake scroll view to handle scrolling.

    Horizontal or Vertical

    // === 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)
    }
    
    0 讨论(0)
  • 2020-12-04 05:37

    The original answer of Олень Безрогий had an issue, so on the last cell collection view was scrolling to the beginning

    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 )
    }
    
    0 讨论(0)
  • 2020-12-04 05:38

    Here is the optimised solution in Swift5, including handling the wrong indexPath. - Michael Lin Liu

    • Step1. Get the indexPath of the current cell.
    • Step2. Detect the velocity when scroll.
    • Step3. Increase the indexPath's row when the velocity is increased.
    • Step4. Tell the collection view to scroll to the next item
        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 )
     }
    
    0 讨论(0)
提交回复
热议问题