【发布时间】:2013-12-10 14:17:35
【问题描述】:
我有一个可以一次显示大约 3.5 个单元格的集合视图,我希望它能够分页。但我希望它捕捉到每个单元格(就像 App Store 应用程序一样),而不是滚动视图的整个宽度。我该怎么做?
【问题讨论】:
标签: ios uicollectionview
我有一个可以一次显示大约 3.5 个单元格的集合视图,我希望它能够分页。但我希望它捕捉到每个单元格(就像 App Store 应用程序一样),而不是滚动视图的整个宽度。我该怎么做?
【问题讨论】:
标签: ios uicollectionview
另一种方法是创建一个自定义 UICollectionViewFlowLayout 并像这样覆盖该方法:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset
withScrollingVelocity:(CGPoint)velocity {
CGRect cvBounds = self.collectionView.bounds;
CGFloat halfWidth = cvBounds.size.width * 0.5f;
CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;
NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];
UICollectionViewLayoutAttributes* candidateAttributes;
for (UICollectionViewLayoutAttributes* attributes in attributesArray) {
// == Skip comparison with non-cell items (headers and footers) == //
if (attributes.representedElementCategory !=
UICollectionElementCategoryCell) {
continue;
}
// == First time in the loop == //
if(!candidateAttributes) {
candidateAttributes = attributes;
continue;
}
if (fabsf(attributes.center.x - proposedContentOffsetCenterX) <
fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
candidateAttributes = attributes;
}
}
return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);
}
如果您正在寻找 Swift 解决方案,请查看此Gist
【讨论】:
这是我在 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。
【讨论】:
let currentPage = velocity.x == 0 ? round(approximatePage) : (velocity.x < 0.0 ? floor(approximatePage) : ceil(approximatePage)) 替换let currentPage = (velocity.x < 0.0) ? floor(approximatePage) : ceil(approximatePage) 似乎会产生预期的捕捉到最近的项目。 (注意:这是针对水平版本的。垂直等效应该很容易解决)
您可以通过成为集合视图的委托并实现该方法来捕捉单元格:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
这告诉您用户已完成拖动,它允许您修改 targetContentOffset 以与您的单元格对齐(即四舍五入到最近的单元格)。请注意,您需要注意如何修改 targetContentOffset;特别是,您需要避免更改它,以便视图需要以与传递的速度相反的方向滚动,否则您将获得动画故障。如果你用谷歌搜索那个方法名,你可能会找到很多这样的例子。
【讨论】:
*targetContentOffset = CGPointMake(...)之类的东西。
在查看此处的解决方案之前,我开发了我的解决方案。我还创建了一个自定义 UICollectionViewFlowLayout 并覆盖了 targetContentOffset 方法。
这对我来说似乎工作得很好(即我得到了与 AppStore 中相同的行为),即使我得到的代码少得多。在这里,请随时指出您能想到的任何缺点:
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let inset: Int = 10
let vcBounds = self.collectionView!.bounds
var candidateContentOffsetX: CGFloat = proposedContentOffset.x
for attributes in self.layoutAttributesForElements(in: vcBounds)! as [UICollectionViewLayoutAttributes] {
if vcBounds.origin.x < attributes.center.x {
candidateContentOffsetX = attributes.frame.origin.x - CGFloat(inset)
break
}
}
return CGPoint(x: candidateContentOffsetX, y: proposedContentOffset.y)
}
【讨论】:
self.layoutAttributesForElements。
Mike M. 之前在帖子中提出的解决方案对我有用,但在我的情况下,我希望第一个单元格从 collectionView 的中间开始。所以我使用集合流委托方法来定义一个插入(collectionView:layout:insetForSectionAtIndex:)。这使得第一个单元格和第二个单元格之间的滚动被卡住并且无法正确滚动到第一个单元格。
原因是candidateAttributes.center.x - halfWidth 的值为负。解决方案是获取绝对值,所以我将 fabs 添加到这一行 return CGPointMake(fabs(candidateAttributes.center.x - halfWidth), offset.y);
应默认添加Fabs以涵盖所有情况。
【讨论】:
这是我的解决方案。适用于任何页面宽度。
设置self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast 感受真实的分页。
解决方案是基于一个部分滚动分页。
- (CGFloat)pageWidth {
return self.itemSize.width + self.minimumLineSpacing;
}
- (CGPoint)offsetAtCurrentPage {
CGFloat width = -self.collectionView.contentInset.left - self.sectionInset.left;
for (int i = 0; i < self.currentPage; i++)
width += [self pageWidth];
return CGPointMake(width, 0);
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset {
return [self offsetAtCurrentPage];
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
// To scroll paginated
/*
if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfItemsInSection:0]-1) self.currentPage += 1;
else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;
return [self offsetAtCurrentPage];
*/
// To scroll and stop always at the center of a page
CGRect proposedRect = CGRectMake(proposedContentOffset.x+self.collectionView.bounds.size.width/2 - self.pageWidth/2, 0, self.pageWidth, self.collectionView.bounds.size.height);
NSMutableArray <__kindof UICollectionViewLayoutAttributes *> *allAttributes = [[self layoutAttributesForElementsInRect:proposedRect] mutableCopy];
__block UICollectionViewLayoutAttributes *proposedAttributes = nil;
__block CGFloat minDistance = CGFLOAT_MAX;
[allAttributes enumerateObjectsUsingBlock:^(__kindof UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
CGFloat distance = CGRectGetMidX(proposedRect) - obj.center.x;
if (ABS(distance) < minDistance) {
proposedAttributes = obj;
minDistance = distance;
}
}];
// Scroll always
if (self.currentPage == proposedAttributes.indexPath.row) {
if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfItemsInSection:0]-1) self.currentPage += 1;
else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;
}
else {
self.currentPage = proposedAttributes.indexPath.row;
}
return [self offsetAtCurrentPage];
}
这是按部分分页的。
- (CGPoint)offsetAtCurrentPage {
CGFloat width = -self.collectionView.contentInset.leff;
for (int i = 0; i < self.currentPage; i++)
width += [self sectionWidth:i];
return CGPointMake(width, 0);
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
return [self offsetAtCurrentPage];
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
// To scroll paginated
/*
if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfSections]-1) self.currentPage += 1;
else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;
return [self offsetAtCurrentPage];
*/
// To scroll and stop always at the center of a page
CGRect proposedRect = CGRectMake(proposedContentOffset.x+self.collectionView.bounds.size.width/2 - [self sectionWidth:0]/2, 0, [self sectionWidth:0], self.collectionView.bounds.size.height);
NSMutableArray <__kindof UICollectionViewLayoutAttributes *> *allAttributes = [[self layoutAttributesForElementsInRect:proposedRect] mutableCopy];
__block UICollectionViewLayoutAttributes *proposedAttributes = nil;
__block CGFloat minDistance = CGFLOAT_MAX;
[allAttributes enumerateObjectsUsingBlock:^(__kindof UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
CGFloat distance = CGRectGetMidX(proposedRect) - obj.center.x;
if (ABS(distance) < minDistance) {
proposedAttributes = obj;
minDistance = distance;
}
}];
// Scroll always
if (self.currentPage == proposedAttributes.indexPath.section) {
if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfSections]-1) self.currentPage += 1;
else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;
}
else {
self.currentPage = proposedAttributes.indexPath.section;
}
return [self offsetAtCurrentPage];
}
【讨论】: