水平滚动 UICollectionView 时对齐到单元格中心

我知道以前有人问过这个问题,但是他们都是关于 UITableViews或者 UIScrollViews的,我不能得到公认的解决方案来为我工作。我想要的是在水平滚动我的 UICollectionView时的快门效果——就像在 iOSAppStore 中发生的一样。IOS9 + 是我的目标版本,所以在回答这个问题之前请先看看 UIKit 的变化。

谢谢。

54698 次浏览

给你后台和 给你后台得到了答案

首先,您可以通过将类设置为 scrollview 委托来设置集合视图的 scrollview 委托

MyViewController : SuperViewController<... ,UIScrollViewDelegate>

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

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

或者在 Interface Builder 中通过控制 + shift 单击您的集合视图,然后控制 + 拖动或右键单击拖动到您的视图控制器,并选择委托。(你应该知道怎么做)。没用的。UiCollectionView 是 uiscrollView 的一个子类,所以你现在可以通过控制 + shift 单击在 Interface Builder 中看到它

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

MyViewController.m


...


- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{


}

文件指出:

参数

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

当滚动时,滚动视图调用此方法 运动停止。 UIScrollView 的减速特性 控制减速。

可用性 可在 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];
}
}

这里值得一提的是我(迅速地)使用的一个简单计算:

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)
}
}

需要在 UIScrollView 中实现代理:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
scrollToNearestVisibleCollectionViewCell()
}


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

这个解决方案提供了一个更好和更平滑的动画。

Swift 3

要使第一项和最后一项居中,请添加 insets:

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

另外,确保班级采用 UICollectionViewDataSource协议

我刚刚找到了我认为解决这个问题的最好办法:

首先将一个目标添加到 CollectionView 已经存在的手势识别器:

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

让选择器指向一个以 UIPanGestureIdentity 作为参数的方法:

- (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];
}

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

虽然最初我使用的是 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

这是一个 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)
}

这是我的实现

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;

这只有在集合视图布局项的大小都只有一个大小并且 UICollectionViewCellclipToBounds属性设置为 YES时才能正常工作。

根据 梅特的回答和 克里斯 · 楚特的评论,

这里有一个 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()
}
}

按照滚动速度对齐最近的单元格。

没有任何故障。

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)
}
}

这来自于2012年 WWDC 的 Objective-C 解决方案视频。

-(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);
}

我得到这个问题的原因是为了捕捉一个本地的感觉,我得到了从马克的接受的答案... 这是我放在 CollectionView 的视图控制器。

collectionView.decelerationRate = UIScrollViewDecelerationRateFast;

迅捷4.2。简单。为固定项目大小。水平流动方向。

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)
}


}

我尝试了“ 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)
}

}

我一直在通过在 uicollectionview 的属性检查器上设置“ Paging Enable”来解决这个问题。

对于我来说,当单元格的宽度与 uicollectionview 的宽度相同时,就会发生这种情况。

不需要编码。