ios 按单元格分页 UICollectionView,而不是屏幕
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/22895465/
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
Paging UICollectionView by cells, not screen
提问by Martin Koles
I have UICollectionView
with horizontal scrolling and there are always 2 cells side-by-side per the entire screen. I need the scrolling to stop at the begining of a cell. With paging enabled, the collection view scrolls the whole page, which is 2 cells at once, and then it stops.
我有UICollectionView
水平滚动,并且每个屏幕总是并排有 2 个单元格。我需要滚动停止在单元格的开头。启用分页后,集合视图会滚动整个页面,即一次 2 个单元格,然后停止。
I need to enable scrolling by a single cell, or scrolling by multiple cells with stopping at the edge of the cell.
我需要启用单个单元格滚动,或者在单元格边缘停止滚动多个单元格。
I tried to subclass UICollectionViewFlowLayout
and to implement the method targetContentOffsetForProposedContentOffset
, but so far I was only able to break my collection view and it stopped scrolling. Is there any easier way to achieve this and how, or do I really need to implement all methods of UICollectionViewFlowLayout
subclass? Thanks.
我试图子类化UICollectionViewFlowLayout
并实现该方法targetContentOffsetForProposedContentOffset
,但到目前为止我只能打破我的集合视图并且它停止滚动。有没有更简单的方法来实现这一点以及如何实现,或者我真的需要实现UICollectionViewFlowLayout
子类的所有方法吗?谢谢。
采纳答案by Martin Koles
OK, so I found the solution here: targetContentOffsetForProposedContentOffset:withScrollingVelocity without subclassing UICollectionViewFlowLayout
好的,所以我在这里找到了解决方案:targetContentOffsetForProposedContentOffset:withScrollingVelocity 没有子类化 UICollectionViewFlowLayout
I should have searched for targetContentOffsetForProposedContentOffset
in the begining.
我应该targetContentOffsetForProposedContentOffset
在一开始就搜索。
回答by evya
just override the method:
只需覆盖该方法:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
*targetContentOffset = scrollView.contentOffset; // set acceleration to 0.0
float pageWidth = (float)self.articlesCollectionView.bounds.size.width;
int minSpace = 10;
int cellToSwipe = (scrollView.contentOffset.x)/(pageWidth + minSpace) + 0.5; // cell width + min spacing for lines
if (cellToSwipe < 0) {
cellToSwipe = 0;
} else if (cellToSwipe >= self.articles.count) {
cellToSwipe = self.articles.count - 1;
}
[self.articlesCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:cellToSwipe inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}
回答by fredpi
Horizontal Paging With Custom Page Width (Swift 4 & 5)
具有自定义页面宽度的水平分页(Swift 4 & 5)
Many solutions presented here result in some weird behaviour that doesn't feel like properly implemented paging.
这里介绍的许多解决方案会导致一些奇怪的行为,感觉不像是正确实现的分页。
The solution presented in this tutorial, however, doesn't seem to have any issues. It just feels like a perfectly working paging algorithm. You can implement it in 5 simple steps:
但是,本教程中提供的解决方案似乎没有任何问题。感觉就像一个完美的分页算法。您可以通过 5 个简单的步骤来实现它:
- Add the following property to your type:
private var indexOfCellBeforeDragging = 0
- Set the
collectionView
delegate
like this:collectionView.delegate = self
- Add conformance to
UICollectionViewDelegate
via an extension:extension YourType: UICollectionViewDelegate { }
Add the following method to the extension implementing the
UICollectionViewDelegate
conformance and set a value forpageWidth
:func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { let pageWidth = // The width your page should have (plus a possible margin) let proportionalOffset = collectionView.contentOffset.x / pageWidth indexOfCellBeforeDragging = Int(round(proportionalOffset)) }
Add the following method to the extension implementing the
UICollectionViewDelegate
conformance, set the same value forpageWidth
(you may also store this value at a central place) and set a value forcollectionViewItemCount
:func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { // Stop scrolling targetContentOffset.pointee = scrollView.contentOffset // Calculate conditions let pageWidth = // The width your page should have (plus a possible margin) let collectionViewItemCount = // The number of items in this section let proportionalOffset = collectionView.contentOffset.x / pageWidth let indexOfMajorCell = Int(round(proportionalOffset)) let swipeVelocityThreshold: CGFloat = 0.5 let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < collectionViewItemCount && velocity.x > swipeVelocityThreshold let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell) if didUseSwipeToSkipCell { // Animate so that swipe is just continued let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1) let toValue = pageWidth * CGFloat(snapToIndex) UIView.animate( withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: { scrollView.contentOffset = CGPoint(x: toValue, y: 0) scrollView.layoutIfNeeded() }, completion: nil ) } else { // Pop back (against velocity) let indexPath = IndexPath(row: indexOfMajorCell, section: 0) collectionView.scrollToItem(at: indexPath, at: .left, animated: true) } }
- 将以下属性添加到您的类型:
private var indexOfCellBeforeDragging = 0
collectionView
delegate
像这样设置:collectionView.delegate = self
UICollectionViewDelegate
通过扩展添加一致性:extension YourType: UICollectionViewDelegate { }
将以下方法添加到实现
UICollectionViewDelegate
一致性的扩展中,并为 设置一个值pageWidth
:func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { let pageWidth = // The width your page should have (plus a possible margin) let proportionalOffset = collectionView.contentOffset.x / pageWidth indexOfCellBeforeDragging = Int(round(proportionalOffset)) }
将以下方法添加到实现
UICollectionViewDelegate
一致性的扩展中,为 设置相同的值pageWidth
(您也可以将此值存储在中央位置)并为 设置一个值collectionViewItemCount
:func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { // Stop scrolling targetContentOffset.pointee = scrollView.contentOffset // Calculate conditions let pageWidth = // The width your page should have (plus a possible margin) let collectionViewItemCount = // The number of items in this section let proportionalOffset = collectionView.contentOffset.x / pageWidth let indexOfMajorCell = Int(round(proportionalOffset)) let swipeVelocityThreshold: CGFloat = 0.5 let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < collectionViewItemCount && velocity.x > swipeVelocityThreshold let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell) if didUseSwipeToSkipCell { // Animate so that swipe is just continued let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1) let toValue = pageWidth * CGFloat(snapToIndex) UIView.animate( withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: { scrollView.contentOffset = CGPoint(x: toValue, y: 0) scrollView.layoutIfNeeded() }, completion: nil ) } else { // Pop back (against velocity) let indexPath = IndexPath(row: indexOfMajorCell, section: 0) collectionView.scrollToItem(at: indexPath, at: .left, animated: true) } }
回答by StevenOjo
Swift 3 version of Evya's answer:
Evya 答案的 Swift 3 版本:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
let pageWidth:Float = Float(self.view.bounds.width)
let minSpace:Float = 10.0
var cellToSwipe:Double = Double(Float((scrollView.contentOffset.x))/Float((pageWidth+minSpace))) + Double(0.5)
if cellToSwipe < 0 {
cellToSwipe = 0
} else if cellToSwipe >= Double(self.articles.count) {
cellToSwipe = Double(self.articles.count) - Double(1)
}
let indexPath:IndexPath = IndexPath(row: Int(cellToSwipe), section:0)
self.collectionView.scrollToItem(at:indexPath, at: UICollectionViewScrollPosition.left, animated: true)
}
回答by Romulo BM
Here's the easiest way that i found to do that in Swift 4.2for horinzontalscroll:
这是我在Swift 4.2 中发现的用于水平滚动的最简单方法:
I'm using the first cell on visibleCells
and scrolling to then, if the first visible cell are showing less of the half of it's width i'm scrolling to the next one.
我正在使用第一个单元格visibleCells
并滚动到然后,如果第一个可见单元格显示的宽度小于其宽度的一半,我将滚动到下一个单元格。
If your collection scroll vertically, simply change x
by y
and width
by height
如果您收藏滚动垂直,只需更改x
通过y
和width
通过height
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
var indexes = self.collectionView.indexPathsForVisibleItems
indexes.sort()
var index = indexes.first!
let cell = self.collectionView.cellForItem(at: index)!
let position = self.collectionView.contentOffset.x - cell.frame.origin.x
if position > cell.frame.size.width/2{
index.row = index.row+1
}
self.collectionView.scrollToItem(at: index, at: .left, animated: true )
}
回答by John Cido
Partly based on StevenOjo's answer. I've tested this using a horizontal scrolling and no Bounce UICollectionView. cellSize is CollectionViewCell size. You can tweak factor to modify scrolling sensitivity.
部分基于 StevenOjo 的回答。我已经使用水平滚动和没有 Bounce UICollectionView 对此进行了测试。cellSize 是 CollectionViewCell 大小。您可以调整因子来修改滚动灵敏度。
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
var factor: CGFloat = 0.5
if velocity.x < 0 {
factor = -factor
}
let indexPath = IndexPath(row: (scrollView.contentOffset.x/cellSize.width + factor).int, section: 0)
collectionView?.scrollToItem(at: indexPath, at: .left, animated: true)
}
回答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 user1046037
Approach 1: Collection View
方法 1:集合视图
flowLayout
is UICollectionViewFlowLayout
property
flowLayout
是UICollectionViewFlowLayout
财产
override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if let collectionView = collectionView {
targetContentOffset.memory = scrollView.contentOffset
let pageWidth = CGRectGetWidth(scrollView.frame) + flowLayout.minimumInteritemSpacing
var assistanceOffset : CGFloat = pageWidth / 3.0
if velocity.x < 0 {
assistanceOffset = -assistanceOffset
}
let assistedScrollPosition = (scrollView.contentOffset.x + assistanceOffset) / pageWidth
var targetIndex = Int(round(assistedScrollPosition))
if targetIndex < 0 {
targetIndex = 0
}
else if targetIndex >= collectionView.numberOfItemsInSection(0) {
targetIndex = collectionView.numberOfItemsInSection(0) - 1
}
print("targetIndex = \(targetIndex)")
let indexPath = NSIndexPath(forItem: targetIndex, inSection: 0)
collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Left, animated: true)
}
}
Approach 2: Page View Controller
方法 2:页面视图控制器
You could use UIPageViewController
if it meets your requirements, each page would have a separate view controller.
UIPageViewController
如果它满足您的要求,您可以使用,每个页面都有一个单独的视图控制器。
回答by Moose
This is a straight way to do this.
这是执行此操作的直接方法。
The case is simple, but finally quite common ( typical thumbnails scroller with fixed cell size and fixed gap between cells )
案例很简单,但最终很常见(典型的缩略图滚动条具有固定的单元格大小和固定的单元格之间的间隙)
var itemCellSize: CGSize = <your cell size>
var itemCellsGap: CGFloat = <gap in between>
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageWidth = (itemCellSize.width + itemCellsGap)
let itemIndex = (targetContentOffset.pointee.x) / pageWidth
targetContentOffset.pointee.x = round(itemIndex) * pageWidth - (itemCellsGap / 2)
}
// CollectionViewFlowLayoutDelegate
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return itemCellSize
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return itemCellsGap
}
Note that there is no reason to call a scrollToOffset or dive into layouts. The native scrolling behaviour already does everything.
请注意,没有理由调用 scrollToOffset 或深入布局。本机滚动行为已经完成了一切。
Cheers All :)
干杯 :)
回答by skensell
Kind of like evya's answer, but a little smoother because it doesn't set the targetContentOffset to zero.
有点像 evya 的回答,但更流畅一点,因为它没有将 targetContentOffset 设置为零。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if ([scrollView isKindOfClass:[UICollectionView class]]) {
UICollectionView* collectionView = (UICollectionView*)scrollView;
if ([collectionView.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]]) {
UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout;
CGFloat pageWidth = layout.itemSize.width + layout.minimumInteritemSpacing;
CGFloat usualSideOverhang = (scrollView.bounds.size.width - pageWidth)/2.0;
// k*pageWidth - usualSideOverhang = contentOffset for page at index k if k >= 1, 0 if k = 0
// -> (contentOffset + usualSideOverhang)/pageWidth = k at page stops
NSInteger targetPage = 0;
CGFloat currentOffsetInPages = (scrollView.contentOffset.x + usualSideOverhang)/pageWidth;
targetPage = velocity.x < 0 ? floor(currentOffsetInPages) : ceil(currentOffsetInPages);
targetPage = MAX(0,MIN(self.projects.count - 1,targetPage));
*targetContentOffset = CGPointMake(MAX(targetPage*pageWidth - usualSideOverhang,0), 0);
}
}
}