我知道有些人以前问过这个问题,但他们都是关于UITableViews
或UIScrollViews
,我无法得到接受的解决方案为我工作。我想要的是水平滚动UICollectionView
时的捕捉效果 - 就像在iOS AppStore中发生的那样。iOS 9+ 是我的目标版本,所以请在回答这个问题之前查看 UIKit 更改。
谢谢。
虽然最初我使用的是 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
根据 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()
}
}
对齐到最近的单元格,尊重滚动速度。
工作没有任何故障。
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)
}
}
对于这里的价值是我使用的一个简单的计算(在 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
}
}
}
在您需要的地方打电话。我称之为
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
}
这是我的实现
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
像魅力一样工作
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()
}
}
我尝试了@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)
}
}
如果你想要简单的本机行为,没有自定义:
collectionView.pagingEnabled = YES;
仅当集合视图布局项的大小仅为一个大小且UICollectionViewCell
的 clipToBounds
属性设置为 YES
时,此操作才能正常工作。
从 SO 帖子这里和文档这里得到答案
首先,您可以做的是通过将类设置为滚动视图委托来设置集合视图的滚动视图的委托您的类
MyViewController : SuperViewController<... ,UIScrollViewDelegate>
然后将视图控制器设置为委托
UIScrollView *scrollView = (UIScrollView *)super.self.collectionView;
scrollView.delegate = self;
或者在界面构建器中通过控制 + shift 单击集合视图,然后控制 + 拖动或右键单击拖动到视图控制器并选择委托。(您应该知道如何执行此操作(。那行不通。UICollectionView是UIScrollView的一个子类,因此您现在可以通过控制+shift单击在界面构建器中看到它
接下来实现委托方法- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
MyViewController.m
...
- (void)scrollViewDidEndDecelerating:(UIScrollView *)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];
}
}
这是来自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);
}
我之所以提出这个问题,是为了给人一种原生的感觉,这是我从马克接受的答案中得到的......我把它放在集合视图的视图控制器中。
collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
通过在 uicollectionview 上的属性检查器上设置"启用分页"来解决此问题。
对我来说,当单元格的宽度与 uicollectionview 的宽度相同时,就会发生这种情况。
不涉及编码。
我刚刚找到了我认为是这个问题的最佳解决方案:
首先将目标添加到集合视图的现有 gestureRecognizer:
[self.collectionView.panGestureRecognizer addTarget:self action:@selector(onPan:)];
让选择器指向将 UIPanGestureRecognizer 作为参数的方法:
- (void)onPan:(UIPanGestureRecognizer *)recognizer {};
然后,在此方法中,强制集合视图在平移手势结束时滚动到相应的单元格。我通过从集合视图中获取可见项目并根据平移方向确定要滚动到的项目来做到这一点。
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];
}
请记住,在我的情况下,一次只能看到两个项目。我相信这种方法可以适用于更多项目。
此解决方案可提供更好、更流畅的动画。
斯威夫特 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
附言:确保类采用UICollectionViewDataSource
协议
这是一个 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。简单。对于固定项目大小。水平流动方向。
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)
}
}
就我而言,我需要单个单元格来捕捉UICollectionView
的中心(就像应用程序中Photoshop Camera
滑块一样(。
我能够使用UIScrollViewDelegate
方法和UICollectionViewFlowLayout
来实现它。
即使您的单元格具有不同的宽度并且布局在项目之间具有间距,此解决方案也有效。
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let targetXOffset = targetContentOffset.pointee.x + collectionView.frame.width / 2
var xOffset = CGFloat.greatestFiniteMagnitude
var targetIndex = 0
for section in 0..<collectionView.numberOfSections {
for item in 0..<collectionView.numberOfItems(inSection: section) {
let indexPath = IndexPath(item: item, section: section)
if let cellAttributes = layout.layoutAttributesForItem(at: indexPath) {
let distance = abs(cellAttributes.frame.midX - targetXOffset)
if distance < xOffset {
xOffset = distance
targetIndex = item
}
}
}
}
let indexPath = IndexPath(item: targetIndex, section: 0)
if let attributes = layout.layoutAttributesForItem(at: indexPath) {
targetContentOffset.pointee = CGPoint(x: attributes.frame.midX - collectionView.frame.width / 2,
y: targetContentOffset.pointee.y)
// Most likely, you want the cell to be selected
// collectionView.selectItem(at: indexPath, animated: true, scrollPosition: [])
}
}