[英]Snap to center of a cell when scrolling UICollectionView horizontally
I know some people have asked this question before but they were all about UITableViews
or UIScrollViews
and I couldn't get the accepted solution to work for me.我知道有些人以前问过这个问题,但他们都是关于UITableViews
或UIScrollViews
的,我无法让接受的解决方案为我工作。 What I would like is the snapping effect when scrolling through my UICollectionView
horizontally - much like what happens in the iOS AppStore.我想要的是水平滚动我的UICollectionView
时的捕捉效果——就像在 iOS AppStore 中发生的一样。 iOS 9+ is my target build so please look at the UIKit changes before answering this. iOS 9+ 是我的目标版本,所以在回答这个问题之前请先查看 UIKit 的变化。
Thanks.谢谢。
While originally I was using Objective-C, I since switched so Swift and the original accepted answer did not suffice.虽然最初我使用的是 Objective-C,但后来我切换为 Swift 并且最初接受的答案还不够。
I ended up creating a UICollectionViewLayout
subclass which provides the best (imo) experience as opposed to the other functions which alter content offset or something similar when the user has stopped scrolling.我最终创建了一个UICollectionViewLayout
子类,它提供了最好的(imo)体验,而不是在用户停止滚动时改变内容偏移或类似内容的其他功能。
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)
}
}
For the most native feeling deceleration with the current layout subclass, make sure to set the following:对于当前布局子类最原生的感觉减速,请确保设置以下内容:
collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
Based on answer from Mete and comment from Chris Chute ,根据Mete的回答和Chris Chute的评论,
Here's a Swift 4 extension that will do just what OP wants.这是一个 Swift 4 扩展,它将满足 OP 的需求。 It's tested on single row and double row nested collection views and it works just fine.它在单行和双行嵌套集合视图上进行了测试,并且运行良好。
extension UICollectionView {
func scrollToNearestVisibleCollectionViewCell() {
self.decelerationRate = UIScrollViewDecelerationRateFast
let visibleCenterPositionOfScrollView = Float(self.contentOffset.x + (self.bounds.size.width / 2))
var closestCellIndex = -1
var closestDistance: Float = .greatestFiniteMagnitude
for i in 0..<self.visibleCells.count {
let cell = self.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 = self.indexPath(for: cell)!.row
}
}
if closestCellIndex != -1 {
self.scrollToItem(at: IndexPath(row: closestCellIndex, section: 0), at: .centeredHorizontally, animated: true)
}
}
}
You need to implement UIScrollViewDelegate
protocol for your collection view and then add these two methods:您需要为您的集合视图实现UIScrollViewDelegate
协议,然后添加这两个方法:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.collectionView.scrollToNearestVisibleCollectionViewCell()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.collectionView.scrollToNearestVisibleCollectionViewCell()
}
}
Snap to the nearest cell, respecting scroll velocity.捕捉到最近的单元格,尊重滚动速度。
Works without any glitches.工作没有任何故障。
import UIKit
final class SnapCenterLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) }
let parent = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
let itemSpace = itemSize.width + minimumInteritemSpacing
var currentItemIdx = round(collectionView.contentOffset.x / itemSpace)
// Skip to the next cell, if there is residual scrolling velocity left.
// This helps to prevent glitches
let vX = velocity.x
if vX > 0 {
currentItemIdx += 1
} else if vX < 0 {
currentItemIdx -= 1
}
let nearestPageOffset = currentItemIdx * itemSpace
return CGPoint(x: nearestPageOffset,
y: parent.y)
}
}
For what it is worth here is a simple calculation that I use (in swift):这里值得的是我使用的一个简单计算(快速):
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
}
}
}
Call where you need it.在需要的地方打电话。 I call it in我把它叫进来
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
snapToNearestCell(scrollView)
}
And和
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
snapToNearestCell(scrollView)
}
Where collectionViewFlowLayout could come from: collectionViewFlowLayout 可能来自哪里:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Set up collection view
collectionViewFlowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
}
SWIFT 3 version of @Iowa15 reply 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)
}
}
Needs to implement in UIScrollViewDelegate:需要在 UIScrollViewDelegate 中实现:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
scrollToNearestVisibleCollectionViewCell()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
scrollToNearestVisibleCollectionViewCell()
}
}
func snapToNearestCell(scrollView: UIScrollView) {
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)
}
}
Implement your scroll view delegates like this像这样实现您的滚动视图委托
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
self.snapToNearestCell(scrollView: scrollView)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.snapToNearestCell(scrollView: scrollView)
}
Also, for better snapping此外,为了更好地捕捉
self.cvCollectionView.decelerationRate = UIScrollViewDecelerationRateFast
Works like a charm奇迹般有效
I tried both @Mark Bourke and @mrcrowley solutions but they give the pretty same results with unwanted sticky effects.我尝试了@Mark Bourke 和@mrcrowley 两种解决方案,但它们给出了几乎相同的结果,但具有不需要的粘性效果。
I managed to solve the problem by taking into account the velocity
.我设法通过考虑velocity
来解决问题。 Here is the full code.这是完整的代码。
final class BetterSnappingLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
}
var offsetAdjusment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + (collectionView.bounds.width / 2)
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 itemHorizontalCenter = layoutAttributes.center.x
if abs(itemHorizontalCenter - horizontalCenter) < abs(offsetAdjusment) {
if abs(velocity.x) < 0.3 { // minimum velocityX to trigger the snapping effect
offsetAdjusment = itemHorizontalCenter - horizontalCenter
} else if velocity.x > 0 {
offsetAdjusment = itemHorizontalCenter - horizontalCenter + layoutAttributes.bounds.width
} else { // velocity.x < 0
offsetAdjusment = itemHorizontalCenter - horizontalCenter - layoutAttributes.bounds.width
}
}
})
return CGPoint(x: proposedContentOffset.x + offsetAdjusment, y: proposedContentOffset.y)
}
} }
If you want simple native behavior, without customization:如果您想要简单的本机行为,无需自定义:
collectionView.pagingEnabled = YES;
This only works properly when the size of the collection view layout items are all one size only and the UICollectionViewCell
's clipToBounds
property is set to YES
.这仅在集合视图布局项的大小均为一种大小且UICollectionViewCell
的clipToBounds
属性设置为YES
时才能正常工作。
Got an answer from SO post here and docs here从这里的 SO 帖子和这里的文档中得到了答案
First What you can do is set your collection view's scrollview's delegate your class by making your class a scrollview delegate首先,您可以做的是通过使您的类成为滚动视图委托来设置您的集合视图的滚动视图的委托您的类
MyViewController : SuperViewController<... ,UIScrollViewDelegate>
Then make set your view controller as the delegate然后将您的视图控制器设置为委托
UIScrollView *scrollView = (UIScrollView *)super.self.collectionView;
scrollView.delegate = self;
Or do it in the interface builder by control + shift clicking on your collection view and then control + drag or right click drag to your view controller and select delegate.或者在界面构建器中通过 control + shift 单击您的集合视图然后控制 + 拖动或右键单击拖动到您的视图控制器并选择委托。 (You should know how to do this). (你应该知道如何做到这一点)。 That doesn't work.那是行不通的。 UICollectionView is a subclass of UIScrollView so you will now be able to see it in the interface builder by control + shift clicking UICollectionView 是 UIScrollView 的子类,因此您现在可以通过 control + shift 单击在界面构建器中看到它
Next implement the delegate method - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
接下来实现委托方法- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
MyViewController.m
...
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
}
The docs state that:文档指出:
Parameters参数
scrollView |滚动视图 | The scroll-view object that is decelerating the scrolling of the content view.使内容视图的滚动减速的滚动视图对象。
Discussion The scroll view calls this method when the scrolling movement comes to a halt.讨论当滚动停止时,滚动视图调用这个方法。 The decelerating property of UIScrollView controls deceleration. UIScrollView 的 decelerating 属性控制减速。
Availability Available in iOS 2.0 and later.可用性适用于 iOS 2.0 及更高版本。
Then inside of that method check which cell was closest to the center of the scrollview when it stopped scrolling然后在该方法内部检查哪个单元格在停止滚动时最接近滚动视图的中心
- (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.
}
A modification of the above answer which you can also try:您也可以尝试修改上述答案:
-(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];
}
}
This solution gives a better and smoother animation.此解决方案提供了更好、更流畅的动画。
Swift 3斯威夫特 3
To get the first and last item to center add insets:要使第一个和最后一个项目居中添加插图:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, cellWidth/2, 0, cellWidth/2)
}
Then use the targetContentOffset
in the scrollViewWillEndDragging
method to alter the ending position.然后使用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
}
Maybe in your case the totalContentWidth
is calculated differently, fe without a minimumInteritemSpacing
, so adjust that accordingly.也许在您的情况下, totalContentWidth
的计算方式不同, fe 没有minimumInteritemSpacing
,因此请相应地进行调整。 Also you can play around with the 300
used in the velocity
您也可以使用velocity
中使用的300
PS Make sure the class adopts the UICollectionViewDataSource
protocol PS 确保类采用UICollectionViewDataSource
协议
I just found what I think is the best possible solution to this problem:我刚刚找到了我认为是解决这个问题的最佳方法:
First add a target to the collectionView's already existing gestureRecognizer:首先在collectionView已经存在的gestureRecognizer中添加一个target:
[self.collectionView.panGestureRecognizer addTarget:self action:@selector(onPan:)];
Have the selector point to a method which takes a UIPanGestureRecognizer as a parameter:让选择器指向一个将 UIPanGestureRecognizer 作为参数的方法:
- (void)onPan:(UIPanGestureRecognizer *)recognizer {};
Then in this method, force the collectionView to scroll to the appropriate cell when the pan gesture has ended.然后在此方法中,强制 collectionView 在平移手势结束时滚动到相应的单元格。 I did this by getting the visible items from the collection view and determining which item I want to scroll to depending on the direction of the pan.我通过从集合视图中获取可见项目并根据平移的方向确定要滚动到哪个项目来做到这一点。
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];
}
Keep in mind that in my case only two items can be visible at a time.请记住,在我的情况下,一次只能看到两个项目。 I'm sure this method can be adapted for more items however.我确信这种方法可以适用于更多项目。
This from a 2012 WWDC video for an Objective-C solution.这是来自 2012 年 WWDC 视频的 Objective-C 解决方案。 I subclassed UICollectionViewFlowLayout and added the following.我将 UICollectionViewFlowLayout 子类化并添加了以下内容。
-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
CGFloat offsetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2);
CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray *array = [super layoutAttributesForElementsInRect:targetRect];
for (UICollectionViewLayoutAttributes *layoutAttributes in array)
{
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment))
{
offsetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
And the reason I got to this question was for the snapping with a native feel, which I got from Mark's accepted answer... this I put in the collectionView's view controller.我提出这个问题的原因是为了以一种原生的感觉捕捉,这是我从 Mark 接受的答案中得到的……我把它放在 collectionView 的视图控制器中。
collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
I've been solving this issue by setting 'Paging Enabled' on the attributes inspector on the uicollectionview.我一直在通过在 uicollectionview 的属性检查器上设置“启用分页”来解决这个问题。
For me this happens when the width of the cell is the same as the width of the uicollectionview.对我来说,当单元格的宽度与 uicollectionview 的宽度相同时,就会发生这种情况。
No coding involved.不涉及编码。
Here is a Swift 3.0 version, which should work for both horizontal and vertical directions based on Mark's suggestion above:这是一个 Swift 3.0 版本,根据上面 Mark 的建议,它应该适用于水平和垂直方向:
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)
}
Swift 4.2.斯威夫特 4.2。 Simple.简单的。 For fixed itemSize.对于固定的 itemSize。 Horizontal flow direction.水平流向。
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
let floatingPage = targetContentOffset.pointee.x/scrollView.bounds.width
let rule: FloatingPointRoundingRule = velocity.x > 0 ? .up : .down
let page = CGFloat(Int(floatingPage.rounded(rule)))
targetContentOffset.pointee.x = page*(layout.itemSize.width + layout.minimumLineSpacing)
}
}
尝试以下解决方案: CarouselWithPaging
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.