1. 程式人生 > >iOS開發之窺探UICollectionViewController(三) :使用UICollectionView自定義瀑布流

iOS開發之窺探UICollectionViewController(三) :使用UICollectionView自定義瀑布流

上篇部落格的例項是自帶的UICollectionViewDelegateFlowLayout佈局基礎上來做的Demo, 詳情請看iOS開發之窺探UICollectionViewController(二) –詳解CollectionView各種回撥。UICollectionView之所以強大,是因為其具有自定義功能,這一自定義就不得了啦,自由度非常大,定製的高,所以功能也是灰常強大的。本篇部落格就不使用自帶的流式佈局了,我們要自定義一個瀑布流。自定義的瀑布流可以配置其引數: 每個Cell的邊距,共有多少列,Cell的最大以及最小高度是多少等。

一.先入為主

先來看一下不同配置引數下執行後的效果吧,每張截圖的列數和Cell之間的邊距都有所不同,瀑布流的列數依次為2,3,8。有密集恐懼證的童鞋就不要看這些執行效果圖了,真的會看暈的。下面這些執行效果就是修改不同的配置引數來進行佈局的。看圖吧,關於瀑布流的效果就不囉嗦了。以下的效果就是使用自定義佈局做的,接下來將會介紹一下其實現原理。

二. UICollectionViewLayout

在介紹上述效果實現原理之前,需要介紹一下UICollectionViewLayout。UICollectionView的自定義功能就是自己去實現UICollectionViewLayout的子類,然後重寫相應的方法來實現Cell的佈局,先介紹一下需要重寫的方法,然後再此方法上進行應用實現上述瀑布流。好,廢話少說,幹活走起。

1.佈局預載入函式

當佈局首次被載入時會呼叫prepareLayout函式,見名知意,就是預先載入佈局,在該方法中可以去初始化佈局相關的資料。該方法類似於檢視控制器的ViewDidLoad方法,稍後回用到該方法。

Objective-C
1234 // The collection view calls -prepareLayout once at its first layout as the first message to the layout instance.// The collection view calls -prepareLayout again after layout is invalidated and before requerying the layout information.// Subclasses should always call super if they override.-(void)prepareLayout;

2.內容滾動範圍

下方是定義ContentSize的方法。該方法會返回CollectionView的大小,這個方法也是自定義佈局中必須實現的方法。說白了,就是設定ScrollView的ContentSize,即滾動區域。

Objective-C
12 // Subclasses must override this method and use it to return the width and height of the collection view’s content. These values represent the width and height of all the content, not just the content that is currently visible. The collection view uses this information to configure its own content size to facilitate scrolling.-(CGSize)collectionViewContentSize;

3. 下方四個方法是確定佈局屬性的,下方第一個方法返回一個數組,該陣列中存放的是為每個Cell繫結的UICollectionViewLayoutAttributes屬性,便於在下面第二個方法中去定製每個Cell的屬性。第三個方法就是根據indexPath來獲取Cell所繫結的layoutAtrributes, 然後去更改UICollectionViewLayoutAttributes物件的一些屬性並返回,第四個是為Header View或者FooterView來定製其對應的UICollectionViewLayoutAttributes,然後返回。

Objective-C
12345678 // UICollectionView calls these four methods to determine the layout information.// Implement -layoutAttributesForElementsInRect: to return layout attributes for for supplementary or decoration views, or to perform layout in an as-needed-on-screen fashion.// Additionally, all layout subclasses should implement -layoutAttributesForItemAtIndexPath: to return layout attributes instances on demand for specific index paths.// If the layout supports any supplementary or decoration view types, it should also implement the respective atIndexPath: methods for those types.-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect;// return an array layout attributes instances for all the views in the given rect-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath;-(UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath*)indexPath;-(UICollectionViewLayoutAttributes*)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath*)indexPath;

4.UICollectionViewLayoutAttributes

下方是UICollectionViewLayoutAttributes常用的屬性,你可以在上面第二個方法中去為下方這些屬性賦值,為Cell定製屬於自己的Attributes。由下方的屬性就對自定義佈局的的強大,在本篇部落格中只用到了下方的一個屬性,那就是frame。

Objective-C
123456789 @property(nonatomic)CGRectframe;@property(nonatomic)CGPointcenter;@property(nonatomic)CGSizesize;@property(nonatomic)CATransform3Dtransform3D;@property(nonatomic)CGRectbounds NS_AVAILABLE_IOS(7_0);@property(nonatomic)CGAffineTransformtransform NS_AVAILABLE_IOS(7_0);@property(nonatomic)CGFloatalpha;@property(nonatomic)NSIntegerzIndex;// default is 0@property(nonatomic,getter=isHidden)BOOLhidden;// As an optimization, UICollectionView might not create a view for items whose hidden attribute is YES

三. UICollectionViewLayout的應用

經過上面的簡單介紹,想必對UICollectionViewLayout有一定的瞭解吧,UICollectionViewLayout中還有好多方法,以後用到的時候在給大家介紹。接下來要使用自定義佈局來實現瀑布流。我們需要在UICollectionViewLayout的子類中實現相應的佈局方法,因為UICollectionViewLayout是虛基類,是不能直接被例項化的,所以我們需要新建一個佈局類,這個佈局類繼承自UICollectionViewLayout。然後去實現上述方法,給每個Cell定製不同的UICollectionViewLayoutAttributes。好了還是拿程式碼說話吧。

1.重寫prepareLayout方法去初始化一些資料,該方法在CollectionView重新載入時只會呼叫一次,所以把一些引數的配置,計算每個Cell的寬度,每個Cell的高度等程式碼放在預處理函式中。在該函式中具體呼叫的函式如下所示:

Objective-C
123456789101112131415 #pragma mark -- 虛基類中重寫的方法/**   * 該方法是預載入layout, 只會被執行一次   */-(void)prepareLayout{[superprepareLayout];[selfinitData];[selfinitCellWidth];[selfinitCellHeight];}

2.返回內容的範圍,即為CollectionView設定ContentSize。ContentSize的Width就是螢幕的寬度,而ContentSize的高度是一列中最後一個Cell的Y座標加上其自身高度的最大值。在此函式中會呼叫求CellY陣列中的最大值。具體實現程式碼如下:

Objective-C
123456789 /**  * 該方法返回CollectionView的ContentSize的大小  */-(CGSize)collectionViewContentSize{CGFloatheight=[self maxCellYArrayWithArray:_cellYArray];returnCGSizeMake(SCREEN_WIDTH,height);}

3.下面的方法是為每個Cell去繫結一個UICollectionViewLayoutAttributes物件,並且以陣列的形式返回,在我們的自定義瀑布流中,我們只自定義了Cell的frame,就可以實現我們的瀑布流,UICollectionViewLayoutAttributes的其他屬性我們沒有用到,由此可以看出自定義Cell佈局功能的強大。

Objective-C
1234567891011121314151617181920212223 /**   * 該方法為每個Cell繫結一個Layout屬性~   */-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect{[selfinitCellYArray];NSMutableArray*array=[NSMutableArrayarray];//add cellsfor(inti=0;i){NSIndexPath*indexPath=[NSIndexPath indexPathForItem:i inSection:0];UICollectionViewLayoutAttributes*attributes=[self layoutAttributesForItemAtIndexPath:indexPath];[array addObject:attributes];}returnarray;}

4. 通過下述方法設定每個Cell的UICollectionViewLayoutAttributes物件的引數,為了實現瀑布流所以我們只需要設定每個Cell的frame即可。每個cell的frame的確定是以列來定的,有所在列的上個Cell的Y座標來確定下個cell的位置。瀑布流實現關鍵點如下:

(1)Cell寬度計算:如果瀑布流的列數和Cell的Padding確定了,那麼每個Cell的寬度再通過螢幕的寬度就可以計算出來了。

(2)Cell高度計算:通過隨機數生成的高度

(3)Cell的X軸座標計算:通過列數,和Padding,以及每個Cell的寬度很容易就可以計算出每個Cell的X座標。

(4)Cell的Y軸座標計算:通過Cell所在列的上一個Cell的Y軸座標,Padding, 和 上一個Cell的高度就可以計算下一個Cell的Y座標,並記錄在Y座標的陣列中了。

Objective-C
123456789101112131415161718192021222324252627 /** * 該方法為每個Cell繫結一個Layout屬性~ */-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath{UICollectionViewLayoutAttributes*attributes=[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];CGRectframe=CGRectZero;CGFloatcellHeight=[_cellHeightArray[indexPath.row] floatValue];NSIntegerminYIndex=[self minCellYArrayWithArray:_cellYArray];CGFloattempX=[_cellXArray[minYIndex] floatValue];CGFloattempY=[_cellYArray[minYIndex] floatValue];frame=CGRectMake(tempX,tempY,_cellWidth,cellHeight);//更新相應的Y座標_cellYArray[minYIndex]=@(tempY+cellHeight+_padding);//計算每個Cell的位置attributes.frame=frame;returnattributes;}

5. initData方法主要是對資料進行初始化,在本篇部落格中為了先實現效果,我們暫且把資料給寫死。下篇部落格會在本篇部落格中的基礎上進行優化和改進,這些配置引數都會在Delegate中提供,便於靈活的去定製屬於你自己的瀑布流。本篇部落格中Demo的配置項先寫死就OK了,還是那句話,下篇部落格中會給出一些相應的代理,來定製我們的瀑布流。

Objective-C
12345678910111213 /** * 初始化相關資料 */-(void)initData{_numberOfSections=[self.collectionView numberOfSections];_numberOfCellsInSections=[self.collectionView numberOfItemsInSection:0];//通過回撥獲取列數_columnCount=5;_padding=5;_cellMinHeight=50;_cellMaxHeight=150;}

6.下方的方法是根據Cell的列數來求出Cell的寬度。因為Cell的寬度都是一樣的,每個Cell的間隔也是一定的。例如有5列Cell, 那麼Cell中間的間隔就有4(5-1)個,那麼每個Cell的寬度就是螢幕的寬度減去所有間隔的寬度,再除以列數就是Cell的寬度。如果沒聽我囉嗦明白的話,直接看程式碼吧,並不複雜。每個Cell的寬度和間隔確定了,那麼每個Cell的X軸座標也就確定了。程式碼如下:

Objective-C
1234567891011121314151617 /**   * 根據Cell的列數求出Cell的寬度   */-(void)initCellWidth{//計算每個Cell的寬度_cellWidth=(SCREEN_WIDTH-(_columnCount-1)*_padding)/_columnCount;//為每個Cell計算X座標_cellXArray=[[NSMutableArrayalloc] initWithCapacity:_columnCount];for(inti=0;i){CGFloattempX=i *(_cellWidth+_padding);[_cellXArray addObject:@(tempX)];}}

7. 根據Cell的最小高度和最大高度來利用隨機數計算每個Cell的高度,把每個Cell的高度記錄在陣列中,便於Cell載入時使用。具體程式碼如下: