iOS教你輕鬆打造瀑布流Layout
前言 :
在寫這篇文章之前, 先祝賀自己, 屬於我的GitHub終於來了. 這也是我的GitHub的第一份程式碼, 以下文章的程式碼均可以在Demo clone或下載. 歡迎大家給予意見. 覺得寫得不錯的也請不要吝惜你們的star.
瀑布流
先普及下什麼叫瀑布流
瀑布流,又稱瀑布流式佈局。是比較流行的一種網站頁面佈局,視覺表現為參差不齊的多欄佈局,隨著頁面滾動條向下滾動,這種佈局還會不斷載入資料塊並附加至當前尾部。最早採用此佈局的網站是Pinterest,逐漸在國內流行開來。
UICollectionView
我們知道, UICollectionView
是蘋果推出的繼UITableView
UITableView
的快取池, 重用機制外, 更能由你自主打造item(tableView中稱cell, collectionView中稱item)的顯示佈局, 只要你給它傳一個layout佈局屬性, 他就能按照你的意願去顯示item.
UICollectionViewLayout
這裡我就用自定義類JRWaterFallLayout
繼承UICollectionViewLayout來寫瀑布流佈局.
-
需要手動實現的4個方法
方法 | 說明 |
---|---|
– (void)prepareLayout | collectionView第一次佈局的時候和佈局失效的時候會呼叫該方法, 需要注意的是子類記得要呼叫super |
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect | 返回rect範圍內所有元素的佈局屬性的陣列 |
– (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath | 返回indexPath位置上的元素的佈局屬性 |
– (CGSize)collectionViewContentSize | 返回collectionView的滾動範圍 |
注意 : 因為layoutAttributesForElementsInRect方法呼叫十分頻繁, 所以佈局屬性的陣列應該只計算一次儲存起來而不是每次呼叫該方法的時候重新計算
-
代理
提供介面給外界修改一些瀑布流佈局的引數, 例如顯示的列數, 列距, 行距, 邊緣距(UIEdgeInsets), 假如代理不實現該方法, 則用預設的引數.. 最最重要的還是item的高度!! 因為每個圖片(item)的高度是由圖片的寬高比和itemWidth來共同決定的. 所以itemHeight必須由代理來決定.這裡展示幾個代理方法 :
代理方法 | 說明 |
---|---|
@required | |
– (CGFloat)waterFallLayout:(JRWaterFallLayout *)waterFallLayout heightForItemAtIndex:(NSUInteger)index width:(CGFloat)width | 返回index位置下的item的高度 |
@optional | |
– (NSUInteger)columnCountOfWaterFallLayout:(JRWaterFallLayout *)waterFallLayout | 返回瀑布流顯示的列數 |
– (CGFloat)rowMarginOfWaterFallLayout:(JRWaterFallLayout *)waterFallLayout | 返回行間距 |
– (CGFloat)columnMarginOfWaterFallLayout:(JRWaterFallLayout *)waterFallLayout | 返回列間距 |
– (UIEdgeInsets)edgeInsetsOfWaterFallLayout:(JRWaterFallLayout *)waterFallLayout | 返回邊緣間距 |
注意 : 由於上面所說的, layoutAttributesForElementsInRect方法呼叫十分頻繁, 所以代理方法勢必也會頻繁呼叫. 但是並不是所有代理方法都是@required的, 所以在呼叫@optional的代理方法時需要如下程式碼那樣每次都判斷代理是否響應了該選擇子, 以防代理沒有實現該方法, 呼叫導致程式崩潰
12345 | if([self.delegate respondsToSelector:@selector(columnCountOfWaterFallLayout:)]){_columnCount=[self.delegate columnCountOfWaterFallLayout:self];}else{_columnCount=JRDefaultColumnCount;} |
每次都這樣判斷顯然效率很低, 我們可以在prepareLayout方法中進行一次性判斷, 然後用一個flags結構體儲存起來, 那麼下次我們在呼叫的時候直接對flag進行判斷即可. 如下 :
123456789101112131415 | struct{// 記錄代理是否響應選擇子BOOLdidRespondColumnCount:1;// 這裡的1是用1個位元組儲存BOOLdidRespondColumnMargin:1;BOOLdidRespondRowMargin:1;BOOLdidRespondEdgeInsets:1;}_delegateFlags;-(void)setupDelegateFlags{_delegateFlags.didRespondColumnCount=[self.delegate respondsToSelector:@selector(columnCountOfWaterFallLayout:)];_delegateFlags.didRespondColumnMargin=[self.delegate respondsToSelector:@selector(columnMarginOfWaterFallLayout:)];_delegateFlags.didRespondRowMargin=[self.delegate respondsToSelector:@selector(rowMarginOfWaterFallLayout:)];_delegateFlags.didRespondEdgeInsets=[self.delegate respondsToSelector:@selector(edgeInsetsOfWaterFallLayout:)];}// 那麼下次呼叫方法的時候就變成下面那麼優雅了_columnCount=_delegateFlags.didRespondColumnCount?[self.delegate columnCountOfWaterFallLayout:self]:JRDefaultColumnCount; |
整個瀑布流layout最重要的是找到item擺放的位置. 正是layoutAttributesForItemAtIndexPath
方法要做的是. 下面開始說說找這個item位置的思路
瀑布流layout思路
這裡本人一共需要用到2個可變陣列和一個assign屬性, 一個用來記錄每列的高度, 一個用來記錄所有itemAttributes. assign用來記錄高度最大的列的高度
123456 | /** itemAttributes陣列 */@property(nonatomic,strong)NSMutableArray *attrsArray;/** 每列的高度陣列 */@property(nonatomic,strong)NSMutableArray *columnHeights;/** 最大Y值 */@property(nonatomic,assign)CGFloat maxY; |
而在prepareLayout
方法中, 以上2個數組都是要清空的, 因為網路請求的新資料到了, collectionView要重新佈局的時候如果不清空, 繼續往裡邊加東西的話, 會導致item的佈局就全部亂套了..
接下里要處理的就是layoutAttributesForItemAtIndexPath
方法中每個item該怎麼佈局了. 思路很簡單
- 建立一個UICollectionViewLayoutAttributes物件
- 根據collectionView的width及行間距等幾個引數, 計算出item的寬度
- 找到最短列的列號
- 根據列號計算item的x值, y值, 詢問代理拿到item的高度
- 設定UICollectionViewLayoutAttributes物件的frame屬性
- 返回UICollectionViewLayoutAttributes物件
問題主要出在怎麼計算出x, y, width, height上. 看圖說話.
詳細的計算步驟可以看Demo.
最後
理論上只要你有足夠強大的演算法計算能力, 什麼顯示佈局都能寫出來. collectionViewLayout並不止於瀑布流!