iOS 最新Swift+UICollectionView實(shí)現(xiàn)圖片無限輪播器

Bg:
圖片輪播器數(shù)不勝數(shù),但大多是UIScrollView + OC實(shí)現(xiàn)的,心血來潮,決定用Swift+UICollectionView造個(gè)輪子玩玩HHScrollView:https://github.com/wanghhh/HHScrollView#hhscrollview。
先看下效果圖:

Untitled.gif

功能實(shí)現(xiàn):

1、Swift+UICollectionView實(shí)現(xiàn)自動(dòng)無限輪播,可手動(dòng)拖動(dòng)
2、頁碼顯示,可以自定義頁碼指示器位置、顏色
3、輪播間隔時(shí)間等屬性設(shè)置

輪播器調(diào)用方法:
下載demo,直接將HHScrollView.swift文件拖進(jìn)自己項(xiàng)目即可。
然后在控制器的viewDidLoad() 中實(shí)例化:

    //準(zhǔn)備圖片數(shù)據(jù),就是圖片url字符串
    imageDataSource = loadImages()
    
    //提供兩種實(shí)例化方法:
    //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,后根據(jù)網(wǎng)絡(luò)數(shù)據(jù)設(shè)置imgUrls
    let scrollView = HHScrollView.init(frame: CGRect.init(x: 0, y: 64, width: UIScreen.main.bounds.width, height: 200))
    //設(shè)置數(shù)據(jù)源(圖片urlStr)******
    //加載本地圖片
    //scrollView.isFromNet = false
    //scrollView.imgUrls = ["ic_banner01","ic_banner02","ic_banner03"]
    //默認(rèn)加載網(wǎng)絡(luò)圖片
    scrollView.imgUrls = imageDataSource
    //設(shè)置代理,根據(jù)需要要不要監(jiān)聽圖片點(diǎn)擊
    scrollView.hhScrollViewDelegae = self

HHScrollView提供的屬性:

    //代理
    weak var hhScrollViewDelegae:HHScrollViewDelegate?
    //分頁指示器頁碼顏色
    var pageControlColor:UIColor?
    //分頁指示器當(dāng)前頁顏色
    var currentPageControlColor:UIColor?
    //分頁指示器位置
    var pageControlPoint:CGPoint?
    //分頁指示器
    fileprivate var pageControl:UIPageControl?
   //自動(dòng)滾動(dòng)時(shí)間默認(rèn)為3.0
   var autoScrollDelay:TimeInterval = 3 {
    didSet{
        removeTimer()
        setUpTimer()
    }
   } 
   //圖片是否來自網(wǎng)絡(luò),默認(rèn)是
   var isFromNet:Bool = true
   //占位圖
   var placeholderImage:String = "ic_place"
   //設(shè)置圖片資源url字符串。
   var imgUrls = NSArray(){
      didSet{
          pageControl?.numberOfPages = imgUrls.count
          itemCount = imgUrls.count
          self.reloadData()
      }  
   }
   fileprivate var itemCount:NSInteger = 0//cellNum
   fileprivate var timer:Timer?//定時(shí)器

可以通過以上屬性和自身項(xiàng)目需要自定義輪播器的樣式、滾動(dòng)時(shí)間間隔等,這些基本屬性都有默認(rèn)值。

HHScrollView提供的便利構(gòu)造器:

//便利構(gòu)造方法
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的復(fù)用機(jī)制,不用自己再去考慮imageView的復(fù)用問題,節(jié)省內(nèi)存,有利于性能提升。

先說下大致思路:

我們知道UICollectionView繼承自UIScrollView,也就是說UIScrollView的基本屬性方法UICollectionView都有,那么UICollectionView也可以分頁顯示。將item(UITableView對(duì)應(yīng)的cell)的寬和高分別設(shè)置成UICollectionView自身的寬和高,數(shù)據(jù)源返回的item個(gè)數(shù)就是參與圖片的圖片個(gè)數(shù),那么問題就在于當(dāng)滾動(dòng)到最后一張或第一張圖片的時(shí)候,怎么繼續(xù)滾動(dòng)呢?

為了解決這個(gè)問題,我們可以通過擴(kuò)大item的個(gè)數(shù)的方法解決它,無限輪播的關(guān)鍵就在于此:

1.將數(shù)據(jù)源方法返回的item個(gè)數(shù)設(shè)置未imgUrls.count(imgUrls是網(wǎng)絡(luò)圖片url或本地圖片的數(shù)組)的2倍,在collectionView加載完成后默認(rèn)滾動(dòng)到索引為imgUrls.count的位置,這樣cell就可以向左或右滾動(dòng)了。

例如:我們想加載3張圖片,那么collectionView:初始位置應(yīng)該在"圖片1-2"的位置,如下圖:

QQ20170820-2@2x.png

2.當(dāng)collectionView滾動(dòng)到最后一張的時(shí)候,即滾到"圖片3-2"的位置時(shí),讓collectionView回到"圖片3-1"的位置,這樣就可以繼續(xù)向右滾動(dòng)了。同理,當(dāng)collectionView滾動(dòng)到第一張的時(shí)候,即滾到"圖片1-1"的位置時(shí),讓collectionView回到"圖片1-2"的位置,這樣就可以繼續(xù)向左滾動(dòng)了。如下圖:

QQ20170820-1@2x.png

以上就是無限輪播的基本實(shí)現(xiàn)原理了。
關(guān)鍵代碼:

1.collectionView初始位置設(shè)置:

    //在collectionView加載完成后默認(rèn)滾動(dòng)到索引為imgUrls.count的位置,這樣cell就可以向左或右滾動(dòng)
    DispatchQueue.main.async {
        //注意:在輪播器視圖添加到控制器的view上以后,這樣是為了將分頁指示器添加到self.superview上(如果將分頁指示器直接添加到collectionView上的話,指示器將不能正常顯示)
        self.setUpPageControl()
        let indexpath = NSIndexPath.init(row: self.imgUrls.count, section: 0)
        //滾動(dòng)位置
        self.scrollToItem(at: indexpath as IndexPath, at: .left, animated: false)
    }

此段代碼寫在collectionView的init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout)方法中,關(guān)鍵在于要等到在collectionView加載完成以后,再去改變滾動(dòng)的位置,這里利用DispatchQueue.main.async異步實(shí)現(xiàn)。本質(zhì)就是利用主隊(duì)列調(diào)度任務(wù)的阻塞特性實(shí)現(xiàn),因?yàn)橹麝?duì)列只會(huì)在主線程"閑暇"的時(shí)候才去執(zhí)行別的任務(wù),這里"閑暇"就是指collectionView加載完成以后。
2.UIPageControl的加載時(shí)機(jī)和方式

要想將頁碼顯示器封裝到輪播器中,而不是在使用輪播器的控制器中創(chuàng)建和加載,做到更好的封裝,也將setUpPageControl的創(chuàng)建頁碼器的代碼放在init()方法的主隊(duì)列異步方法中去,在上面代碼中可以看到self.setUpPageControl()。創(chuàng)建代碼如下:

@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!)
}

另外發(fā)現(xiàn)將UIPageControl直接add到collectionView上時(shí)不能正常顯示,這個(gè)問題還沒有研究,有知道的大神可以告訴我哈O(∩_∩)O~~,這里解決方法是,add到collectionView的superview上,在init的方法中要想獲取到collectionView的superview,只能等到collectionView加載完成也就是添加到控制器的view上以后。這也是將創(chuàng)建方法放在DispatchQueue.main.async{}方法中的原因。也就做到了等collectionView被添加到控制器的view上以后才去創(chuàng)建pageControl。

3.手動(dòng)無限滾動(dòng)實(shí)現(xiàn):在于拖動(dòng)時(shí),collectionView滾動(dòng)位置的控制,在scrollView滾動(dòng)減速的代理方法中:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    //當(dāng)前的索引
    var offset:NSInteger = NSInteger(scrollView.contentOffset.x / scrollView.bounds.size.width)
    
    //第0頁時(shí),跳到索引imgUrls.count位置;最后一頁時(shí),跳到索引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)
}

關(guān)鍵點(diǎn)就是上面原理中說的改變contentOffset或者滾動(dòng)位置: 第0頁時(shí),跳到索引imgUrls.count位置;最后一頁時(shí),跳到索引imgUrls.count-1位置

4.自動(dòng)輪播實(shí)現(xiàn):

首先,在init()調(diào)用創(chuàng)建定時(shí)器,去觸發(fā)自動(dòng)滾動(dòng)方法:

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

自動(dòng)滾動(dòng)方法autoScroll的實(shí)現(xiàn):

    //當(dāng)前的索引
    var offset:NSInteger = NSInteger(self.contentOffset.x / self.bounds.size.width)
    
    //第0頁時(shí),跳到索引imgUrls.count位置;最后一頁時(shí),跳到索引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)
    }

此方法關(guān)鍵點(diǎn)在于:當(dāng)滾動(dòng)到第0頁和最后一頁時(shí)要做特殊處理,比如當(dāng)滾到最后一頁時(shí),要先把contentOffset設(shè)置為imgUrls.count-1位置,然后再動(dòng)畫改變contentOffset到imgUrls.count位置,這樣就實(shí)現(xiàn)了視覺上的平滑滾動(dòng)效果了。

5.定時(shí)器的添加與移除控制:
//拖動(dòng)停止時(shí)添加定時(shí)器

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

//將要拖動(dòng)時(shí)移除

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

//添加定時(shí)器

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

//移除定時(shí)器

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

//輪播器銷毀時(shí)也要移除

deinit {
    removeTimer()
}

6.自定義CollectionViewFlowLayout

class HHCollectionViewFlowLayout:UICollectionViewFlowLayout{
//prepare方法在collectionView第一次布局的時(shí)候被調(diào)用
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 {
//點(diǎn)擊代理方法
@objc optional func hhScrollView(_ scrollView: HHScrollView, didSelectRowAt index: NSInteger)
}
通過代理可以監(jiān)聽被點(diǎn)擊的圖片的索引。

好了,到此Swift+UICollectionView實(shí)現(xiàn)圖片無限輪播器主要過程介紹完了,詳細(xì)代碼請(qǐng)查看demo:下載地址:https://github.com/wanghhh/HHScrollView#hhscrollview。demo中下載圖片用了SDWebImage,運(yùn)行前請(qǐng)cocoaPods install一下。
文辭粗淺,對(duì)于代碼中可能存在的問題,歡迎大家指出,共同學(xué)習(xí)進(jìn)步。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容