ios targetContentOffsetForProposedContentOffset:withScrollingVelocity 没有子类化 UICollectionViewFlowLayout

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13492037/
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

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 21:16:14  来源:igfitidea点击:

targetContentOffsetForProposedContentOffset:withScrollingVelocity without subclassing UICollectionViewFlowLayout

iphoneiosuicollectionview

提问by Fogmeister

I've got a very simple collectionView in my app (just a single row of square thumbnail images).

我的应用程序中有一个非常简单的 collectionView(只有一行方形缩略图)。

I'd like to intercept the scrolling so that the offset always leaves a full image at the left side. At the moment it scrolls to wherever and will leave cut off images.

我想拦截滚动,以便偏移量始终在左侧留下完整的图像。目前它会滚动到任何地方,并会留下截断的图像。

Anyway, I know I need to use the function

无论如何,我知道我需要使用该功能

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

to do this but I'm just using a standard UICollectionViewFlowLayout. I'm not subclassing it.

要做到这一点,但我只是使用标准的UICollectionViewFlowLayout. 我不是子类化它。

Is there any way of intercepting this without subclassing UICollectionViewFlowLayout?

有没有办法在没有子类化的情况下拦截它UICollectionViewFlowLayout

Thanks

谢谢

回答by Fogmeister

OK, answer is no, there is no way to do this without subclassing UICollectionViewFlowLayout.

好吧,答案是否定的,没有子类化 UICollectionViewFlowLayout 就没有办法做到这一点。

However, subclassing it is incredibly easy for anyone who is reading this in the future.

但是,对于将来阅读本文的任何人来说,将其子类化是非常容易的。

First I set up the subclass call MyCollectionViewFlowLayoutand then in interface builder I changed the collection view layout to Custom and selected my flow layout subclass.

首先我设置了子类调用MyCollectionViewFlowLayout,然后在界面构建器中我将集合视图布局更改为自定义并选择了我的流布局子类。

Because you're doing it this way you can't specify items sizes, etc... in IB so in MyCollectionViewFlowLayout.m I have this...

因为你是这样做的,所以你不能在 IB 中指定项目大小等......所以在 MyCollectionViewFlowLayout.m 我有这个......

- (void)awakeFromNib
{
    self.itemSize = CGSizeMake(75.0, 75.0);
    self.minimumInteritemSpacing = 10.0;
    self.minimumLineSpacing = 10.0;
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    self.sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0);
}

This sets up all the sizes for me and the scroll direction.

这为我设置了所有尺寸和滚动方向。

Then ...

然后 ...

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    CGFloat offsetAdjustment = MAXFLOAT;
    CGFloat horizontalOffset = proposedContentOffset.x + 5;

    CGRect targetRect = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);

    NSArray *array = [super layoutAttributesForElementsInRect:targetRect];

    for (UICollectionViewLayoutAttributes *layoutAttributes in array) {
        CGFloat itemOffset = layoutAttributes.frame.origin.x;
        if (ABS(itemOffset - horizontalOffset) < ABS(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset;
        }
    }

    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

This ensures that the scrolling ends with a margin of 5.0 on the left hand edge.

这确保滚动结束时左边缘的边距为 5.0。

That's all I needed to do. I didn't need to set the flow layout in code at all.

这就是我需要做的。我根本不需要在代码中设置流布局。

回答by DarthMike

Dan's solution is flawed. It does not handle user flicking well. The cases when user flicks fast and scroll did not move so much, have animation glitches.

丹的解决方案是有缺陷的。它不能很好地处理用户轻弹。用户快速滑动和滚动没有那么多移动的情况下,会出现动画故障。

My proposed alternative implementation has the same pagination as proposed before, but handles user flicking between pages.

我提议的替代实现与之前提议的分页相同,但处理用户在页面之间轻弹。

 #pragma mark - Pagination
 - (CGFloat)pageWidth {
     return self.itemSize.width + self.minimumLineSpacing;
 }

 - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
 {           
        CGFloat rawPageValue = self.collectionView.contentOffset.x / self.pageWidth;
        CGFloat currentPage = (velocity.x > 0.0) ? floor(rawPageValue) : ceil(rawPageValue);
        CGFloat nextPage = (velocity.x > 0.0) ? ceil(rawPageValue) : floor(rawPageValue);

        BOOL pannedLessThanAPage = fabs(1 + currentPage - rawPageValue) > 0.5;
        BOOL flicked = fabs(velocity.x) > [self flickVelocity];
        if (pannedLessThanAPage && flicked) {
            proposedContentOffset.x = nextPage * self.pageWidth;
        } else {
            proposedContentOffset.x = round(rawPageValue) * self.pageWidth;
        }

        return proposedContentOffset;
 }

 - (CGFloat)flickVelocity {
     return 0.3;
 }

回答by André Abreu

Swift version of the accepted answer.

已接受答案的 Swift 版本。

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var offsetAdjustment = CGFloat.greatestFiniteMagnitude
    let horizontalOffset = proposedContentOffset.x
    let targetRect = CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: self.collectionView!.bounds.size)

    for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
        let itemOffset = layoutAttributes.frame.origin.x
        if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset
        }
    }

    return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}    

Valid for Swift 5.

Swift 5有效。

回答by Dan Abramov

While this answerhas been a great help to me, there is a noticeable flicker when you swipe fast on a small distance. It's much easier to reproduce it on the device.

虽然这个答案对我有很大帮助,但是当您在一小段距离内快速滑动时,会出现明显的闪烁。在设备上重现它要容易得多。

I found that this always happens when collectionView.contentOffset.x - proposedContentOffset.xand velocity.xhave different sings.

我发现,这总是发生时collectionView.contentOffset.x - proposedContentOffset.x,并velocity.x有不同的歌唱。

My solution was to ensure that proposedContentOffsetis more than contentOffset.xif velocity is positive, and less if it is negative. It's in C# but should be fairly simple to translate to Objective C:

我的解决方案是确保proposedContentOffsetcontentOffset.x速度为正时更多,如果速度为负则更少。它在 C# 中,但转换为目标 C 应该相当简单:

public override PointF TargetContentOffset (PointF proposedContentOffset, PointF scrollingVelocity)
{
    /* Determine closest edge */

    float offSetAdjustment = float.MaxValue;
    float horizontalCenter = (float) (proposedContentOffset.X + (this.CollectionView.Bounds.Size.Width / 2.0));

    RectangleF targetRect = new RectangleF (proposedContentOffset.X, 0.0f, this.CollectionView.Bounds.Size.Width, this.CollectionView.Bounds.Size.Height);
    var array = base.LayoutAttributesForElementsInRect (targetRect);

    foreach (var layoutAttributes in array) {
        float itemHorizontalCenter = layoutAttributes.Center.X;
        if (Math.Abs (itemHorizontalCenter - horizontalCenter) < Math.Abs (offSetAdjustment)) {
            offSetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
    }

    float nextOffset = proposedContentOffset.X + offSetAdjustment;

    /*
     * ... unless we end up having positive speed
     * while moving left or negative speed while moving right.
     * This will cause flicker so we resort to finding next page
     * in the direction of velocity and use it.
     */

    do {
        proposedContentOffset.X = nextOffset;

        float deltaX = proposedContentOffset.X - CollectionView.ContentOffset.X;
        float velX = scrollingVelocity.X;

        // If their signs are same, or if either is zero, go ahead
        if (Math.Sign (deltaX) * Math.Sign (velX) != -1)
            break;

        // Otherwise, look for the closest page in the right direction
        nextOffset += Math.Sign (scrollingVelocity.X) * SnapStep;
    } while (IsValidOffset (nextOffset));

    return proposedContentOffset;
}

bool IsValidOffset (float offset)
{
    return (offset >= MinContentOffset && offset <= MaxContentOffset);
}

This code is using MinContentOffset, MaxContentOffsetand SnapStepwhich should be trivial for you to define. In my case they turned out to be

该代码使用MinContentOffsetMaxContentOffsetSnapStep这应该是平凡的你定义。就我而言,他们原来是

float MinContentOffset {
    get { return -CollectionView.ContentInset.Left; }
}

float MaxContentOffset {
    get { return MinContentOffset + CollectionView.ContentSize.Width - ItemSize.Width; }
}

float SnapStep {
    get { return ItemSize.Width + MinimumLineSpacing; }
}

回答by Pion

After long testing I found solution to snap to center with custom cell width (each cell has diff. width) which fixes the flickering. Feel free to improve the script.

经过长时间的测试,我找到了使用自定义单元格宽度(每个单元格都有不同的宽度)对齐中心的解决方案,它修复了闪烁。随意改进脚本。

- (CGPoint) targetContentOffsetForProposedContentOffset: (CGPoint) proposedContentOffset withScrollingVelocity: (CGPoint)velocity
{
    CGFloat offSetAdjustment = MAXFLOAT;
    CGFloat horizontalCenter = (CGFloat) (proposedContentOffset.x + (self.collectionView.bounds.size.width / 2.0));

    //setting fastPaging property to NO allows to stop at page on screen (I have pages lees, than self.collectionView.bounds.size.width)
    CGRect targetRect = CGRectMake(self.fastPaging ? proposedContentOffset.x : self.collectionView.contentOffset.x, 
                                   0.0,
                                   self.collectionView.bounds.size.width,
                                   self.collectionView.bounds.size.height);

    NSArray *attributes = [self layoutAttributesForElementsInRect:targetRect];
    NSPredicate *cellAttributesPredicate = [NSPredicate predicateWithBlock: ^BOOL(UICollectionViewLayoutAttributes * _Nonnull evaluatedObject,
                                                                             NSDictionary<NSString *,id> * _Nullable bindings) 
    {
        return (evaluatedObject.representedElementCategory == UICollectionElementCategoryCell); 
    }];        

    NSArray *cellAttributes = [attributes filteredArrayUsingPredicate: cellAttributesPredicate];

    UICollectionViewLayoutAttributes *currentAttributes;

    for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
    {
        CGFloat itemHorizontalCenter = layoutAttributes.center.x;
        if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offSetAdjustment))
        {
            currentAttributes   = layoutAttributes;
            offSetAdjustment    = itemHorizontalCenter - horizontalCenter;
        }
    }

    CGFloat nextOffset          = proposedContentOffset.x + offSetAdjustment;

    proposedContentOffset.x     = nextOffset;
    CGFloat deltaX              = proposedContentOffset.x - self.collectionView.contentOffset.x;
    CGFloat velX                = velocity.x;

    // detection form  gist.github.com/rkeniger/7687301
    // based on http://stackoverflow.com/a/14291208/740949
    if (fabs(deltaX) <= FLT_EPSILON || fabs(velX) <= FLT_EPSILON || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) 
    {

    } 
    else if (velocity.x > 0.0) 
    {
       // revert the array to get the cells from the right side, fixes not correct center on different size in some usecases
        NSArray *revertedArray = [[array reverseObjectEnumerator] allObjects];

        BOOL found = YES;
        float proposedX = 0.0;

        for (UICollectionViewLayoutAttributes *layoutAttributes in revertedArray)
        {
            if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell)
            {
                CGFloat itemHorizontalCenter = layoutAttributes.center.x;
                if (itemHorizontalCenter > proposedContentOffset.x) {
                     found = YES;
                     proposedX = nextOffset + (currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2);
                } else {
                     break;
                }
            }
        }

       // dont set on unfound element
        if (found) {
            proposedContentOffset.x = proposedX;
        }
    } 
    else if (velocity.x < 0.0) 
    {
        for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
        {
            CGFloat itemHorizontalCenter = layoutAttributes.center.x;
            if (itemHorizontalCenter > proposedContentOffset.x) 
            {
                proposedContentOffset.x = nextOffset - ((currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2));
                break;
            }
        }
    }

    proposedContentOffset.y = 0.0;

    return proposedContentOffset;
}

回答by JoniVR

Here's my implementation in Swift 5for verticalcell-based paging:

这是我在Swift 5 中实现的基于单元格的垂直分页:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    guard let collectionView = self.collectionView else {
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    }

    // Page height used for estimating and calculating paging.
    let pageHeight = self.itemSize.height + self.minimumLineSpacing

    // Make an estimation of the current page position.
    let approximatePage = collectionView.contentOffset.y/pageHeight

    // Determine the current page based on velocity.
    let currentPage = velocity.y == 0 ? round(approximatePage) : (velocity.y < 0.0 ? floor(approximatePage) : ceil(approximatePage))

    // Create custom flickVelocity.
    let flickVelocity = velocity.y * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - collectionView.contentInset.top

    return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}

Some notes:

一些注意事项:

  • Doesn't glitch
  • SET PAGING TO FALSE! (otherwise this won't work)
  • Allows you to set your own flickvelocityeasily.
  • If something is still not working after trying this, check if your itemSizeactually matches the size of the item as that's often a problem, especially when using collectionView(_:layout:sizeForItemAt:), use a custom variable with the itemSize instead.
  • This works best when you set self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.
  • 不会出现故障
  • 将分页设置为假!(否则这将不起作用)
  • 允许您轻松设置自己的轻弹速度
  • 如果尝试此操作后仍然无法正常工作,请检查您的itemSize实际大小是否与项目的大小匹配,因为这通常是一个问题,尤其是在使用时collectionView(_:layout:sizeForItemAt:),请改用带有 itemSize 的自定义变量。
  • 当您设置self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Here's a horizontalversion (haven't tested it thoroughly so please forgive any mistakes):

这是一个横向版本(尚未彻底测试,因此请原谅任何错误):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    guard let collectionView = self.collectionView else {
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    }

    // Page width used for estimating and calculating paging.
    let pageWidth = self.itemSize.width + self.minimumInteritemSpacing

    // Make an estimation of the current page position.
    let approximatePage = collectionView.contentOffset.x/pageWidth

    // Determine the current page based on velocity.
    let currentPage = velocity.x == 0 ? round(approximatePage) : (velocity.x < 0.0 ? floor(approximatePage) : ceil(approximatePage))

    // Create custom flickVelocity.
    let flickVelocity = velocity.x * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    // Calculate newHorizontalOffset.
    let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - collectionView.contentInset.left

    return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y)
}

This code is based on the code I use in my personal project, you can check it out hereby downloading it and running the Example target.

此代码基于我在个人项目中使用的代码,您可以在此处下载并运行 Example 目标来查看它。

回答by katopz

refer to this answer by Dan Abramovhere's Swift version

参考Dan Abramov 的这个答案,这里是 Swift 版本

    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var _proposedContentOffset = CGPoint(x: proposedContentOffset.x, y: proposedContentOffset.y)
    var offSetAdjustment: CGFloat = CGFloat.max
    let horizontalCenter: CGFloat = CGFloat(proposedContentOffset.x + (self.collectionView!.bounds.size.width / 2.0))

    let targetRect = CGRect(x: proposedContentOffset.x, y: 0.0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)

    let array: [UICollectionViewLayoutAttributes] = self.layoutAttributesForElementsInRect(targetRect)! as [UICollectionViewLayoutAttributes]
    for layoutAttributes: UICollectionViewLayoutAttributes in array {
        if (layoutAttributes.representedElementCategory == UICollectionElementCategory.Cell) {
            let itemHorizontalCenter: CGFloat = layoutAttributes.center.x
            if (abs(itemHorizontalCenter - horizontalCenter) < abs(offSetAdjustment)) {
                offSetAdjustment = itemHorizontalCenter - horizontalCenter
            }
        }
    }

    var nextOffset: CGFloat = proposedContentOffset.x + offSetAdjustment

    repeat {
        _proposedContentOffset.x = nextOffset
        let deltaX = proposedContentOffset.x - self.collectionView!.contentOffset.x
        let velX = velocity.x

        if (deltaX == 0.0 || velX == 0 || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) {
            break
        }

        if (velocity.x > 0.0) {
            nextOffset = nextOffset + self.snapStep()
        } else if (velocity.x < 0.0) {
            nextOffset = nextOffset - self.snapStep()
        }
    } while self.isValidOffset(nextOffset)

    _proposedContentOffset.y = 0.0

    return _proposedContentOffset
}

func isValidOffset(offset: CGFloat) -> Bool {
    return (offset >= CGFloat(self.minContentOffset()) && offset <= CGFloat(self.maxContentOffset()))
}

func minContentOffset() -> CGFloat {
    return -CGFloat(self.collectionView!.contentInset.left)
}

func maxContentOffset() -> CGFloat {
    return CGFloat(self.minContentOffset() + self.collectionView!.contentSize.width - self.itemSize.width)
}

func snapStep() -> CGFloat {
    return self.itemSize.width + self.minimumLineSpacing;
}

or gist here https://gist.github.com/katopz/8b04c783387f0c345cd9

或者这里的要点https://gist.github.com/katopz/8b04c783387f0c345cd9

回答by Oliver Pearmain

For anyone looking for a solution that...

对于任何正在寻找解决方案的人...

  • DOES NOT GLITCH when the user performs a short fast scroll (i.e. it considers positive and negative scroll velocities)
  • takes the collectionView.contentInset(and safeArea on iPhone X) into consideration
  • only considers thoes cells visible at the point of scrolling (for peformance)
  • uses well named variables and comments
  • is Swift 4
  • 当用户执行短的快速滚动时不会出现故障(即它考虑正和负滚动速度)
  • 考虑collectionView.contentInset(和 iPhone X 上的安全区域)
  • 只考虑滚动点可见的 thoes 单元格(为了性能)
  • 使用命名良好的变量和注释
  • 是斯威夫特 4

then please see below...

那么请看下面...

public class CarouselCollectionViewLayout: UICollectionViewFlowLayout {

    override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

        guard let collectionView = collectionView else {
            return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        }

        // Identify the layoutAttributes of cells in the vicinity of where the scroll view will come to rest
        let targetRect = CGRect(origin: proposedContentOffset, size: collectionView.bounds.size)
        let visibleCellsLayoutAttributes = layoutAttributesForElements(in: targetRect)

        // Translate those cell layoutAttributes into potential (candidate) scrollView offsets
        let candidateOffsets: [CGFloat]? = visibleCellsLayoutAttributes?.map({ cellLayoutAttributes in
            if #available(iOS 11.0, *) {
                return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left - sectionInset.left
            } else {
                return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - sectionInset.left
            }
        })

        // Now we need to work out which one of the candidate offsets is the best one
        let bestCandidateOffset: CGFloat

        if velocity.x > 0 {
            // If the scroll velocity was POSITIVE, then only consider cells/offsets to the RIGHT of the proposedContentOffset.x
            // Of the cells/offsets to the right, the NEAREST is the `bestCandidate`
            // If there is no nearestCandidateOffsetToLeft then we default to the RIGHT-MOST (last) of ALL the candidate cells/offsets
            //      (this handles the scenario where the user has scrolled beyond the last cell)
            let candidateOffsetsToRight = candidateOffsets?.toRight(ofProposedOffset: proposedContentOffset.x)
            let nearestCandidateOffsetToRight = candidateOffsetsToRight?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffsetToRight ?? candidateOffsets?.last ?? proposedContentOffset.x
        }
        else if velocity.x < 0 {
            // If the scroll velocity was NEGATIVE, then only consider cells/offsets to the LEFT of the proposedContentOffset.x
            // Of the cells/offsets to the left, the NEAREST is the `bestCandidate`
            // If there is no nearestCandidateOffsetToLeft then we default to the LEFT-MOST (first) of ALL the candidate cells/offsets
            //      (this handles the scenario where the user has scrolled beyond the first cell)
            let candidateOffsetsToLeft = candidateOffsets?.toLeft(ofProposedOffset: proposedContentOffset.x)
            let nearestCandidateOffsetToLeft = candidateOffsetsToLeft?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffsetToLeft ?? candidateOffsets?.first ?? proposedContentOffset.x
        }
        else {
            // If the scroll velocity was ZERO we consider all `candidate` cells (regarless of whether they are to the left OR right of the proposedContentOffset.x)
            // The cell/offset that is the NEAREST is the `bestCandidate`
            let nearestCandidateOffset = candidateOffsets?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffset ??  proposedContentOffset.x
        }

        return CGPoint(x: bestCandidateOffset, y: proposedContentOffset.y)
    }

}

fileprivate extension Sequence where Iterator.Element == CGFloat {

    func toLeft(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {

        return filter() { candidateOffset in
            return candidateOffset < proposedOffset
        }
    }

    func toRight(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {

        return filter() { candidateOffset in
            return candidateOffset > proposedOffset
        }
    }

    func nearest(toProposedOffset proposedOffset: CGFloat) -> CGFloat? {

        guard let firstCandidateOffset = first(where: { _ in true }) else {
            // If there are no elements in the Sequence, return nil
            return nil
        }

        return reduce(firstCandidateOffset) { (bestCandidateOffset: CGFloat, candidateOffset: CGFloat) -> CGFloat in

            let candidateOffsetDistanceFromProposed = fabs(candidateOffset - proposedOffset)
            let bestCandidateOffsetDistancFromProposed = fabs(bestCandidateOffset - proposedOffset)

            if candidateOffsetDistanceFromProposed < bestCandidateOffsetDistancFromProposed {
                return candidateOffset
            }

            return bestCandidateOffset
        }
    }
}

回答by Scott Kaiser

Here is my Swift solution on a horizontally scrolling collection view. It's simple, sweet and avoids any flickering.

这是我在水平滚动集合视图上的 Swift 解决方案。它简单,甜美,避免任何闪烁。

  override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else { return proposedContentOffset }

    let currentXOffset = collectionView.contentOffset.x
    let nextXOffset = proposedContentOffset.x
    let maxIndex = ceil(currentXOffset / pageWidth())
    let minIndex = floor(currentXOffset / pageWidth())

    var index: CGFloat = 0

    if nextXOffset > currentXOffset {
      index = maxIndex
    } else {
      index = minIndex
    }

    let xOffset = pageWidth() * index
    let point = CGPointMake(xOffset, 0)

    return point
  }

  func pageWidth() -> CGFloat {
    return itemSize.width + minimumInteritemSpacing
  }

回答by Anton Gaenko

I prefer to allow user flicking through several pages. So here is my version of targetContentOffsetForProposedContentOffset(which based on DarthMike answer) for verticallayout.

我更喜欢允许用户快速浏览多个页面。所以这是我targetContentOffsetForProposedContentOffset垂直布局版本(基于 DarthMike 的回答)。

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    CGFloat approximatePage = self.collectionView.contentOffset.y / self.pageHeight;
    CGFloat currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage);

    NSInteger flickedPages = ceil(velocity.y / self.flickVelocity);

    if (flickedPages) {
        proposedContentOffset.y = (currentPage + flickedPages) * self.pageHeight;
    } else {
        proposedContentOffset.y = currentPage * self.pageHeight;
    }

    return proposedContentOffset;
}

- (CGFloat)pageHeight {
    return self.itemSize.height + self.minimumLineSpacing;
}

- (CGFloat)flickVelocity {
    return 1.2;
}