【发布时间】:2016-03-27 13:15:59
【问题描述】:
我想实现水平无限滚动的UICollectionView?
【问题讨论】:
-
使用 scrollView 实现了这一目标 - stackoverflow.com/a/52275472/4776634
标签: ios swift uiscrollview uicollectionview uiscrollviewdelegate
我想实现水平无限滚动的UICollectionView?
【问题讨论】:
标签: ios swift uiscrollview uicollectionview uiscrollviewdelegate
如果你的数据是静态的并且你想要一种循环行为,你可以这样做:
var dataSource = ["item 0", "item 1", "item 2"]
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return Int.max // instead of returnin dataSource.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let itemToShow = dataSource[indexPath.row % dataSource.count]
let cell = UICollectionViewCell() // setup cell with your item and return
return cell
}
基本上,您对您的集合视图说您有大量单元格(Int.max 不会是无限的,但可能会起作用),并且您使用 % 运算符访问您的数据源。在我的示例中,我们将得到“item 0”、“item 1”、“item 2”、“item 0”、“item 1”、“item 2”......
我希望这会有所帮助:)
【讨论】:
Int.max 会使模拟器崩溃:(
heightForRowAtIndexPath 或 @987654324 中计算它们的大小/高度,则单元格重用机制只会解决内存问题@ ,这些方法将被调用的次数与您返回的 numberOf .. InSection 一样多。我猜这可能会导致性能问题?即使我做了一些缓存,这里仍然会有数百万次不必要的调用。
UITableView 和UICollectionView 是UIScrollView 的子类,系统将在初始化滚动视图以确定其contentSize 时计算每个单元格的大小,我尚未在实际应用程序上测试此方法,只是担心我可能会提出性能问题,很高兴知道您已经实现了它并且工作正常。
显然,Manikanta Adimulam 提出了最接近好的解决方案。最干净的解决方案是将最后一个元素添加到数据列表的开头,并将第一个元素添加到最后一个数据列表位置(例如:[4] [0] [1] [2] [3] [4] [ 0]),所以我们在触发最后一个列表项时滚动到第一个数组项,反之亦然。这适用于具有一个可见项目的集合视图:
重写 UICollectionViewDelegate 并重写以下方法:
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let numberOfCells = items.count
let page = Int(scrollView.contentOffset.x) / Int(cellWidth)
if page == 0 { // we are within the fake last, so delegate real last
currentPage = numberOfCells - 1
} else if page == numberOfCells - 1 { // we are within the fake first, so delegate the real first
currentPage = 0
} else { // real page is always fake minus one
currentPage = page - 1
}
// if you need to know changed position, you can delegate it
customDelegate?.pageChanged(currentPage)
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
let numberOfCells = items.count
if numberOfCells == 1 {
return
}
let regularContentOffset = cellWidth * CGFloat(numberOfCells - 2)
if (scrollView.contentOffset.x >= cellWidth * CGFloat(numberOfCells - 1)) {
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x - regularContentOffset, y: 0.0)
} else if (scrollView.contentOffset.x < cellWidth) {
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x + regularContentOffset, y: 0.0)
}
}
覆盖 UICollectionView 中的 layoutSubviews() 方法,以便始终为第一项设置正确的偏移量:
override func layoutSubviews() {
super.layoutSubviews()
let numberOfCells = items.count
if numberOfCells > 1 {
if contentOffset.x == 0.0 {
contentOffset = CGPoint(x: cellWidth, y: 0.0)
}
}
}
覆盖 init 方法并计算您的单元格尺寸:
let layout = self.collectionViewLayout as! UICollectionViewFlowLayout
cellPadding = layout.minimumInteritemSpacing
cellWidth = layout.itemSize.width
像魅力一样工作! 如果您想通过具有多个可见项的集合视图来实现此效果,请使用发布在here 的解决方案。
【讨论】:
我在UICollectionView 中实现了无限滚动。使代码在 github 中可用。你可以试一试。它在 swift 3.0 中。
您可以使用 pod 添加它。用法很简单。只需初始化InfiniteScrollingBehaviour,如下所示。
infiniteScrollingBehaviour = InfiniteScrollingBehaviour(withCollectionView: collectionView, andData: Card.dummyCards, delegate: self)
并实现所需的委托方法以返回配置的UICollectionViewCell。示例实现如下所示:
func configuredCell(forItemAtIndexPath indexPath: IndexPath, originalIndex: Int, andData data: InfiniteScollingData, forInfiniteScrollingBehaviour behaviour: InfiniteScrollingBehaviour) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CellID", for: indexPath)
if let collectionCell = cell as? CollectionViewCell,
let card = data as? Card {
collectionCell.titleLabel.text = card.name
}
return cell
}
它将在您的原始数据集中添加适当的前导和尾随边界元素,并将调整 collectionView 的contentOffset。
在回调方法中,它会为您提供原始数据集中某个项目的索引。
【讨论】:
测试代码
我通过简单地重复单元x次来实现这一点。如下,
声明你想要多少个循环
let x = 50
实现numberOfItems
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return myArray.count*x // large scrolling: lets see who can reach the end :p
}
添加这个实用函数来计算arrayIndex给定一个indexPath行
func arrayIndexForRow(_ row : Int)-> Int {
return row % myArray.count
}
实现cellForItem
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myIdentifier", for: indexPath) as! MyCustomCell
let arrayIndex = arrayIndexForRow(indexPath.row)
let modelObject = myArray[arrayIndex]
// configure cell
return cell
}
添加实用函数以在给定索引处滚动到collectionView 的中间
func scrollToMiddle(atIndex: Int, animated: Bool = true) {
let middleIndex = atIndex + x*yourArray.count/2
collectionView.scrollToItem(at: IndexPath(item: middleIndex, section: 0), at: .centeredHorizontally, animated: animated)
}
【讨论】:
还暗示您的数据是静态的,并且您的所有 UICollectionView 单元格应该具有相同的大小,我发现 this promising solution。
您可以在github 下载示例项目并自己运行该项目。 ViewController 中创建UICollectionView 的代码非常简单。
您基本上遵循以下步骤:
InfiniteCollectionView
infiniteDataSource和infiniteDelegate
【讨论】:
对于那些正在寻找无限和水平滚动的集合视图(其数据源附加到末尾)的用户,请在 scrollViewDidScroll 中附加到您的数据源并在您的集合视图上调用 reloadData()。它将保持滚动偏移量。
下面的示例代码。我将我的集合视图用于分页日期选择器,当用户靠近右端(倒数第二个)时,我会在其中加载更多页面(整个月):
func scrollViewDidScroll(scrollView: UIScrollView) {
let currentPage = self.customView.collectionView.contentOffset.x / self.customView.collectionView.bounds.size.width
if currentPage > CGFloat(self.months.count - 2) {
let nextMonths = self.generateMonthsFromDate(self.months[self.months.count - 1], forPageDirection: .Next)
self.months.appendContentsOf(nextMonths)
self.customView.collectionView.reloadData()
}
// DOESN'T WORK - adding more months to the left
// if currentPage < 2 {
// let previousMonths = self.generateMonthsFromDate(self.months[0], forPageDirection: .Previous)
// self.months.insertContentsOf(previousMonths, at: 0)
// self.customView.collectionView.reloadData()
// }
}
编辑: - 当您在数据源的开头插入时,这似乎不起作用。
【讨论】:
如果cell.width == collectionView.width,这个解决方案对我有用:
首先,你需要你的items * 2:
func set(items colors: [UIColor]) {
items = colors + colors
}
然后添加这两个计算变量以确定索引:
var firstCellIndex: Int {
var targetItem = items.count / 2 + 1
if !isFirstCellSeen {
targetItem -= 1
isFirstCellSeen = true
}
return targetItem
}
var lastCellIndex: Int {
items.count / 2 - 2
}
如您所见,firstCellIndex 有一个标志 isFirstCellSeen。 CV第一次出现时需要这个标志,否则会显示items[1]而不是items[0]。所以不要忘记将该标志添加到您的代码中。
主要逻辑发生在这里:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item == 0 {
scroll(to: firstCellIndex)
} else if indexPath.item == items.count - 1 {
scroll(to: lastCellIndex)
}
}
private func scroll(to row: Int) {
DispatchQueue.main.async {
self.collectionView.scrollToItem(
at: IndexPath(row: row, section: 0),
at: .centeredHorizontally,
animated: false
)
}
}
就是这样。集合视图滚动现在应该是无限的。我喜欢这个解决方案,因为它不需要任何额外的 pod,而且很容易理解:你只需将 cv 项乘以 2,然后在 indexPath == 0 或 indexPath == lastItem 时总是滚动到中间
【讨论】:
要应用此无限循环功能,您应该有正确的 collectionView 布局
您需要在最后添加数组的第一个元素,并首先添加数组的最后一个元素
例如:- 数组 = [1,2,3,4]
呈现数组 = [4,1,2,3,4,1]
func infinateLoop(scrollView: UIScrollView) {
var index = Int((scrollView.contentOffset.x)/(scrollView.frame.width))
guard currentIndex != index else {
return
}
currentIndex = index
if index <= 0 {
index = images.count - 1
scrollView.setContentOffset(CGPoint(x: (scrollView.frame.width+60) * CGFloat(images.count), y: 0), animated: false)
} else if index >= images.count + 1 {
index = 0
scrollView.setContentOffset(CGPoint(x: (scrollView.frame.width), y: 0), animated: false)
} else {
index -= 1
}
pageController.currentPage = index
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
infinateLoop(scrollView: scrollView)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
infinateLoop(scrollView: scrollView)
}
【讨论】:
此处提供的答案有助于实现该功能。但在我看来,它们包含一些可以避免的低级更新(设置内容偏移量、操作数据源......)。如果您仍然不满意并寻找不同的方法,这就是我所做的。
主要思想是每当您到达最后一个单元格之前的单元格时更新单元格的数量。每次将项目数增加 1 时,都会产生无限滚动的错觉。为此,我们可以利用scrollViewDidEndDecelerating(_ scrollView: UIScrollView) 函数检测用户何时完成滚动,然后更新集合视图中的项目数。这是实现此目的的代码 sn-p:
class InfiniteCarouselView: UICollectionView {
var data: [Any] = []
private var currentIndex: Int?
private var currentMaxItemsCount: Int = 0
// Set up data source and delegate
}
extension InfiniteCarouselView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// Set the current maximum to a number above the maximum count by 1
currentMaxItemsCount = max(((currentIndex ?? 0) + 1), data.count) + 1
return currentMaxItemsCount
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
let row = indexPath.row % data.count
let item = data[row]
// Setup cell
return cell
}
}
extension InfiniteCarouselView: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
}
// Detect when the collection view has finished scrolling to increase the number of items in the collection view
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// Get the current index. Note that the current index calculation will keep changing because the collection view is expanding its content size based on the number of items (currentMaxItemsCount)
currentIndex = Int(scrollView.contentOffset.x/scrollView.contentSize.width * CGFloat(currentMaxItemsCount))
// Reload the collection view to get the new number of items
reloadData()
}
}
【讨论】: