【问题标题】:targetContentOffsetForProposedContentOffset:withScrollingVelocity without subclassing UICollectionViewFlowLayouttargetContentOffsetForProposedContentOffset:withScrollingVelocity 没有子类化 UICollectionViewFlowLayout
【发布时间】:2012-11-09 15:09:57
【问题描述】:

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

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

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

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

要做到这一点,但我只是使用标准的UICollectionViewFlowLayout。我没有继承它。

有没有办法在不继承 UICollectionViewFlowLayout 的情况下拦截它?

谢谢

【问题讨论】:

    标签: iphone ios uicollectionview


    【解决方案1】:

    好的,答案是否定的,如果不继承 UICollectionViewFlowLayout 就没有办法做到这一点。

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

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

    因为你这样做你不能指定项目大小等...在 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);
    }
    

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

    然后……

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

    这可确保滚动在左侧边缘以 5.0 的边距结束。

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

    【讨论】:

    • 使用得当真的很强大。您是否观看过 WWDC 2012 的 Collection View 会议?他们真的很值得一看。一些不可思议的东西。
    • targetContentOffsetForProposedContentOffset:withVelocity 在我滚动时不会被调用。怎么回事?
    • @TomSawyer 将 UICollectionView 的声明率设置为 UIScrollViewDecelerationRateFast。
    • @fatuhoku 确保您的 collectionView 的 paginEnabled 属性设置为 false
    • 天哪,我不得不向下滚动一百万英里才能看到这个答案。 :)
    【解决方案2】:

    Dan 的解决方案有缺陷。它不能很好地处理用户轻弹。用户快速滑动和滚动没有太多移动的情况,存在动画故障。

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

     #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;
     }
    

    【讨论】:

    • 谢谢!这就像一个魅力。有点难以理解,但到达那里。
    • 我遇到了这个错误:不能分配给 'proposedContentOffset' 中的 'x' ?使用迅捷?我如何分配给 x 值?
    • @TomSawyer 参数默认为“让”。尝试在 Swift 中这样声明函数(在参数之前使用 var): override func targetContentOffsetForProposedContentOffset(var提出的ContentOffset: CGPoint) -> CGPoint
    • 你不能在swift中使用CGPointMake。我个人使用这个:“var targetContentOffset: CGPoint if pannedLessThanAPage && flicked { targetContentOffset = CGPoint(x: nextPage * pageWidth(), y:proposedContentOffset.y); } else { targetContentOffset = CGPoint(x: round(rawPageValue) * pageWidth( ), y:提出的ContentOffset.y); } 返回提出的ContentOffset"
    • 应该是选择的答案。
    【解决方案3】:

    已接受答案的 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)
    }    
    

    适用于 Swift 5

    【讨论】:

    • 这个版本很好用,如果你交换代码,它也适用于 Y 轴。
    • 在这里效果很好。但是,如果我停止滚动并(小心地)抬起手指,它就不会滚动到任何页面并停在那里。
    • @ChristianA.Strømmen Weird,它适用于我的应用程序。
    • @AndréAbreu 我在哪里放置这个函数?
    • @Jay 您需要继承 UICollectionViewLayout 或任何已经继承它的类(例如 UICollectionViewFlowLayout)。
    【解决方案4】:

    这是我在 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)
    }
    

    一些注意事项:

    • 不会出现故障
    • 将分页设置为假! (否则这将不起作用)
    • 允许您轻松设置您自己的轻弹速度
    • 如果尝试此操作后仍有问题,请检查您的 itemSize 是否确实与项目的大小匹配,因为这通常是一个问题,尤其是在使用 collectionView(_:layout:sizeForItemAt:) 时,请改用带有 itemSize 的自定义变量。李>
    • 设置self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast 时效果最佳。

    这是一个横向版本(未彻底测试,如有错误请见谅):

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

    此代码基于我在个人项目中使用的代码,您可以通过下载并运行示例目标来查看here

    【讨论】:

    • 你是生命的救星!重要的是要注意将分页设置为 FALSE!!!我花了 2 个小时来修复你的功能,这已经可以工作了......
    • @denis631 很抱歉!我应该补充一点,我会编辑帖子以反映这一点!很高兴它起作用了:)
    • jesssus,我想知道为什么这不起作用,直到我看到这条关于禁用分页的评论......当然我的设置为 true
    • @JoniVR 它告诉我这个错误方法不会覆盖其超类中的任何方法
    • @JoniVR 这行得通,但我注意到如果我滚动得非常快,它不会捕捉到下一个单元格,它只会滚动到下一个“组”单元格。这是为什么呢?
    【解决方案5】:

    对于任何寻求解决方案的人......

    • 当用户执行短时间快速滚动时不会出现故障(即它考虑正负滚动速度)
    • 考虑到collectionView.contentInset(以及 iPhone X 上的 safeArea)
    • 仅考虑滚动点可见的单元格(用于性能)
    • 使用命名良好的变量和 cmets
    • 是 Swift 4

    那么请看下面...

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

    【讨论】:

    • 谢谢!刚刚复制和粘贴,完美运行.. 比预期的要好。
    • 一个也是唯一一个实际上有效的解决方案。不错的工作!谢谢!
    • return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left CandidateOffsets - sectionInset.left 这行有问题
    • @Dalmaz 感谢您通知我。我现在已经解决了这个问题。
    • 是的,刚刚复制和粘贴,你节省了我的时间。
    【解决方案6】:

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

    我发现当collectionView.contentOffset.x - proposedContentOffset.xvelocity.x 有不同的歌声时,总会发生这种情况。

    我的解决方案是确保proposedContentOffset 在速度为正时大于contentOffset.x,在速度为负时小于。它在 C# 中,但转换为 Objective 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);
    }
    

    此代码使用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; }
    }
    

    【讨论】:

    【解决方案7】:

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

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

    【讨论】:

    • 最好的解决方案,谢谢!同样对于任何未来的读者,您必须关闭分页才能使其正常工作。
    • 如果要从左对齐,而不是单元格在中心右对齐,我们将如何改变它?
    • 不确定我是否理解正确,但是如果您想在中心开始项目,并将其对齐到中心,则需要更改 contentInset。我用这个:gist.github.com/pionl/432fc8059dee3b540e38
    • 要将单元格的 X 位置对齐到视图的中间,只需删除速度部分中的 + (layoutAttributes.frame.size.width / 2)。
    • @Jay 嗨,只需创建一个自定义 Flow 委托并将此代码添加到其中。不要忘记在 nib 或代码中设置自定义布局。
    【解决方案8】:

    参考this answer by 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;
    }
    

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

    【讨论】:

    • 该死@mstubna,我继续复制上面的内容,将其更新为 swift 3,开始制作更新的要点,然后回到这里收集笔记/标题,此时我注意到你有已经做了一个快速的 3 要点。谢谢!可惜我错过了。
    【解决方案9】:

    这是我在水平滚动集合视图上的 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
      }
    

    【讨论】:

    • 什么是itemSize ??
    • 它是集合单元格的大小。这些函数在子类化 UICollectionViewFlowLayout 时使用。
    • 我喜欢这个解决方案,但我有几个 cmets。 pageWidth() 应该使用minimumLineSpacing,因为它水平滚动。在我的例子中,我有一个 contentInset 用于集合视图,以便第一个和最后一个单元格可以居中,所以我使用 let xOffset = pageWidth() * index - collectionView.contentInset.left
    【解决方案10】:

    我在使用 targetContentOffsetForProposedContentOffset 时遇到的一个小问题是最后一个单元格没有根据我返回的新点进行调整。
    我发现我返回的 CGPoint 的 Y 值大于允许的值,因此我在 targetContentOffsetForProposedContentOffset 实现的末尾使用了以下代码:

    // if the calculated y is bigger then the maximum possible y we adjust accordingly
    CGFloat contentHeight = self.collectionViewContentSize.height;
    CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
    CGFloat maxY = contentHeight - collectionViewHeight;
    if (newY > maxY)
    {
        newY = maxY;
    }
    
    return CGPointMake(0, newY);
    

    为了更清楚,这是我的完整布局实现,它只是模仿垂直分页行为:

    - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
    {
        return [self targetContentOffsetForProposedContentOffset:proposedContentOffset];
    }
    
    - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
    {
        CGFloat heightOfPage = self.itemSize.height;
        CGFloat heightOfSpacing = self.minimumLineSpacing;
    
        CGFloat numOfPage = lround(proposedContentOffset.y / (heightOfPage + heightOfSpacing));
        CGFloat newY = numOfPage * (heightOfPage + heightOfSpacing);
    
        // if the calculated y is bigger then the maximum possible y we adjust accordingly
        CGFloat contentHeight = self.collectionViewContentSize.height;
        CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
        CGFloat maxY = contentHeight - collectionViewHeight;
        if (newY > maxY)
        {
            newY = maxY;
        }
    
        return CGPointMake(0, newY);
    }
    

    希望这会节省一些时间和头痛

    【讨论】:

    • 同样的问题,似乎集合视图会忽略无效值,而不是将它们四舍五入到其范围内。
    【解决方案11】:

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

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

    【讨论】:

      【解决方案12】:

      除非我滚动到行尾,否则 Fogmeisters 的答案对我有用。我的单元格不能整齐地显示在屏幕上,因此它会滚动到最后并突然跳回,因此最后一个单元格总是与屏幕的右边缘重叠。

      为了防止这种情况,在 targetcontentoffset 方法的开头添加以下代码行

      if(proposedContentOffset.x>self.collectionViewContentSize.width-320-self.sectionInset.right)
          return proposedContentOffset;
      

      【讨论】:

      • 我想 320 是您的收藏视图宽度 :)
      • 喜欢回顾旧代码。我猜那个神奇的数字就是那个。
      【解决方案13】:

      @André Abreu的代码

      Swift3 版本

      class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
          override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
              var offsetAdjustment = CGFloat.greatestFiniteMagnitude
              let horizontalOffset = proposedContentOffset.x
              let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
              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)
          }
      }
      

      【讨论】:

      • 谢谢!预期的最佳行为谢谢它的帮助很大!
      【解决方案14】:

      斯威夫特 4

      使用一种尺寸的单元格(水平滚动)的集合视图的最简单解决方案:

      override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
          guard let collectionView = collectionView else { return proposedContentOffset }
      
          // Calculate width of your page
          let pageWidth = calculatedPageWidth()
      
          // Calculate proposed page
          let proposedPage = round(proposedContentOffset.x / pageWidth)
      
          // Adjust necessary offset
          let xOffset = pageWidth * proposedPage - collectionView.contentInset.left
      
          return CGPoint(x: xOffset, y: 0)
      }
      
      func calculatedPageWidth() -> CGFloat {
          return itemSize.width + minimumInteritemSpacing
      }
      

      【讨论】:

        【解决方案15】:

        更短的解决方案(假设您正在缓存布局属性):

        override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
            let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
            let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
            return CGPoint(x: targetLayoutAttributes.frame.minX - horizontalPadding, y: 0)
        }
        

        把它放在上下文中:

        class Layout : UICollectionViewLayout {
            private var cache: [UICollectionViewLayoutAttributes] = []
            private static let horizontalPadding: CGFloat = 16
            private static let interItemSpacing: CGFloat = 8
        
            override func prepare() {
                let (itemWidth, itemHeight) = (collectionView!.bounds.width - 2 * Layout.horizontalPadding, collectionView!.bounds.height)
                cache.removeAll()
                let count = collectionView!.numberOfItems(inSection: 0)
                var x: CGFloat = Layout.horizontalPadding
                for item in (0..<count) {
                    let indexPath = IndexPath(item: item, section: 0)
                    let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                    attributes.frame = CGRect(x: x, y: 0, width: itemWidth, height: itemHeight)
                    cache.append(attributes)
                    x += itemWidth + Layout.interItemSpacing
                }
            }
        
            override var collectionViewContentSize: CGSize {
                let width: CGFloat
                if let maxX = cache.last?.frame.maxX {
                    width = maxX + Layout.horizontalPadding
                } else {
                    width = collectionView!.width
                }
                return CGSize(width: width, height: collectionView!.height)
            }
        
            override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
                return cache.first { $0.indexPath == indexPath }
            }
        
            override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
                return cache.filter { $0.frame.intersects(rect) }
            }
        
            override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
                let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
                let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
                return CGPoint(x: targetLayoutAttributes.frame.minX - Layout.horizontalPadding, y: 0)
            }
        }
        

        【讨论】:

          【解决方案16】:

          为了确保它在 Swift 版本(现在是 swift 5)中工作,我使用了来自 @André Abreu 的 answer,我添加了一些更多信息:

          子类化 UICollectionViewFlowLayout 时,“覆盖 func awakeFromNib(){}”不起作用(不知道为什么)。相反,我使用了“覆盖 init(){super.init()}”

          这是我放在 SubclassFlowLayout 类中的代码:UICollectionViewFlowLayout {}:

          let padding: CGFloat = 16
          override init() {
              super.init()
              self.minimumLineSpacing = padding
              self.minimumInteritemSpacing = 2
              self.scrollDirection = .horizontal
              self.sectionInset = UIEdgeInsets(top: 0, left: padding, bottom: 0, right: 100) //right = "should set for footer" (Horizental)
          
          }
          
          required init?(coder: NSCoder) {
              fatalError("init(coder:) has not been implemented")
          }
          
          override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
              var offsetAdjustment = CGFloat.greatestFiniteMagnitude
              let leftInset = padding
              let horizontalOffset = proposedContentOffset.x + leftInset // leftInset is for "where you want the item stop on the left"
              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
                  }
              }
          
              let targetPoint = CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
              return targetPoint
          
          }
          

          子类化后,一定要把它放在 ViewDidLoad() 中:

          customCollectionView.collectionViewLayout = SubclassFlowLayout()
          customCollectionView.isPagingEnabled = false
          customCollectionView.decelerationRate = .fast //-> this for scrollView speed
          

          【讨论】:

          • 谢谢! collectionView.decelerationRate = .fast 是我要找的东西!
          【解决方案17】:

          对于那些在 Swift 中寻找解决方案的人:

          class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
              private let collectionViewHeight: CGFloat = 200.0
              private let screenWidth: CGFloat = UIScreen.mainScreen().bounds.width
          
              override func awakeFromNib() {
                  super.awakeFromNib()
          
                  self.itemSize = CGSize(width: [InsertItemWidthHere], height: [InsertItemHeightHere])
                  self.minimumInteritemSpacing = [InsertItemSpacingHere]
                  self.scrollDirection = .Horizontal
                  let inset = (self.screenWidth - CGFloat(self.itemSize.width)) / 2
                  self.collectionView?.contentInset = UIEdgeInsets(top: 0,
                                                                   left: inset,
                                                                   bottom: 0,
                                                                   right: inset)
              }
          
              override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
                  var offsetAdjustment = CGFloat.max
                  let horizontalOffset = proposedContentOffset.x + ((self.screenWidth - self.itemSize.width) / 2)
          
                  let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.screenWidth, height: self.collectionViewHeight)
                  var array = super.layoutAttributesForElementsInRect(targetRect)
          
                  for layoutAttributes in array! {
                      let itemOffset = layoutAttributes.frame.origin.x
                      if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
                          offsetAdjustment = itemOffset - horizontalOffset
                      }
                  }
          
                  return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
              }
          }
          

          【讨论】:

            【解决方案18】:

            这是一个按单元格分页的演示(快速滚动时,不要跳过一个或多个单元格):https://github.com/ApesTalk/ATPagingByCell

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2018-01-03
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多