iOS 手把手搭建 · 无限循环滚动视图

3,750 阅读4分钟
原文链接: www.jianshu.com

原文:iOS手把手搭建·无限循环滚动视图

在大多数常规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:)实现。
该方法包含以下三个参数:

  • indexPathCollectionView滚动到的位置。
  • 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动画参数为关闭。