targetContentOffsetForProposedContentOffset:withScrollingVelocity无子类化UICollectionViewFlowLayout

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

我想拦截滚动,以便偏移总是在左侧留下完整的图像。 此刻它滚动到任何地方,并会留下切断的图像。

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

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity 

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

有没有任何方法来拦截这个没有UICollectionViewFlowLayout

谢谢

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

但是,对于将来阅读这些内容的人来说,对它进行子类化是非常容易的。

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

因为你这样做,你不能指定项目大小,等等…在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的余量结束。

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

丹的解决scheme是有缺陷的。 它不处理用户轻弹。 用户快速滚动和滚动的情况并没有那么多,有animation故障。

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

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

经过长时间的testing,我发现解决scheme捕捉中心与自定义单元格宽度(每个单元格有diff.宽度)修复闪烁。 随意改进脚本。

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

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

我发现这总是发生在collectionView.contentOffset.x - proposedContentOffset.xvelocity.x有不同的唱歌。

我的解决scheme是,如果velocity是正值,那么确保proposedContentOffsetcontentOffset.x如果是负值,则更less。 这是在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); } 

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

我只是想把这个接受的答案的Swift版本放在这里。

 override func targetContentOffsetForProposedContentOffset(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 3

丹·阿布拉莫夫(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

我更喜欢让用户轻弹几页。 所以这里是我的版本的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; } 

除非我滚动到行的末尾,否则Fogmeisters的答案对我来说很有效。 我的单元格不适合在屏幕上,所以它会滚动到结尾,并跳跃回来,使最后一个单元格总是重叠屏幕的右边缘。

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

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

我在使用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); } 

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

这是我的Swift解决scheme在水平滚动collections视图。 它很简单,甜美,避免任何闪烁。

  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 } 

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

斯威夫特4

一个大小的单元格(水平滚动)的集合视图的最简单的解决scheme:

 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 } 

一个较短的解决scheme(假设你正在caching你的布局属性):

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

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

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