在大多数常规App开发当中,我们都会有产品运营栏的需求,也就是列表页或者产品顶部,又或者整个页面需要展示几个滚动的运营活动、产品、广告什么的,当然,也可能是一个自己实现的一个图片浏览器。
在早些年,这类需求大多都是从First逐个滚动到Last,然后再自动滚到First,技术上无非都是通过UIScrollView + Timer的方案,iOS开发往往都喜欢专注于用(xuan)户(ji)体(zhuang)验(bi)的,所以后来出现了无限循环滚动的体验。
得益于iOS6以后出现的UICollectionView控件,无论是滚动视图,还是做图片浏览,都降低了很多难度和代码量,但是它为了灵活性,官方没有做无限滚动Api,那么今天,我们就用UICollectionView来实现无限循环滚动视图。
这里使用UICollectionView管理Cell方式来减少代码量和复用Cell的内存优化,通过关闭
scrollToItem(at:at:animated:)
滚动动画来让用户无法发觉是代码在控制滚动,让用户产生错觉变成无限循环。
我们假设视图是在水平滚动,Cell是横屏全部宽度填充,然后设置paging
属性为true
以便滚动到边缘从而获得更好的体验。
揣测
原理:这么做依赖于有操作表的概念,这样我们就可以在收尾添加元素。好比如,你有一个包含三个项目的数组,想要他们无限循环的滚动,那就把首位元素拷贝插入到末尾,同时末尾元素拷贝一份插入到首部。演示如下:
OK,我们直接上代码:
private func setupDataForCollectionView() {
let originalItems = ["One", "Two", "Three"]
if let firstItem = originalItems.first, let lastItem = originalItems.last {
var workItems = originalItems
workItems.insert(lastItem, at: 0)
workItems.append(firstItem)
items = workItems
}
}
那么我们得到的items
的内部结构就是这样:
["Three", "One", "Two", "Three", "One"]
结构上就和假想图一致。
臆测
这个过程依赖于在首尾的indexPath
需要关闭动画来实现,通过方法scrollToItem(at:at:animated:)
实现。
该方法包含以下三个参数:
-
indexPath
为CollectionView滚动到的位置。 -
UICollectionViewScrollPosition
来控制CollectionView应该滚动到什么位置。 -
animated
这个布尔值控制是否展示动画。
UICollectionViewScrollPosition
控制滚动位置,假设CollectionView被设置了分页,如果是水平视图,我们希望它滚到左边那就为UICollectionViewScrollPosition.left
,如果是垂直视图,希望它滚到顶部那就是UICollectionViewScrollPosition.top
。
下面我们来看看是演示情况:
如果是往前滚的话就正好相反:
实施
我们实现的关键技术点就是检测用户的滚动意图,这样才能触发scrollToItem(at:at:animated:)
方法来实现我们的目的。
为了能做到这一点,我们需要实现UICollectionView
的父类的UIScrollView
的代理方法scrollViewDidEndDecelerating
来检测滚动停止信号。
再通过检测contentOffset
属性来判断具体位置。
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let contentOffsetX = scrollView.contentOffset.x
let contentOffsetWhenFullyScrolledRight = (collectionView?.frame.width)! * CGFloat(items.count - 1)
if contentOffsetX == contentOffsetWhenFullyScrolledRight {
let indexPath = IndexPath.init(item: 1, section: 0)
collectionView?.scrollToItem(at: indexPath, at: .left, animated: false)
} else if contentOffsetX == 0 {
let indexPath = IndexPath.init(item: (items.count - 2), section: 0)
collectionView?.scrollToItem(at: indexPath, at: .left, animated: false)
}
}
OK,我们来看下视图结构和对应的索引结构:
视图结构能清晰的解答:
- 如果在我们滚到最右边,所看到的元素为我们拷贝的第一个元素,那么就应该调用
scrollToItem(at:at:animated:)
方法来滚动到实际上的第一个元素位置,也就是索引[0, 1]的位置。 - 如果我们滚动到最左边,所看到的元素为我们拷贝的最后一个元素,那么就应该调用
scrollToItem(at:at:animated:)
方法来滚动到实际上的最后一个元素位置,也就是索引[0, 3]的位置,也就是处理位置里的items.count - 2
位置。
总结
创建一个无限循环的滚动视图其实So Easy,也就五个步骤:
- 根据实际数据,填充收尾的假数据。
- 检查滚动视图滚动停止时的偏移位置。
- 如果滚动到最末尾,则移动到
填充过的数据项
中的第二项。 - 如果滚动到最首位,则移动到
填充过的数据项
中的最后一个数据项。 - 保证方法
scrollToItem:
里的animated
动画参数为关闭。