ios 水平滚动 UICollectionView 时对齐单元格的中心
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/33855945/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Snap to center of a cell when scrolling UICollectionView horizontally
提问by Mark Bourke
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. What I would like is the snapping effect when scrolling through my UICollectionView
horizontally - much like what happens in the iOS AppStore. iOS 9+ is my target build so please look at the UIKit changes before answering this.
我知道有些人之前已经问过这个问题,但他们都是关于UITableViews
或的UIScrollViews
,我无法获得为我工作的公认解决方案。我想要的是UICollectionView
水平滚动时的捕捉效果- 就像在 iOS AppStore 中发生的一样。iOS 9+ 是我的目标版本,所以在回答这个问题之前请先看看 UIKit 的变化。
Thanks.
谢谢。
回答by Mark Bourke
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
collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
回答by Vahid Amiri
Based on answer from Meteand comment from Chris Chute,
根据Mete 的回答和Chris Chute 的评论,
Here's a Swift 4 extension that will do just what OP wants. It's tested on single row and double row nested collection views and it works just fine.
这是一个 Swift 4 扩展,它可以满足 OP 的要求。它在单行和双行嵌套集合视图上进行了测试,效果很好。
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()
}
}
回答by Nam
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
}
回答by Richard Topchii
Snap to the nearest cell, respecting scroll velocity.
捕捉到最近的单元格,尊重滚动速度。
Works without any glitches.
工作没有任何故障。
import UIKit
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)
}
}
回答by Mette
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()
}
}
回答by Sourav Chandra
Here is my implementation
这是我的实现
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 delegateslike 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
奇迹般有效
回答by NSDestr0yer
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
。
回答by Nhon Nguyen
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
. Here is the full code.
我设法通过考虑velocity
. 这是完整的代码。
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)
}
}
}
回答by Minebomber
Got an answer from SO post hereand docs here
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. (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
或者在界面构建器中通过 control + shift 单击您的集合视图,然后 control + drag 或右键单击拖动到您的视图控制器并选择委托。(你应该知道如何做到这一点)。那行不通。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.
DiscussionThe scroll view calls this method when the scrolling movement comes to a halt. The decelerating property of UIScrollView controls deceleration.
AvailabilityAvailable in iOS 2.0 and later.
参数
滚动视图 | 正在减速内容视图滚动的滚动视图对象。
讨论当滚动移动停止时,滚动视图调用此方法。UIScrollView 的 decelerating 属性控制减速。
可用性在 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.
}
回答by Iowa15
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];
}
}