Swift實現"視差效果"的檢視輪播
來自Leo的原創部落格,轉載請著名出處
我的StackOverflow
注意:本文的程式碼是用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