Search code examples
iosswiftuicollectionviewuicollectionviewcelluicollectionviewlayout

Why collectionView cell centering works only in one direction?


I'm trying to centerelize my cells on horizontal scroll. I've written one method, but it works only when I scroll to right, on scroll on left it just scrolls, without stopping on the cell's center.

Can anyone help me to define this bug, please?

class CenterCellCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    if let cv = self.collectionView {
        let cvBounds = cv.bounds
        let halfWidth = cvBounds.size.width * 0.5;
        let proposedContentOffsetCenterX = proposedContentOffset.x + halfWidth;

        if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(cvBounds) {
            var candidateAttributes: UICollectionViewLayoutAttributes?
            for attributes in attributesForVisibleCells {

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

                if (attributes.center.x == 0) || (attributes.center.x > (cv.contentOffset.x + halfWidth) && velocity.x < 0) {
                    continue
                }

                // == First time in the loop == //
                guard let candAttrs = candidateAttributes else {
                    candidateAttributes = attributes
                    continue
                }

                let a = attributes.center.x - proposedContentOffsetCenterX
                let b = candAttrs.center.x - proposedContentOffsetCenterX

                if fabsf(Float(a)) < fabsf(Float(b)) {
                    candidateAttributes = attributes;
                }
            }

            if(proposedContentOffset.x == -(cv.contentInset.left)) {
                return proposedContentOffset
            }

             return CGPoint(x: floor(candidateAttributes!.center.x - halfWidth), y: proposedContentOffset.y)
        }
    } else {
        print("else")
    }

    // fallback
    return super.targetContentOffsetForProposedContentOffset(proposedContentOffset)
    }
}

And in my UIViewController:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    var insets = self.collectionView.contentInset
    let value = ((self.view.frame.size.width - ((CGRectGetWidth(collectionView.frame) - 35))) * 0.5)
    insets.left = value
    insets.right = value
    self.collectionView.contentInset = insets

    self.collectionView.decelerationRate = UIScrollViewDecelerationRateNormal
}

If you have any question - please ask me


Solution

  • I actually just did a slightly different implementation for another thread, but adjusted it to work for this questions. Try out the solution below :)

    /**
     *  Custom FlowLayout
     *  Tracks the currently visible index and updates the proposed content offset
     */
    class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
    
        // Tracks the currently visible index
        private var visibleIndex : Int = 0
    
        // The width offset threshold percentage from 0 - 1
        let thresholdOffsetPrecentage : CGFloat = 0.5
    
        // This is the flick velocity threshold
        let velocityThreshold : CGFloat = 0.4
    
        override init() {
            super.init()
            self.minimumInteritemSpacing = 0.0
            self.minimumLineSpacing = 0.0
            self.scrollDirection = .Horizontal
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    
            let leftThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) - 0.5))
            let rightThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) + 0.5))
    
            let currentHorizontalOffset = collectionView!.contentOffset.x
    
            // If you either traverse far enough in either direction,
            // or flicked the scrollview over the horizontal velocity in either direction,
            // adjust the visible index accordingly
    
            if currentHorizontalOffset < leftThreshold || velocity.x < -velocityThreshold {
                visibleIndex = max(0 , (visibleIndex - 1))
            } else if currentHorizontalOffset > rightThreshold || velocity.x > velocityThreshold {
                visibleIndex += 1
            }
    
            var _proposedContentOffset = proposedContentOffset
            _proposedContentOffset.x = CGFloat(collectionView!.bounds.width) * CGFloat(visibleIndex)
    
            return _proposedContentOffset
        }
    }