ios 带分页的 UICollectionView - 设置页面宽度
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/20496850/
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
UICollectionView with paging - setting page width
提问by Guilherme
I have a Collection View that can show about 3.5 cells at a time, and I want it to be paging-enabled. But I'd like it to snap to each cell (just like the App Store app does), and not scroll the full width of the view. How can I do that?
我有一个可以一次显示大约 3.5 个单元格的集合视图,我希望它能够分页。但我希望它对齐到每个单元格(就像 App Store 应用程序那样),而不是滚动视图的整个宽度。我怎样才能做到这一点?
采纳答案by Jesse Rusak
You can snap to cells by being the delegate of the collection view and implementing the method:
您可以通过成为集合视图的委托并实现该方法来捕捉单元格:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
This tells you that the user has finished a drag and it allows you to modify the targetContentOffset
to align with your cells (i.e. round to the nearest cell). Note that you need to be careful about how you modify the targetContentOffset; in particular, you need to avoid changing it so that the view needs to scroll in the opposite direction of the passed velocity, or you'll get animation glitches. You can probably find many examples of this if you google for that method name.
这告诉您用户已完成拖动并允许您修改targetContentOffset
以与您的单元格对齐(即舍入到最近的单元格)。请注意,您需要注意如何修改 targetContentOffset;特别是,您需要避免更改它,以便视图需要在传递速度的相反方向上滚动,否则您将获得动画故障。如果你用谷歌搜索那个方法名称,你可能会找到很多这样的例子。
回答by Mike M
Another way is to create a custom UICollectionViewFlowLayout and override the method like so:
另一种方法是创建一个自定义 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);
}
If you are looking for a Swiftsolution, check out this Gist
如果您正在寻找Swift解决方案,请查看此Gist
- note: this will work when we are really showing preview cells (even if they have an alpha of 0.0f). This is because if the preview cells are not available at the time of scrolling their attributes object will not be passed in the loop...
- 注意:当我们真正显示预览单元格时(即使它们的 alpha 为 0.0f),这将起作用。这是因为如果在滚动时预览单元格不可用,它们的属性对象将不会在循环中传递......
回答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
itemSize
actually matches the size of the item as that's often a problem, especially when usingcollectionView(_: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 Filip Juncu
I developed my solution before looking at the ones here. I also went with creating a custom UICollectionViewFlowLayout and override the targetContentOffset method.
在查看这里的解决方案之前,我开发了我的解决方案。我还创建了一个自定义 UICollectionViewFlowLayout 并覆盖了 targetContentOffset 方法。
It seems to work fine for me (i.e. I get the same behavior as in the AppStore) even though I got much less code. Here it is, feel free to point me any drawback you can think of:
即使我的代码少得多,它似乎对我来说工作正常(即我得到与 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)
}
回答by lopes710
The solution Mike M. presented in the post before worked for me but in my case I wanted to have the first cell starting in the middle of the collectionView. So I used the collection flow delegate method to defined an inset (collectionView:layout:insetForSectionAtIndex:
). This made the scroll between the first cell and second to be stuck and not scroll correctly to the first cell.
之前在帖子中提出的解决方案 Mike M. 为我工作,但在我的情况下,我希望第一个单元格从 collectionView 的中间开始。所以我使用了集合流委托方法来定义一个 inset( collectionView:layout:insetForSectionAtIndex:
)。这使得第一个单元格和第二个单元格之间的滚动被卡住并且无法正确滚动到第一个单元格。
The reason for this was that candidateAttributes.center.x - halfWidth
was having a negative value. The solution was to get the absolute value so I add fabs to this line return CGPointMake(fabs(candidateAttributes.center.x - halfWidth), offset.y);
这样做的原因是它candidateAttributes.center.x - halfWidth
具有负值。解决方案是获得绝对值,所以我在这条线上添加了晶圆厂return CGPointMake(fabs(candidateAttributes.center.x - halfWidth), offset.y);
Fabs should be added by default to cover all situations.
默认情况下应添加 Fab 以涵盖所有情况。
回答by Javi
This is my solution. Works with any page width.
这是我的解决方案。适用于任何页面宽度。
Set self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast
to feel a real paging.
设置self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast
感受真正的分页。
The solution is based on one section to scroll paginated by items.
该解决方案基于一个部分来滚动按项目分页。
- (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];
}
This is paginated by sections.
这是按部分分页的。
- (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];
}