1. 程式人生 > >Swift實現"視差效果"的檢視輪播

Swift實現"視差效果"的檢視輪播

來自Leo的原創部落格,轉載請著名出處

我的StackOverflow

profile for Leo on Stack Exchange, a network of free, community-driven Q&A sites

注意:本文的程式碼是用Swift 2.2寫的。

視差效果

什麼是視差效果?我們來看下格瓦拉的App,就知道了

這裡寫圖片描述

格瓦拉的視差效果算是比較明顯的。所謂視差效果,就是看起來在”上面”的檢視滾動的速度大於”底層”的時圖滾動。所以,給人的視覺體驗要比”屌絲”的滾動效果好不少。

ParallexBanner

之前專案趕進度,一直用的開源的。最近剛好在複習Swift,腦袋一熱,就自己寫了個。

支援

  • 迴圈滾動
  • 自動滾動
  • 本地圖片和網路圖片
  • 視差效果
  • Storyboard和純Code佈局

效果

實現檢視輪播的幾種方式

檢視輪播沒什麼難度,大致分為幾種實現方式

  • 單純的用ScrollView實現,然後一張一張圖片subview新增進去。
  • UICollectionView實現
  • UIPageViewController實現

大致分析了下。

  • ScrollView實現簡單粗暴,但是有一個很大的問題,檢視複用。因為是一次性addSubView進去的。所以,在圖片較多的時候,記憶體佔用較多。
  • UIPageViewController實現依賴於ViewController,而作為一個檢視來說,還是輕量級比較好一點。
  • UICollectionView幫我們實現了複用,我們只需要關注輪播本身就可以了。

So,
本文就選用CollectionView實現吧。

定義介面

寫一個功能或者業務的第一步,定義介面,想要整體的類分佈,值傳遞的邏輯。(這個很重要)

用Swift寫程式碼要注意一點:Swift是一個面相協議程式設計的語言

所以,Try start with protocol.

檢視輪播需要資料來源傳遞進來,同樣需要把點選和滾動事件傳遞出去。所以,我們就採用Cocoa Touch的常用設計模式:dataSource和delegate,定義如下

@objc public protocol ParallexBannerDelegate {
    //點選事件
    optional func banner(banner
:ParallexBanner,didClickAtIndex index:NSInteger) //滾動事件 optional func banner(banner:ParallexBanner,didScrollToIndex index:NSInteger) } @objc public protocol ParallexBannerDataSource{ //一共有幾個 func numberOfBannersIn(bannner:ParallexBanner)->NSInteger //每一個index處的圖片,這裡可以返回String或者UIImage型別 func banner(banner:ParallexBanner,urlOrImageAtIndex index:NSInteger)->AnyObject //Placeholder optional func banner(banner:ParallexBanner,placeHolderForIndex index:NSInteger)->UIImage? //Image的ContentMode optional func banner(banner:ParallexBanner,contentModeAtIndex index:NSInteger)->UIViewContentMode }

對了,我們要支援兩種型別的滾動:普通滾動,和視差滾動。這裡有兩種方式試下,一種是用一個Bool來表示,另一種是用列舉。

考慮到以後,我可能新增更多的滾動模式,這裡用列舉表示。

public enum ParallexBannerTransition{
    case Normal
    case Parallex
}

然後,我們還需要幾個屬性,暴露出來給使用者設定。這時候的程式碼如下

public class ParallexBanner: UIView {
// MARK: - Propertys -
    public  weak var dataSource:ParallexBannerDataSource?
    public  weak var delegate:ParallexBannerDelegate?
    public  var transitionMode:ParallexBannerTransition = ParallexBannerTransition.Parallex
    public  var autoScroll:Bool = true
    public  var enableScrollForSinglePage = false
    public  var parllexSpeed:CGFloat = 0.4
    public  var autoScrollTimeInterval:NSTimeInterval = 3.0 
    public  let pageControl:UIPageControl = UIPageControl()
    private var _currentIndex = 1
    private var collectionView:UICollectionView!
    private var timer:NSTimer?
    private var flowLayout:UICollectionViewFlowLayout!

// MARK: - Init -
    override public init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
}

檢視佈局

在定義好介面之後,我們要考慮佈局了。
對於ParallexBanner來說,佈局比較簡單

  • 底層是一個UICollectionView
  • 上層是一個UIPageControl

我們再來看看CollectionViewCell
普通的滾動CollectionViewCell中只有一個UIImageView,為了實現”視差效果”,我們需要Cell本身也能夠控制ImageView滾動。所以,我們用一個ScrollView來包含ImageView,通過控制ContentOffset來控制ImageView的滾動。

public class BannerCell:UICollectionViewCell{
    let imageView = UIImageView()
    let scrollView = UIScrollView()
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    private func commonInit(){
        contentView.addSubview(scrollView)
        scrollView.scrollEnabled = false
        //這裡要設定,不然這個scrollView會吃掉我們的觸控
        scrollView.userInteractionEnabled = false
        scrollView.addSubview(imageView)
        imageView.contentMode = UIViewContentMode.ScaleAspectFill;
    }
    required public init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override public func layoutSubviews() {
        super.layoutSubviews()
        scrollView.contentSize = self.bounds.size;
        scrollView.frame = self.bounds
        imageView.frame = scrollView.bounds
    }
}

迴圈滾動

用CollectionView實現基於Timer的滾動沒什麼難度。
無非就是一行程式碼

collectionView.scrollToItemAtIndexPath(nextIndx, atScrollPosition: UICollectionViewScrollPosition.None, animated: true)

那麼如何實現迴圈滾動呢?有很多種方式實現,本文采用在前後插入兩個額外的資料來實現。比如我有三張圖,

然後,在前後各插入兩張

當我向右滾動,滾動到如圖紅色虛線的臨街區域的時候,就把contentOffset調整到左邊的位置

同樣,當我向左滾動到臨界區域,就調整contentOffset到右側區域

這樣就實現了迴圈滾動。
對應程式碼

    public func scrollViewDidScroll(scrollView: UIScrollView) {
        var offSetX = scrollView.contentOffset.x
        let width = CGRectGetWidth(scrollView.bounds)
        guard width != 0 else{
            return
        }
        if offSetX >= width * CGFloat(self.dataSource!.numberOfBannersIn(self) + 2 - 1){
            offSetX = width;
            scrollView.contentOffset = CGPointMake(offSetX,0);
        }else if(offSetX < 0 ){
            offSetX = width * CGFloat(self.dataSource!.numberOfBannersIn(self) + 2 - 2);
            scrollView.contentOffset = CGPointMake(offSetX,0);
        }
    }

視差效果

視差效果還是比較簡單實現的。我們獲取當前在螢幕上的Cell,然後計算相對移動的距離,然後,把Cell本身的ImageView像相反方向按照Speed來移動。

      collectionView.visibleCells().forEach { (cell) in
            if let bannerCell = cell as? BannerCell{
               handleEffect(bannerCell)
            }
        }

調整Cell中的ScrollView的ContentOffset

   private func handleEffect(cell:BannerCell){
        switch transitionMode {
        case .Parallex:
            let minusX = self.collectionView.contentOffset.x - cell.frame.origin.x
            let imageOffsetX = -minusX * parllexSpeed;
            cell.scrollView.contentOffset = CGPointMake(imageOffsetX, 0)
        default:
            break
        }
    }

總結

到這裡,基本的原理就講解完了。其實,所謂的視差效果,就是合理的利用ScrollView。感興趣的同學可以看看原始碼,不到300行,很簡單。地址:ParallexBanner