UICollectionView在水平滚动时捕捉到单元格

我知道以前有人问过这个问题,但是他们都是关于UITableViews或者UIScrollViews ,我无法得到可以接受的解决scheme。 我想要的是水平滚动我的UICollectionView时的捕捉效果 – 很像在iOS AppStore中发生的情况。 iOS 9+是我的目标构build,所以请在回答之前查看UIKit的变化。

谢谢。

虽然最初我使用的是Objective-C,但是由于Swift和原来接受的答案都不够用,

我最终创build了一个UICollectionViewLayout子类,它提供了最好的(imo)体验,而不是其他的function,当用户停止滚动时会改变内容偏移量或类似的东西。

 class SnappingCollectionViewLayout: UICollectionViewFlowLayout { override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) } var offsetAdjustment = CGFloat.greatestFiniteMagnitude let horizontalOffset = proposedContentOffset.x + collectionView.contentInset.left let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height) let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect) layoutAttributesArray?.forEach({ (layoutAttributes) in let itemOffset = layoutAttributes.frame.origin.x if fabsf(Float(itemOffset - horizontalOffset)) < fabsf(Float(offsetAdjustment)) { offsetAdjustment = itemOffset - horizontalOffset } }) return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y) } } 

对于当前布局子类最本地的感觉减速,请确保设置以下内容:

collectionView?.decelerationRate = UIScrollViewDecelerationRateFast

在这里值得一个简单的计算,我使用(在迅速):

 func snapToNearestCell(_ collectionView: UICollectionView) { for i in 0..<collectionView.numberOfItems(inSection: 0) { let itemWithSpaceWidth = collectionViewFlowLayout.itemSize.width + collectionViewFlowLayout.minimumLineSpacing let itemWidth = collectionViewFlowLayout.itemSize.width if collectionView.contentOffset.x <= CGFloat(i) * itemWithSpaceWidth + itemWidth / 2 { let indexPath = IndexPath(item: i, section: 0) collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) break } } } 

打电话给你需要的地方。 我打电话进来

 func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) { snapToNearestCell(scrollView) } 

 func scrollViewDidEndDecelerating(scrollView: UIScrollView) { snapToNearestCell(scrollView) } 

collectionViewFlowLayout可以来自哪里:

 override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // Set up collection view collectionViewFlowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout } 

SWIFT 3版@ Iowa15回复

 func scrollToNearestVisibleCollectionViewCell() { let visibleCenterPositionOfScrollView = Float(collectionView.contentOffset.x + (self.collectionView!.bounds.size.width / 2)) var closestCellIndex = -1 var closestDistance: Float = .greatestFiniteMagnitude for i in 0..<collectionView.visibleCells.count { let cell = collectionView.visibleCells[i] let cellWidth = cell.bounds.size.width let cellCenter = Float(cell.frame.origin.x + cellWidth / 2) // Now calculate closest cell let distance: Float = fabsf(visibleCenterPositionOfScrollView - cellCenter) if distance < closestDistance { closestDistance = distance closestCellIndex = collectionView.indexPath(for: cell)!.row } } if closestCellIndex != -1 { self.collectionView!.scrollToItem(at: IndexPath(row: closestCellIndex, section: 0), at: .centeredHorizontally, animated: true) } } 

需要在UIScrollViewDelegate中实现:

 func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { scrollToNearestVisibleCollectionViewCell() } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { scrollToNearestVisibleCollectionViewCell() } } 

得到了SO贴在这里和文档在这里的答案

首先你可以做的是设置你的集合视图的scrollview委托你的类,通过使你的类为scrollview委托

 MyViewController : SuperViewController<... ,UIScrollViewDelegate> 

然后将视图控制器设置为委托

 UIScrollView *scrollView = (UIScrollView *)super.self.collectionView; scrollView.delegate = self; 

或者在界面生成器中通过控制+移动单击您的集合视图,然后控制拖动或右键单击拖动到您的视图控制器并select委托。 (你应该知道如何做到这一点)。 这是行不通的。 UICollectionView是UIScrollView的一个子类,所以你现在可以在界面生成器中通过control + shift点击

接下来实现委托方法- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

 MyViewController.m ... - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { } 

该文件指出:

参数

scrollView | 正在减速滚动内容视图的滚动视图对象。

讨论滚动视图在滚动运动停止时调用此方法。 UIScrollView的减速属性控制减速。

Availability在iOS 2.0及更高版本可用。

然后在该方法中,当停止滚动时,检查哪个单元格离滚动视图的中心最近

 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { //NSLog(@"%f", truncf(scrollView.contentOffset.x + (self.pictureCollectionView.bounds.size.width / 2))); float visibleCenterPositionOfScrollView = scrollView.contentOffset.x + (self.pictureCollectionView.bounds.size.width / 2); //NSLog(@"%f", truncf(visibleCenterPositionOfScrollView / imageArray.count)); NSInteger closestCellIndex; for (id item in imageArray) { // equation to use to figure out closest cell // abs(visibleCenter - cellCenterX) <= (cellWidth + cellSpacing/2) // Get cell width (and cell too) UICollectionViewCell *cell = (UICollectionViewCell *)[self collectionView:self.pictureCollectionView cellForItemAtIndexPath:[NSIndexPath indexPathWithIndex:[imageArray indexOfObject:item]]]; float cellWidth = cell.bounds.size.width; float cellCenter = cell.frame.origin.x + cellWidth / 2; float cellSpacing = [self collectionView:self.pictureCollectionView layout:self.pictureCollectionView.collectionViewLayout minimumInteritemSpacingForSectionAtIndex:[imageArray indexOfObject:item]]; // Now calculate closest cell if (fabsf(visibleCenterPositionOfScrollView - cellCenter) <= (cellWidth + (cellSpacing / 2))) { closestCellIndex = [imageArray indexOfObject:item]; break; } } if (closestCellIndex != nil) { [self.pictureCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathWithIndex:closestCellIndex] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES]; // This code is untested. Might not work. } 

上面的答案的修改,你也可以尝试:

 -(void)scrollToNearestVisibleCollectionViewCell { float visibleCenterPositionOfScrollView = _collectionView.contentOffset.x + (self.collectionView.bounds.size.width / 2); NSInteger closestCellIndex = -1; float closestDistance = FLT_MAX; for (int i = 0; i < _collectionView.visibleCells.count; i++) { UICollectionViewCell *cell = _collectionView.visibleCells[i]; float cellWidth = cell.bounds.size.width; float cellCenter = cell.frame.origin.x + cellWidth / 2; // Now calculate closest cell float distance = fabsf(visibleCenterPositionOfScrollView - cellCenter); if (distance < closestDistance) { closestDistance = distance; closestCellIndex = [_collectionView indexPathForCell:cell].row; } } if (closestCellIndex != -1) { [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:closestCellIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; } } 

这个解决scheme提供了一个更好,更stream畅的animation。

Swift 3

为了让第一个和最后一个项目居中添加插入:

 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { return UIEdgeInsetsMake(0, cellWidth/2, 0, cellWidth/2) } 

然后使用scrollViewWillEndDragging方法中的targetContentOffset来改变结束位置。

 func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let numOfItems = collectionView(mainCollectionView, numberOfItemsInSection:0) let totalContentWidth = scrollView.contentSize.width + mainCollectionViewFlowLayout.minimumInteritemSpacing - cellWidth let stopOver = totalContentWidth / CGFloat(numOfItems) var targetX = round((scrollView.contentOffset.x + (velocity.x * 300)) / stopOver) * stopOver targetX = max(0, min(targetX, scrollView.contentSize.width - scrollView.frame.width)) targetContentOffset.pointee.x = targetX } 

也许在你的情况下, totalContentWidth计算方式不同,因为没有minimumInteritemSpacing ,所以要相应地调整。 你也可以玩在velocity 300使用

我刚刚发现我认为是解决此问题的最佳解决scheme:

首先将一个目标添加到collectionView已经存在的gestureRecognizer中:

 [self.collectionView.panGestureRecognizer addTarget:self action:@selector(onPan:)]; 

让select器指向以UIPanGestureRecognizer作为参数的方法:

 - (void)onPan:(UIPanGestureRecognizer *)recognizer {}; 

然后在此方法中,当平移手势结束时强制collectionView滚动到适当的单元格。 我通过从收集视图中获取可见项目并根据锅的方向确定要滚动的项目来完成此操作。

 if (recognizer.state == UIGestureRecognizerStateEnded) { // Get the visible items NSArray<NSIndexPath *> *indexes = [self.collectionView indexPathsForVisibleItems]; int index = 0; if ([(UIPanGestureRecognizer *)recognizer velocityInView:self.view].x > 0) { // Return the smallest index if the user is swiping right for (int i = index;i < indexes.count;i++) { if (indexes[i].row < indexes[index].row) { index = i; } } } else { // Return the biggest index if the user is swiping left for (int i = index;i < indexes.count;i++) { if (indexes[i].row > indexes[index].row) { index = i; } } } // Scroll to the selected item [self.collectionView scrollToItemAtIndexPath:indexes[index] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES]; } 

请记住,在我的情况下,一次只能看到两个项目。 我相信这种方法可以适应更多的项目。

这里是一个Swift3.0版本,根据Mark的build议,这个版本应该适用于水平和垂直两个方向:

  override func targetContentOffset( forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint ) -> CGPoint { guard let collectionView = collectionView else { return super.targetContentOffset( forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity ) } let realOffset = CGPoint( x: proposedContentOffset.x + collectionView.contentInset.left, y: proposedContentOffset.y + collectionView.contentInset.top ) let targetRect = CGRect(origin: proposedContentOffset, size: collectionView.bounds.size) var offset = (scrollDirection == .horizontal) ? CGPoint(x: CGFloat.greatestFiniteMagnitude, y:0.0) : CGPoint(x:0.0, y:CGFloat.greatestFiniteMagnitude) offset = self.layoutAttributesForElements(in: targetRect)?.reduce(offset) { (offset, attr) in let itemOffset = attr.frame.origin return CGPoint( x: abs(itemOffset.x - realOffset.x) < abs(offset.x) ? itemOffset.x - realOffset.x : offset.x, y: abs(itemOffset.y - realOffset.y) < abs(offset.y) ? itemOffset.y - realOffset.y : offset.y ) } ?? .zero return CGPoint(x: proposedContentOffset.x + offset.x, y: proposedContentOffset.y + offset.y) } 

这是我的实现

 let middlePoint = Int(scrollView.contentOffset.x + UIScreen.main.bounds.width / 2) if let indexPath = self.cvCollectionView.indexPathForItem(at: CGPoint(x: middlePoint, y: 0)) { self.cvCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) } 

像这样实现你的滚动视图委托

 func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { self.snapToNearestCell(scrollView: scrollView) } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { self.snapToNearestCell(scrollView: scrollView) } 

另外,为了更好的捕捉

 self.cvCollectionView.decelerationRate = UIScrollViewDecelerationRateFast 

奇迹般有效

如果你想要简单的本地行为,不用定制:

 collectionView.pagingEnabled = YES; 

这只能在集合视图布局项目的大小仅为一个大小且UICollectionViewCellclipToBounds属性设置为YES