iOS 最新Swift+UICollectionView实现图片无限轮播器

Bg:
图片轮播器数不胜数,但大多是UIScrollView + OC实现的,心血来潮,决定用Swift+UICollectionView造个轮子玩玩HHScrollView:https://github.com/wanghhh/HHScrollView#hhscrollview
先看下效果图:

Untitled.gif

功能实现:

1、Swift+UICollectionView实现自动无限轮播,可手动拖动
2、页码显示,可以自定义页码指示器位置、颜色
3、轮播间隔时间等属性设置

轮播器调用方法:
下载demo,直接将HHScrollView.swift文件拖进自己项目即可。
然后在控制器的viewDidLoad() 中实例化:

    //准备图片数据,就是图片url字符串
    imageDataSource = loadImages()
    
    //提供两种实例化方法:
    //1.通过frame和imageUrls
    //let scrollView = HHScrollView.init(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 200), imageUrls: imageDataSource)
    
    //2.通过frame,后根据网络数据设置imgUrls
    let scrollView = HHScrollView.init(frame: CGRect.init(x: 0, y: 64, width: UIScreen.main.bounds.width, height: 200))
    //设置数据源(图片urlStr)******
    //加载本地图片
    //scrollView.isFromNet = false
    //scrollView.imgUrls = ["ic_banner01","ic_banner02","ic_banner03"]
    //默认加载网络图片
    scrollView.imgUrls = imageDataSource
    //设置代理,根据需要要不要监听图片点击
    scrollView.hhScrollViewDelegae = self

HHScrollView提供的属性:

    //代理
    weak var hhScrollViewDelegae:HHScrollViewDelegate?
    //分页指示器页码颜色
    var pageControlColor:UIColor?
    //分页指示器当前页颜色
    var currentPageControlColor:UIColor?
    //分页指示器位置
    var pageControlPoint:CGPoint?
    //分页指示器
    fileprivate var pageControl:UIPageControl?
   //自动滚动时间默认为3.0
   var autoScrollDelay:TimeInterval = 3 {
    didSet{
        removeTimer()
        setUpTimer()
    }
   } 
   //图片是否来自网络,默认是
   var isFromNet:Bool = true
   //占位图
   var placeholderImage:String = "ic_place"
   //设置图片资源url字符串。
   var imgUrls = NSArray(){
      didSet{
          pageControl?.numberOfPages = imgUrls.count
          itemCount = imgUrls.count
          self.reloadData()
      }  
   }
   fileprivate var itemCount:NSInteger = 0//cellNum
   fileprivate var timer:Timer?//定时器

可以通过以上属性和自身项目需要自定义轮播器的样式、滚动时间间隔等,这些基本属性都有默认值。

HHScrollView提供的便利构造器:

//便利构造方法
convenience init(frame:CGRect) {
    self.init(frame: frame, collectionViewLayout: HHCollectionViewFlowLayout.init())
}

convenience init(frame:CGRect,imageUrls:NSArray) {
    self.init(frame: frame, collectionViewLayout: HHCollectionViewFlowLayout.init())
     imgUrls = imageUrls
}

基本原理:

充分利用UICollectionView的cell的复用机制,不用自己再去考虑imageView的复用问题,节省内存,有利于性能提升。

先说下大致思路:

我们知道UICollectionView继承自UIScrollView,也就是说UIScrollView的基本属性方法UICollectionView都有,那么UICollectionView也可以分页显示。将item(UITableView对应的cell)的宽和高分别设置成UICollectionView自身的宽和高,数据源返回的item个数就是参与图片的图片个数,那么问题就在于当滚动到最后一张或第一张图片的时候,怎么继续滚动呢?

为了解决这个问题,我们可以通过扩大item的个数的方法解决它,无限轮播的关键就在于此:

1.将数据源方法返回的item个数设置未imgUrls.count(imgUrls是网络图片url或本地图片的数组)的2倍,在collectionView加载完成后默认滚动到索引为imgUrls.count的位置,这样cell就可以向左或右滚动了。

例如:我们想加载3张图片,那么collectionView:初始位置应该在"图片1-2"的位置,如下图:

QQ20170820-2@2x.png

2.当collectionView滚动到最后一张的时候,即滚到"图片3-2"的位置时,让collectionView回到"图片3-1"的位置,这样就可以继续向右滚动了。同理,当collectionView滚动到第一张的时候,即滚到"图片1-1"的位置时,让collectionView回到"图片1-2"的位置,这样就可以继续向左滚动了。如下图:

QQ20170820-1@2x.png

以上就是无限轮播的基本实现原理了。
关键代码:

1.collectionView初始位置设置:

    //在collectionView加载完成后默认滚动到索引为imgUrls.count的位置,这样cell就可以向左或右滚动
    DispatchQueue.main.async {
        //注意:在轮播器视图添加到控制器的view上以后,这样是为了将分页指示器添加到self.superview上(如果将分页指示器直接添加到collectionView上的话,指示器将不能正常显示)
        self.setUpPageControl()
        let indexpath = NSIndexPath.init(row: self.imgUrls.count, section: 0)
        //滚动位置
        self.scrollToItem(at: indexpath as IndexPath, at: .left, animated: false)
    }

此段代码写在collectionView的init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout)方法中,关键在于要等到在collectionView加载完成以后,再去改变滚动的位置,这里利用DispatchQueue.main.async异步实现。本质就是利用主队列调度任务的阻塞特性实现,因为主队列只会在主线程"闲暇"的时候才去执行别的任务,这里"闲暇"就是指collectionView加载完成以后。
2.UIPageControl的加载时机和方式

要想将页码显示器封装到轮播器中,而不是在使用轮播器的控制器中创建和加载,做到更好的封装,也将setUpPageControl的创建页码器的代码放在init()方法的主队列异步方法中去,在上面代码中可以看到self.setUpPageControl()。创建代码如下:

@objc private func setUpPageControl(){
    pageControl = UIPageControl.init()
    pageControl?.frame = (pageControlPoint != nil) ? CGRect.init(x: (pageControlPoint?.x)!, y: (pageControlPoint?.y)!, width: self.bounds.size.width - (pageControlPoint?.x)!, height: 8) : CGRect.init(x: 0, y: self.frame.maxY - 16, width: self.bounds.size.width, height: 8)
    pageControl?.pageIndicatorTintColor = pageControlColor ?? UIColor.lightGray
    pageControl?.currentPageIndicatorTintColor = currentPageControlColor ?? UIColor.orange
    pageControl?.numberOfPages = imgUrls.count
    pageControl?.currentPage = 0

    //一定要将指示器添加到superview上
    self.superview?.addSubview(pageControl!)
}

另外发现将UIPageControl直接add到collectionView上时不能正常显示,这个问题还没有研究,有知道的大神可以告诉我哈O(∩_∩)O~~,这里解决方法是,add到collectionView的superview上,在init的方法中要想获取到collectionView的superview,只能等到collectionView加载完成也就是添加到控制器的view上以后。这也是将创建方法放在DispatchQueue.main.async{}方法中的原因。也就做到了等collectionView被添加到控制器的view上以后才去创建pageControl。

3.手动无限滚动实现:在于拖动时,collectionView滚动位置的控制,在scrollView滚动减速的代理方法中:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    //当前的索引
    var offset:NSInteger = NSInteger(scrollView.contentOffset.x / scrollView.bounds.size.width)
    
    //第0页时,跳到索引imgUrls.count位置;最后一页时,跳到索引imgUrls.count-1位置
    if offset == 0 || offset == (self.numberOfItems(inSection: 0) - 1) {
        if offset == 0 {
            offset = imgUrls.count
        }else {
            offset = imgUrls.count - 1
        }
    }
    scrollView.contentOffset = CGPoint.init(x: CGFloat(offset) * scrollView.bounds.size.width, y: 0)
}

关键点就是上面原理中说的改变contentOffset或者滚动位置: 第0页时,跳到索引imgUrls.count位置;最后一页时,跳到索引imgUrls.count-1位置

4.自动轮播实现:

首先,在init()调用创建定时器,去触发自动滚动方法:

@objc private func setUpTimer(){
    timer = Timer.init(timeInterval: autoScrollDelay, target: self, selector: #selector(autoScroll), userInfo: nil, repeats: true)
    RunLoop.current.add(timer!, forMode: .commonModes)
}

自动滚动方法autoScroll的实现:

    //当前的索引
    var offset:NSInteger = NSInteger(self.contentOffset.x / self.bounds.size.width)
    
    //第0页时,跳到索引imgUrls.count位置;最后一页时,跳到索引imgUrls.count-1位置
    if offset == 0 || offset == (itemCount - 1) {
        if offset == 0 {
            offset = imgUrls.count
        }else {
            offset = imgUrls.count - 1
        }
        
        self.contentOffset = CGPoint.init(x: CGFloat(offset) * self.bounds.size.width, y: 0)
        //再滚到下一页
        self.setContentOffset(CGPoint.init(x: CGFloat(offset + 1) * self.bounds.size.width, y: 0), animated: true)
    }else{
        //直接滚到下一页
        self.setContentOffset(CGPoint.init(x: CGFloat(offset + 1) * self.bounds.size.width, y: 0), animated: true)
    }

此方法关键点在于:当滚动到第0页和最后一页时要做特殊处理,比如当滚到最后一页时,要先把contentOffset设置为imgUrls.count-1位置,然后再动画改变contentOffset到imgUrls.count位置,这样就实现了视觉上的平滑滚动效果了。

5.定时器的添加与移除控制:
//拖动停止时添加定时器

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    setUpTimer()
}

//将要拖动时移除

  func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    removeTimer()
}

//添加定时器

@objc private func setUpTimer(){
    timer = Timer.init(timeInterval: autoScrollDelay, target: self, selector: #selector(autoScroll), userInfo: nil, repeats: true)
    RunLoop.current.add(timer!, forMode: .commonModes)
}

//移除定时器

@objc private func removeTimer(){
    if (timer != nil) {
        timer?.invalidate()
        timer = nil
    }
}

//轮播器销毁时也要移除

deinit {
    removeTimer()
}

6.自定义CollectionViewFlowLayout

class HHCollectionViewFlowLayout:UICollectionViewFlowLayout{
//prepare方法在collectionView第一次布局的时候被调用
override func prepare() {
    super.prepare()//必须写
    collectionView?.backgroundColor = UIColor.white
    // 通过collectionView 的属性布局cell
    self.itemSize = (self.collectionView?.bounds.size)!
    self.minimumInteritemSpacing = 0 //cell之间最小间距
    self.minimumLineSpacing = 0 //最小行间距
    self.scrollDirection = .horizontal;
    
    self.collectionView?.bounces = false //禁用弹簧效果
    self.collectionView?.isPagingEnabled = true //分页
    self.collectionView?.showsHorizontalScrollIndicator = false
    self.collectionView?.showsVerticalScrollIndicator = false
}}

7.自定义HHCollectionViewCell:

  class HHCollectionViewCell:UICollectionViewCell {

var imageView:UIImageView?
override init(frame: CGRect) {
    super.init(frame: frame)
    self.clipsToBounds = true
    imageView = UIImageView.init(frame: self.bounds)
    imageView?.contentMode = .scaleAspectFill
    contentView.addSubview(imageView!)
}
required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}}

8.HHScrollView的代理方法:
@objc protocol HHScrollViewDelegate:NSObjectProtocol {
//点击代理方法
@objc optional func hhScrollView(_ scrollView: HHScrollView, didSelectRowAt index: NSInteger)
}
通过代理可以监听被点击的图片的索引。

好了,到此Swift+UICollectionView实现图片无限轮播器主要过程介绍完了,详细代码请查看demo:下载地址:https://github.com/wanghhh/HHScrollView#hhscrollview。demo中下载图片用了SDWebImage,运行前请cocoaPods install一下。
文辞粗浅,对于代码中可能存在的问题,欢迎大家指出,共同学习进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容