手把手教你自定義流水布局寫特效

最終效果

步驟思路
1.寫出基本的colletionView,讓他進行水平滑動,一個高度只能顯示一個cell
2.了解基本的UICollectionViewLayoutAttributes屬性,讓他可以改變一些cell的屬性,例如aphelia,和縮小和放大
3.了解一個方法,當我們rect改變的時候,會判斷是否刷新cell的布局
4.判斷哪個cell離colleview的中點最近,了解一個target函數(shù)
4.1使用剛才rect方法,調(diào)用super方法,獲取的是計算好餓cell的中心點,所以知道了他的中心點,如果使用的是self,那么會在調(diào)用一次,比較麻煩
4.2 計算collviontView的中心點值(注意,不能像過去的那個計算 過去:偏移量(手松開的哪一個可)+寬度的一般),此時拿到的偏移量事不準的,因為我們刺客還有速度,應(yīng)該拿到最后的偏移量,也就是propertyTargetPoint,將要去哪里!!!
4.3,要知道手松開的那一刻,的offSet是不準的,應(yīng)該獲取最終的,將要去的位置,所以傳遞rect是不能下傳遞,而是將來結(jié)束的時候,rect,用x最難判斷,就是propertyPointOffset.x
4.4 遍歷布局屬性,獲取那個距離最短,所有的都偏移這段多
4.5 明確,其實最后的所有的偏移量,= 最小的間距 + 目標偏移量!但是“最小的間距”可能是正負數(shù)!!
5.給cell的初始化和結(jié)束設(shè)置一個sectionInset
6.了解prepare函數(shù)的使用


1.寫出基本的colletionView,讓他進行水平滑動,一個高度只能顯示一個cell

直接去寫一個colletionView放在vc上就好,保證colletionView上只有一行,設(shè)置數(shù)據(jù)源和代理方法,布局對象直接使用系統(tǒng)的流水布局

控制器成為了代理方法和數(shù)據(jù)源方法,用extension來寫代理和數(shù)據(jù)源方法,突然紅色,一臉懵逼,后來才發(fā)現(xiàn),原來是在這里,沒有寫數(shù)據(jù)源必要的方法,寫完方法就好了
通過系統(tǒng)的寫出這個樣式
2.了解基本的UICollectionViewLayoutAttributes屬性

每個cell都有尺寸,位置和aplha值等等,其實每個cell都有一個UICollectionViewLayoutAttributes屬性,這里有cell所有的信息,包括剛剛說的三個,還有很多~~

    /**
     
          解釋類:UICollectionViewLayoutAttributes 
     
         *  每一個cell的尺寸,位置等各種屬性都對應(yīng)這個一個UICollectionViewLayoutAttributes,這個類中含有很多自己的屬性,改變這個屬性,那么cell的大小或者位置也會發(fā)生變化.還有`transform `可以更改他的形變等~
         
         @property (nonatomic) CGRect frame;
         @property (nonatomic) CGPoint center;
         @property (nonatomic) CGSize size;
         @property (nonatomic) CATransform3D transform3D;
         @property (nonatomic) CGRect bounds NS_AVAILABLE_IOS(7_0);
         @property (nonatomic) CGAffineTransform transform NS_AVAILABLE_IOS(7_0);
         @property (nonatomic) CGFloat alpha;
         @property (nonatomic) NSInteger zIndex; // default is 0
         @property (nonatomic, getter=isHidden) BOOL hidden; // As an optimization, UICollectionView might not create a view for items whose hidden attribute is YES
         @property (nonatomic, strong) NSIndexPath *indexPath;
         在這里還能拿到collectionView這個控件
         */

想寫出本文的目標效果,我們必須要重寫布局,自定義一個布局,但是繼承那個比較好,有兩個選擇,

  • 一個是抽象類UICollectionViewLayout,繼承這個,那么我們滑動都不行,要重寫的東西特別多,特別費事,不建議
  • 還有就是UICollectionViewFlowLayout這個蘋果已經(jīng)給我計算好了很多的東西,可以拿來就用,改改基本的某個屬性就行
2.1 自定義一個layout布局SFLayout

直接在項目中將系統(tǒng)的替換,效果應(yīng)該是一樣的~

2.2 func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?這個函數(shù)是干啥的?

剛才都說了,每個cell都有一個UICollectionViewLayoutAttributes屬性,這個函數(shù)的意思是,在rect范圍之內(nèi)的cell的屬性放到一個數(shù)組中,傳遞出來,一定要調(diào)用super,然后用一個數(shù)組承接,為什么用super?因為super是流水布局,返回來的數(shù)組是計算好的,可以微調(diào)使用

    /**
     打印出當前rect之內(nèi)的所有cell的“布局屬性”- UICollectionViewLayoutAttributes(返回的是一個數(shù)組)
     */
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        //獲得super已經(jīng)計算好的尺寸
        let array = super.layoutAttributesForElementsInRect(rect)
        
        //對于每個屬性進行尺寸,位置的微調(diào)
        for index in 0 ..< array!.count
        {
            let  abc:UICollectionViewLayoutAttributes  = array![index]
// 1.改變allah值
      //      abc.alpha = 0.2

//2.改變了大小
//            let scale:CGFloat = CGFloat(arc4random_uniform(345))/345.0
//            abc.transform = CGAffineTransformMakeScale(scale, scale)   
        }
        return array
abc.alpha = 0.2
改變了大小-隨機變化

3.了解一個方法,當我們rect改變的時候,會判斷是否刷新cell的布局
3.1 我們看到的這些item都沒有變化~

我們滑動的時候,item一直沒有發(fā)生變化,但是我想調(diào)用
layoutAttributesForElementsInRect(默認之調(diào)用一次)刷新一下內(nèi)部,如何處理?

重寫一個方法,只有滑動colletionView,rect發(fā)生了變化,就會調(diào)用布局屬性方法

    /**
     當collectionView顯示的rect發(fā)生了額變化,詢問一下,是否要去刷新所在cell的layoutAttribute,返回true,調(diào)用layoutAttributesForElementsInRect刷新
     */
    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return true
    }
3.2 如何讓放大的倍數(shù)會根據(jù)距離來變化?

這個相對來說有點不好理解,不過多看兩遍就好了~

結(jié)構(gòu)圖

1.要注意藍色的是frame,也就是rect
2.綠色的是contentSize我們現(xiàn)在的size肯定是大于frame的
3.黑色的永遠在frame的中點,挺好了,是frame的中點,隨著content offset的改變(綠色的view一直前進或者后退),黑線還在frame 的中點,但是他在綠色的view的x值一直發(fā)生變化
4.紅色的是cell
5.黃色的是cell的中心


思路解析
1.黑色線的位置如何計算?
黑線.x = contentOffset.x + frame.size.width*0.5
2.黃線如何計算?
attir.center.x
3.如何就算黃線和黑線的間距?
用間距絕對值就行 abs(黑線.x - atria.center.x)
4.如何根據(jù)絕對值改變cell的大小?
比例 = 間距絕對值/frame.width ,除以寬度,是隨便取得數(shù)據(jù),可以是任意的,但是一定要大于間距的絕對值,我們要比例一定是(0,1)之間的,但是隨著間距的變大,那么比例越來越大啊,咋整,只要用 真實比例 =(1-比例)就行,也可以用1.2- 比例這個隨意,看具體的效果就好了,根據(jù)你們的要求,看看到底調(diào)到幾

    /**
     打印出當前rect之內(nèi)的所有cell的“布局屬性”- UICollectionViewLayoutAttributes(返回的是一個數(shù)組)
     */
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        //獲得super已經(jīng)計算好的尺寸
        let array = super.layoutAttributesForElementsInRect(rect)
        
        //對于每個屬性進行尺寸,位置的微調(diào)
        for index in 0 ..< array!.count
        {
            let  abc:UICollectionViewLayoutAttributes  = array![index]
            //1.獲取collctionView最中間的線的X值
            let collectionViewX:CGFloat = (collectionView?.contentOffset.x)! + CGFloat((collectionView?.frame.width)!)*0.5
           //2.獲取cell的中心點位置
            let cellX = abc.center.x
           let scale =  abs(cellX - collectionViewX)/(collectionView?.frame.width)!
            let trueScale = 1-scale
            abc.transform = CGAffineTransformMakeScale(trueScale, trueScale)
            
        }
        return array
    }
隨著遠近改變大小
4.判斷哪個cell離colleview的中點最近

剛才寫完了如何將cell按照比例放大和縮小,還有就是如何放置到中間


思路
1.重寫過去cell停止應(yīng)該在什么位置的函數(shù)
2.找出rect中那個一個cell里黑線最近
3.獲取那個cell里黑線最近的距離,可能是正數(shù)也可能是負數(shù)
4.讓所有的cell都微調(diào) 合適的間距,最終讓那個特定的cell在屏幕中間

/**
     當結(jié)束后,cell應(yīng)該停止的位置
     
     :param: proposedContentOffset 打算停止的位置(通過速度計算出來的)
     :param: velocity              速度
     
     :returns: 最后停止的位置,你可能重新給數(shù)據(jù)了,就按照你給的位置停止,如果沒有重寫這個方法,那么就會返回proposedContentOffset這個位置(相對于當前的位置)
     */
    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        return CGPointZero
    }

4.1.當結(jié)束后,cell應(yīng)該停止的位置,正常cell停止的位置是
proposedContentOffset,如果你重寫了,那么cell停止后的位置就是你返回的位置

返回的是CGPointZero

4.2 找出rect中那個一個cell里黑線最近
求間距,還用剛才的那個公式用間距絕對值就行 abs(黑線.x - attri.center.x)行不行,為什么?
答案:不行,公式中獲取的cell的中心點是可控的,我們知道,那一時刻,必須的,但是為什么在本函數(shù)中,我們認為他不行,不是準確的值?,因為我們快速滑動,然后松手,他還有速度,根據(jù)慣性,他還要滑動一會,所以不能取此時此刻的黃線(cell的中點),所以我們的公式是 本函數(shù)返回值 = proposedContentOffset .x + 黃線和黑線最近的間距(可能正也可能負)


首先在本方法中,應(yīng)該獲取到rect中的cell的那些屬性(layoutAttributesForElementsInRect),如何獲取那個數(shù)組?
調(diào)用super.layoutAttributesForElementsInRect方法,可以輕松獲取的是計算好的cell的中心點。
如果使用的是self.layoutAttributesForElementsInRect,獲取的數(shù)據(jù)是經(jīng)過我們計算的,我們此時要拿到未經(jīng)過計算的attir數(shù)組

       //獲取將要顯示rect里面的cell屬性
        let attrs = super.layoutAttributesForElementsInRect(rect)

獲取完了數(shù)組,我們拿數(shù)組中的對象和中線比較一下,看看那個是最近的,做一個記錄,然后讓所有的返回都是本函數(shù)返回值 = proposedContentOffset .x + 黃線和黑線最近的間距(可能正也可能負)

    /**
     當結(jié)束后,cell應(yīng)該停止的位置
     
     :param: proposedContentOffset 打算停止的位置(通過速度計算出來的)
     :param: velocity              速度
     
     :returns: 最后停止的位置,你可能重新給數(shù)據(jù)了,就按照你給的位置停止,如果沒有重寫這個方法,那么就會返回proposedContentOffset這個位置(相對于當前的位置)
     */
    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
//        print(proposedContentOffset)
        
        //1.獲取所有的布局屬性,調(diào)用super的方法獲取
        //獲取將要顯示的rect值
        let rX:CGFloat = proposedContentOffset.x
        let rect = CGRectMake(rX, 0, (collectionView?.frame.width)!, (collectionView?.frame.height)!)
       //獲取將要顯示rect里面的cell屬性
        let attrs = super.layoutAttributesForElementsInRect(rect)
        
        //計算colletionView中間哪個線的x
        let centX:CGFloat = proposedContentOffset.x + (collectionView?.frame.width)!*0.5
        
        //保存最小的間距
        var margin = MAXFLOAT
        //2.遍歷屬性,獲取最小的間距
        for  index  in 0 ..< (attrs?.count)! {
            let att = attrs![index]
            let op = Float(att.center.x - centX)
            if abs(margin) > abs(op)
            {
                margin = Float(att.center.x - centX)
            }
        }
        
        
        //所有的cell都要偏移量 = proposedContentOffset.x + marign(margin可能是正負)
        
        var currentOffset = proposedContentOffset
        currentOffset.x += CGFloat(margin)

        return currentOffset
    }

4.3 計算collviontView的中心點值(注意,不能像過去的那個計算 過去:偏移量(手松開的哪一個可)+寬度的一般),此時拿到的偏移量事不準的,因為我們刺客還有速度,應(yīng)該拿到最后的偏移量,也就是propertyTargetPoint,將要去哪里!!!
4.3,要知道手松開的那一刻,的offSet是不準的,應(yīng)該獲取最終的,將要去的位置,所以傳遞rect是不能下傳遞,而是將來結(jié)束的時候,rect,用x最難判斷,就是propertyPointOffset.x
4.4 遍歷布局屬性,獲取那個距離最短,所有的都偏移這段多
4.5 明確,其實最后的所有的偏移量,= 最小的間距 + 目標偏移量!但是“最小的間距”可能是正負數(shù)!!


5.給cell的初始化和結(jié)束設(shè)置一個sectionInset ,了解prepare函數(shù)的使用

基本都講完了,但是剛剛啟動程序的時候,0號cell里左邊太近了,不好看,我們想第一個cell和最后一個cell都在屏幕中間,怎么辦?
給他sectionInset設(shè)置數(shù)據(jù)就好了,組間距

最終效果
    /**
     用來做布局的初始化,不建議在init中調(diào)用,因為那時候的colletionView = nil
     */
    override func prepareLayout() {
        super.prepareLayout()
        //設(shè)置sectionInset
        let magin:CGFloat =  ((collectionView?.frame.width)! - itemSize.width) * 0.5
        sectionInset = UIEdgeInsetsMake(0, magin, 0, magin)
    }

6.自頂一個有imageView的cell就好了~

同xib加載的,所以注冊的時候有點不同,是這樣的


        //注冊cell
       collectionView.registerNib(UINib.init(nibName: "SFImageCell", bundle: nil),
                                  forCellWithReuseIdentifier: sfCellIdent)
最終效果

demo地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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