我知道有些人以前曾问过这个问题,但是他们全都UITableViews
或者或者UIScrollViews
我无法获得公认的解决方案来为我工作。我想要的是UICollectionView
水平滚动时的捕捉效果-就像iOS AppStore中发生的情况一样。iOS 9+是我的目标版本,因此在回答此问题之前,请先查看UIKit的更改。
谢谢。
Answers:
当我最初使用Objective-C时,自那以后我切换到了Swift,最初接受的答案就不够了。
我最终创建了一个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)
}
}
对于当前布局子类,最自然的感觉减速,请确保设置以下内容:
collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
sectionInset.left
设定条件的人,将退货声明替换为:return CGPoint(x: proposedContentOffset.x + offsetAdjustment - sectionInset.left, y: proposedContentOffset.y)
layoutAttributesArray?.forEach({ (layoutAttributes) in let itemOffset = layoutAttributes.frame.origin.x let itemWidth = Float(layoutAttributes.frame.width) let direction: Float = velocity.x > 0 ? 1 : -1 if fabsf(Float(itemOffset - horizontalOffset)) < fabsf(Float(offsetAdjustment)) + itemWidth * direction { offsetAdjustment = itemOffset - horizontalOffset } })
根据Mete的回答和Chris Chute的评论,
这是一个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)
}
}
}
您需要UIScrollViewDelegate
为您的集合视图实现协议,然后添加以下两种方法:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.collectionView.scrollToNearestVisibleCollectionViewCell()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.collectionView.scrollToNearestVisibleCollectionViewCell()
}
}
if closestCellIndex != -1 { UIView.animate(withDuration: 0.1) { let toX = (cellWidth + cellHorizontalSpacing) * CGFloat(closestCellIndex) scrollView.contentOffset = CGPoint(x: toX, y: 0) scrollView.layoutIfNeeded() } }
Paging Enabled
在要为其实现此扩展的collectionView上禁用,但该解决方案也对我有用。启用分页后,行为是不可预测的。我相信这是因为自动分页功能会干扰手动计算。
遵循滚动速度,捕捉到最近的单元格。
正常工作。
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)
}
}
contentInset
,请确保使用var添加或删除它nearestPageOffset
。
itemSize
。此方法只能itemSize
直接设置为layout
,但是如果itemSize
通过设置UICollectionViewDelegateFlowLayout
,则此新设置的值将无法识别
对于这里值得的是,我使用了一个简单的计算方法(迅速):
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()
}
}
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
在某个时候进行设置,则您将更接近App Store的“感觉” 。我还要补充一句,FLT_MAX
将第4行更改为Float.greatestFiniteMagnitude
,以避免Xcode警告。
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)
}
}
像这样实现滚动视图委托
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;
仅当集合视图布局项目的大小全部为一种大小且UICollectionViewCell
的clipToBounds
属性设置为时,此方法才能正常工作YES
。
minimumLineSpacing
布局设置为0
我同时尝试了@Mark Bourke和@mrcrowley解决方案,但它们给出的结果几乎相同,并且具有不良的粘滞效果。
我设法通过考虑解决问题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)
}
}
首先,您可以做的是通过将类设为滚动视图委托来设置集合视图的scrollview委托类
MyViewController : SuperViewController<... ,UIScrollViewDelegate>
然后将您的视图控制器设置为委托
UIScrollView *scrollView = (UIScrollView *)super.self.collectionView;
scrollView.delegate = self;
或在界面构建器中通过以下方式进行操作:Ctrl + Shift单击集合视图,然后按住Control拖动或右键单击拖动到视图控制器并选择委托。(您应该知道如何执行此操作)。那不行 UICollectionView是UIScrollView的子类,因此您现在可以通过按Control + Shift单击来在界面生成器中看到它
接下来实现委托方法 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
MyViewController.m
...
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
}
该文档指出:
参量
scrollView | 滚动视图对象正在使内容视图的滚动减速。
讨论当滚动运动停止时,滚动视图将调用此方法。UIScrollView的decelerating属性控制减速度。
可用性在iOS 2.0和更高版本中可用。
然后在该方法内部检查停止滚动时哪个单元格最靠近scrollview的中心
- (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];
}
}
我刚刚发现我认为是解决此问题的最佳方法:
首先,将一个目标添加到collectionView的已经存在的actionRecognizer中:
[self.collectionView.panGestureRecognizer addTarget:self action:@selector(onPan:)];
让选择器指向以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];
}
请记住,就我而言,一次只能看到两个项目。我敢肯定,这种方法可以适应更多的项目。
摘自2012年WWDC视频中的Objective-C解决方案。我将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);
}
我之所以要问这个问题,是因为它具有本机感,这是我从Mark的可接受答案中得到的……这是我放入collectionView的视图控制器中的结果。
collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
该解决方案提供了更好,更流畅的动画。
迅捷3
要使第一个和最后一个项目居中,请添加插图:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, cellWidth/2, 0, cellWidth/2)
}
然后使用targetContentOffset
中的scrollViewWillEndDragging
方法更改结束位置。
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
计算的是不一样的,fe没有minimumInteritemSpacing
,因此请相应地调整。您也可以玩300
在velocity
PS确保课程采用UICollectionViewDataSource
协议
collectionView(mainCollectionView, numberOfItemsInSection:0)
如果您的对象采用,则只能以这种方式使用该方法UICollectionViewDataSource
。为什么cellWidth
要从中减去scrollView.contentSize.width
,总宽度不总是总是scrollView.contentSize.width
吗?
cellWidth
需要减去来补偿它以使其居中。也许在您的情况下,totalContentWidth
计算方式有所不同。
这是一个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)
}
斯威夫特4.2。简单。对于固定的itemSize。水平流向。
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)
}
}
jumping to previous cell
当您向左滚动并立即单击时(滚动动画仍在发生时)。您将看到滚动被“取消”并跳到上一个单元格。