swift:自定義UICollectionViewFlowLayout

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构罗,估計進度得緩緩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末智玻,一起剝皮案震驚了整個濱河市遂唧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吊奢,老刑警劉巖盖彭,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異页滚,居然都是意外死亡召边,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門裹驰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來隧熙,“玉大人,你說我怎么就攤上這事幻林≌甓ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵沪饺,是天一觀的道長邻悬。 經(jīng)常有香客問我,道長随闽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任肝谭,我火速辦了婚禮掘宪,結(jié)果婚禮上蛾扇,老公的妹妹穿的比我還像新娘。我一直安慰自己魏滚,他們只是感情好镀首,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鼠次,像睡著了一般更哄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上腥寇,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天成翩,我揣著相機與錄音,去河邊找鬼赦役。 笑死麻敌,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的掂摔。 我是一名探鬼主播术羔,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼乙漓!你這毒婦竟也來了级历?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤叭披,失蹤者是張志新(化名)和其女友劉穎寥殖,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體趋观,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡扛禽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了皱坛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片编曼。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖剩辟,靈堂內(nèi)的尸體忽然破棺而出掐场,到底是詐尸還是另有隱情,我是刑警寧澤贩猎,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布熊户,位于F島的核電站,受9級特大地震影響吭服,放射性物質(zhì)發(fā)生泄漏嚷堡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一艇棕、第九天 我趴在偏房一處隱蔽的房頂上張望蝌戒。 院中可真熱鬧串塑,春花似錦、人聲如沸北苟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽友鼻。三九已至傻昙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間彩扔,已是汗流浹背妆档。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留借杰,地道東北人过吻。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像蔗衡,于是被迫代替她去往敵國和親纤虽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容