swift:自定義UICollectionViewFlowLayout
寫作目的
UICollectionView是ios中一個十分強大的控件葛假,利用它能夠十分簡單的實現(xiàn)一些很好看的效果。UICollectionView的效果又依賴于UICollectionViewLayout或者它的子類UICollectionViewFlowLayout茅撞。而關(guān)于自定義UICollectionViewFlowLayout網(wǎng)上介紹的比較少。出于這一目的,寫下這邊文章,希望能夠幫助初學(xué)者(我也是)實現(xiàn)一些簡單的流水布局效果。下面的演示就是本篇文章的目標(biāo)赃磨。最終版代碼和所有圖片素材(圖片名和項目中有點不一樣)已經(jīng)上傳至Github,大家可以下載學(xué)習(xí)。

幾個簡單的概念
- UICollectionViewLayout與UICollectionViewFlowLayout
UICollectionView的顯示效果幾乎全部由UICollectionViewLayout負責(zé)(甚至是cell的大小)洼裤。所以邻辉,一般開發(fā)中所說的自定義UICollectionView也就是自定義UICollectionViewLayout。而UICollectionViewFlowLayout是繼承自UICollectionViewLayout的腮鞍,由蘋果官方實現(xiàn)的流水布局效果值骇。如果想自己實現(xiàn)一些流水布局效果可以繼承自最原始UICollectionViewLayout從頭寫,也可以繼承自UICollectionViewFlowLayout進行修改移国。文本是繼承自UICollectionViewFlowLayt*
- UICollectionViewLayoutAttributes
第二點就說了UICollectionView的顯示效果幾乎全部由UICollectionViewLayout負責(zé)吱瘩,而真正存儲著每一個cell的位置、大小等屬性的是UICollectionViewLayoutAttributes迹缀。每一個cell對應(yīng)著一個屬于自己的UICollectionViewLayoutAttributes使碾,而UICollectionViewLayout正是利用UICollectionViewLayoutAttributes里存在的信息對每一個cel進行布局。
- 流水布局
所謂流水布局就是:就是cell以一定的規(guī)律進行如同流水一般的有規(guī)律的一個接著一個的排列祝懂。?最經(jīng)典的流水布局便是九宮格布局票摇,絕大部分的圖片選擇器也是流水布局。
準(zhǔn)備工作
- xcode7.0
- swift2.0
- 自己我提供的素材并在控制器中添加如下代碼
class ViewController: UIViewController,UICollectionViewDelegate, UICollectionViewDataSource {
lazy var imageArray: [String] = {
var array: [String] = []
for i in 1...20 {
array.append("\(i)-1")
}
return array
}()
override func viewDidLoad() {
super.viewDidLoad()
let collectionView = UICollectionView(frame: CGRectMake(0, 100, self.view.bounds.width, 200), collectionViewLayout: UICollectionViewFlowLayout())
collectionView.backgroundColor = UIColor.blackColor()
collectionView.dataSource = self
collectionView.delegate = self
collectionView.registerClass(ImageTextCell.self, forCellWithReuseIdentifier: "ImageTextCell")
self.view.addSubview(collectionView)
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.imageArray.count;
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ImageTextCell", forIndexPath: indexPath) as! ImageTextCell
cell.imageStr = self.imageArray[indexPath.item]
return cell
}
}
//這里是自定義cell的代碼
class ImageTextCell: UICollectionViewCell {
var imageView: UIImageView?
var imageStr: NSString? {
didSet {
self.imageView!.image = UIImage(named: self.imageStr as! String)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.imageView = UIImageView()
self.addSubview(self.imageView!)
}
override func layoutSubviews() {
super.layoutSubviews()
self.imageView?.frame = self.bounds
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
效果應(yīng)該是這樣的

編碼
水平排列
創(chuàng)建一個名為LineLayout.swift的文件(繼承自UICollectionViewFlowLayout)砚蓬。添加如下幾行代碼
var itemW: CGFloat = 100
var itemH: CGFloat = 100
override init() {
super.init()
//設(shè)置每一個元素的大小
self.itemSize = CGSizeMake(itemW, itemH)
//設(shè)置滾動方向
self.scrollDirection = .Horizontal
//設(shè)置間距
self.minimumLineSpacing = 0.7 * itemW
}
//蘋果推薦矢门,對一些布局的準(zhǔn)備操作放在這里
override func prepareLayout() {
//設(shè)置邊距(讓第一張圖片與最后一張圖片出現(xiàn)在最中央)ps:這里可以進行優(yōu)化
let inset = (self.collectionView?.bounds.width ?? 0) * 0.5 - self.itemSize.width * 0.5
self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset)
}
效果就成了這樣

shouldInvalidateLayoutForBoundsChange方法與layoutAttributesForElementsInRect方法關(guān)系
標(biāo)題所寫出的是十分重要的兩方法,先看我添加的如下測試代碼
/**
返回true只要顯示的邊界發(fā)生改變就重新布局:(默認(rèn)是false)
內(nèi)部會重新調(diào)用prepareLayout和調(diào)用
layoutAttributesForElementsInRect方法獲得部分cell的布局屬性
*/
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
print(newBounds)
return true
}
/**
用來計算出rect這個范圍內(nèi)所有cell的UICollectionViewLayoutAttributes灰蛙,
并返回祟剔。
*/
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
print("layoutAttributesForElementsInRect==\(rect)")
let ret = super.layoutAttributesForElementsInRect(rect)
// print(ret?.count)
return ret
}
為了解釋,我添加了幾個打印語句摩梧,在shouldInvalidateLayoutForBoundsChange返回值設(shè)置為true后峡扩,會發(fā)現(xiàn)layoutAttributesForElementsInRect方法調(diào)用十分頻繁,幾乎是每滑動一點就會調(diào)用一次障本。觀察打印信息可以發(fā)現(xiàn)很多秘密。
- 啟動程序有如下打印
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
好像看不太懂响鹃,沒事驾霜,嘗試滑動。
- 滑動
(0.5, 0.0, 320.0, 200.0) //這個是shouldInvalidateLayoutForBoundsChange方法的打印的newBounds
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)//這個是layoutAttributesForElementsInRect打印的rect
(1.5, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
(3.5, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
...
不難發(fā)現(xiàn)买置,shouldInvalidateLayoutForBoundsChange的參數(shù)newBounds的意思是UICollectionView的可見矩形粪糙。什么叫可見矩陣?忿项,因為UICollectionView也是UIScrollView的子類蓉冈,所以它真正的“內(nèi)容”遠遠不止我們屏幕上看到的那么多(這里不再話時間繼續(xù)解釋可見矩陣)城舞。那好像layoutAttributesForElementsInRect打印出來的東西沒有啥變化是怎么回事?不急繼續(xù)滑動寞酿。
- 解密
繼續(xù)滑動后有這些信息家夺,經(jīng)過刪除一些無用信息,顯示如下伐弹。(注意看有注釋的行)
...
(248.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
(249.0, 0.0, 320.0, 200.0) //這里是可見矩陣
layoutAttributesForElementsInRect==(0.0, 0.0, 1136.0, 568.0) //這里變化了1136.0是568.0的2倍(1136代表的是寬度的意思應(yīng)該知道不需要解釋吧)
(250.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 1136.0, 568.0)
...
(567.5, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 1136.0, 568.0)
(568.5, 0.0, 320.0, 200.0)//這里是可見矩陣
layoutAttributesForElementsInRect==(568.0, 0.0, 568.0, 568.0) // 這里又變化了拉馋,x變成了568,寬度變成了568
(571.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 568.0, 568.0)
...
(815.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 568.0, 568.0)
(817.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 1136.0, 568.0) //還有這里
...
(1135.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 1136.0, 568.0)
(1136.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(1136.0, 0.0, 568.0, 568.0) //還有這里
上面的的數(shù)據(jù)展示其實已經(jīng)足夠解釋一切了惨好。讀到這里煌茴,推薦你自己去找找規(guī)律,通過自己發(fā)現(xiàn)的奧秘絕對比直接看我寫出答案有意義的多日川!下面這張圖例已經(jīng)說明了一切

至于為什么會是568的倍數(shù)蔓腐。。因為我是用的5s模擬器龄句。你換成4s就變成480了回论。至于這樣設(shè)計的理由,我猜測是為了方便進行范圍的確定撒璧。
縮放效果
了解了上面shouldInvalidateLayoutForBoundsChange方法與layoutAttributesForElementsInRect方法關(guān)系后透葛,可以繼續(xù)進行編碼了。因為主要的內(nèi)容已經(jīng)講解完畢卿樱,剩下的就只是一些動畫的計算僚害,所以不再繼續(xù)講解,直接貼出代碼繁调。
class LineLayout: UICollectionViewFlowLayout {
var itemW: CGFloat = 100
var itemH: CGFloat = 100
lazy var inset: CGFloat = {
//這樣設(shè)置萨蚕,inset就只會被計算一次,減少了prepareLayout的計算步驟
return (self.collectionView?.bounds.width ?? 0) * 0.5 - self.itemSize.width * 0.5
}()
override init() {
super.init()
//設(shè)置每一個元素的大小
self.itemSize = CGSizeMake(itemW, itemH)
//設(shè)置滾動方向
self.scrollDirection = .Horizontal
//設(shè)置間距
self.minimumLineSpacing = 0.7 * itemW
}
//蘋果推薦蹄胰,對一些布局的準(zhǔn)備操作放在這里
override func prepareLayout() {
//設(shè)置邊距(讓第一張圖片與最后一張圖片出現(xiàn)在最中央)
self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/**
返回true只要顯示的邊界發(fā)生改變就重新布局:(默認(rèn)是false)
內(nèi)部會重新調(diào)用prepareLayout和調(diào)用
layoutAttributesForElementsInRect方法獲得部分cell的布局屬性
*/
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
/**
用來計算出rect這個范圍內(nèi)所有cell的UICollectionViewLayoutAttributes岳遥,
并返回。
*/
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
//取出rect范圍內(nèi)所有的UICollectionViewLayoutAttributes裕寨,然而
//我們并不關(guān)心這個范圍內(nèi)所有的cell的布局浩蓉,我們做動畫是做給人看的,
//所以我們只需要取出屏幕上可見的那些cell的rect即可
let array = super.layoutAttributesForElementsInRect(rect)
//可見矩陣
let visiableRect = CGRectMake(self.collectionView!.contentOffset.x, self.collectionView!.contentOffset.y, self.collectionView!.frame.width, self.collectionView!.frame.height)
//接下來的計算是為了動畫效果
let maxCenterMargin = self.collectionView!.bounds.width * 0.5 + itemW * 0.5;
//獲得collectionVIew中央的X值(即顯示在屏幕中央的X)
let centerX = self.collectionView!.contentOffset.x + self.collectionView!.frame.size.width * 0.5;
for attributes in array! {
//如果不在屏幕上宾袜,直接跳過
if !CGRectIntersectsRect(visiableRect, attributes.frame) {continue}
let scale = 1 + (0.8 - abs(centerX - attributes.center.x) / maxCenterMargin)
attributes.transform = CGAffineTransformMakeScale(scale, scale)
}
return array
}
/**
用來設(shè)置collectionView停止?jié)L動那一刻的位置
- parameter proposedContentOffset: 原本collectionView停止?jié)L動那一刻的位置
- parameter velocity: 滾動速度
- returns: 最終停留的位置
*/
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
//實現(xiàn)這個方法的目的是:當(dāng)停止滑動捻艳,時刻有一張圖片是位于屏幕最中央的。
let lastRect = CGRectMake(proposedContentOffset.x, proposedContentOffset.y, self.collectionView!.frame.width, self.collectionView!.frame.height)
//獲得collectionVIew中央的X值(即顯示在屏幕中央的X)
let centerX = proposedContentOffset.x + self.collectionView!.frame.width * 0.5;
//這個范圍內(nèi)所有的屬性
let array = self.layoutAttributesForElementsInRect(lastRect)
//需要移動的距離
var adjustOffsetX = CGFloat(MAXFLOAT);
for attri in array! {
if abs(attri.center.x - centerX) < abs(adjustOffsetX) {
adjustOffsetX = attri.center.x - centerX;
}
}
return CGPointMake(proposedContentOffset.x + adjustOffsetX, proposedContentOffset.y)
}
}
如果在控制器中加入下面兩個方法庆猫,在你點擊控制器认轨,或者點擊某個cell會有很炫的動畫產(chǎn)生,這都是蘋果幫我們做好的月培。
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
self.imageArray.removeAtIndex(indexPath.item)
collectionView.deleteItemsAtIndexPaths([indexPath])
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if self.collectionView!.collectionViewLayout.isKindOfClass(LineLayout.self) {
self.collectionView!.setCollectionViewLayout(UICollectionViewFlowLayout(), animated: true)
}else {
self.collectionView!.setCollectionViewLayout(LineLayout(), animated: true)
}
}
總結(jié)
本篇文章記錄了我在自定義UICollectionViewFlowLayout過程中遇到的一些問題和解決方式(其實有一些坑爹的問題我沒有列出嘁字,怕誤導(dǎo)大家)恩急。上面的全部都是基于UICollectionViewFlowLayout進行的更改。而我在GitHub上面上傳的也有一份繼承自UICollectionViewLayout的非流水布局纪蜒。效果如下衷恭,因為原理性的東西都差不多,就不再進行分析(代碼也有注釋)霍掺。感興趣的可以這Github上面下載匾荆。如果文章中有什么錯誤或者更好的方法、建議之類杆烁,感謝您的指出牙丽。我們共同學(xué)習(xí)!O(∩_∩)O兔魂!

后記
我不會告訴你烤芦,介紹UICollectionView的自定義布局這篇文章,是我下一個實驗的前傳析校。不過最近被老師強迫幫他們?nèi)懳臋n构罗,估計進度得緩緩。