一直以來都想研究瀑布流的具體實現方法(起因是因為一則男女程序員應聘的笑話,做程序的朋友應該都知道)。最近學習到了瀑布流的實現方法,瀑布流的實現方式有多種,這里應用collectionView來重寫其UICollectionViewLayout進行布局是最為簡單方便的。但再用其布局之前必須了解其布局原理。為方便大家學習理解此處補上demo地址https://github.com/PurpleSweetPotatoes/CollcetionViewLayout_demo
在這里筆者挑出其中較為重要的幾個方法來進行講解。
1.- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 當collectionView視圖位置有新改變(發生移動)時調用,其若返回YES則重新布局
2.- (void)prepareLayout 准備好布局時調用。此時collectionView所有屬性都已確定。讀者在這里可以將collectionView當做畫布,有了畫布后,我們便可以在其上面畫出每個item
3.- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 返回collectionView視圖中所有視圖的屬性(UICollectionViewLayoutAttributes)數組
4.- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath 返回indexPath對應item的屬性
5.- (CGSize)collectionViewContentSize 設置collectionView的可顯示范圍
這些方法中最重要的便是3,4方法,在3方法中返回所有視圖屬性數組,並根據這些屬性進行布局,而4方法則返回每個item的屬性,我們則在這里設置每個item的屬性(主要是frame),就可以讓collectionView按照我們的意願進行布局了!(在這里我們不需要用到1方法,若item屬性根據滑動改變,此時就需要隨時進行布局改變)
瀑布流的實現示意圖如下
由圖示意可看出除開最開始3個item,后面的item都是存放3列中的最短列上面,因此我們只需要計算出每個item的frame,並擺放的話那我們的瀑布流自然就成功了。
.h文件中
1 typedef CGFloat(^HeightBlock)(NSIndexPath *indexPath , CGFloat width); 2 @interface BQWaterLayout : UICollectionViewLayout 3 /** 列數 */ 4 @property (nonatomic, assign) NSInteger lineNumber; 5 /** 行間距 */ 6 @property (nonatomic, assign) CGFloat rowSpacing; 7 /** 列間距 */ 8 @property (nonatomic, assign) CGFloat lineSpacing; 9 /** 內邊距 */ 10 @property (nonatomic, assign) UIEdgeInsets sectionInset; 11 /** 12 * 計算各個item高度方法 必須實現 13 * 14 * @param block 設計計算item高度的block 15 */ 16 - (void)computeIndexCellHeightWithWidthBlock:(CGFloat(^)(NSIndexPath *indexPath , CGFloat width))block; 17 @end
為了方便修改瀑布流的布局我們需要設置排列布局的各個接口,因為瀑布流中只能通過列數計算出item的寬,因此需要使用computeIndexCellHeightWithWidthBlock來計算出每個item的高度(利用寬高比)!
.m文件中
代碼中注釋已經寫的很明白了,無需多做解釋,此處寫法只能實現item的布局,不能添加headview或footview!
1 @interface BQWaterLayout() 2 /** 存放每列高度字典*/ 3 @property (nonatomic, strong) NSMutableDictionary *dicOfheight; 4 /** 存放所有item的attrubutes屬性*/ 5 @property (nonatomic, strong) NSMutableArray *array; 6 /** 計算每個item高度的block,必須實現*/ 7 @property (nonatomic, copy) HeightBlock block; 8 @end 9 10 @implementation BQWaterLayout 11 - (instancetype)init 12 { 13 self = [super init]; 14 if (self) { 15 //對默認屬性進行設置 16 /** 17 默認行數 3行 18 默認行間距 10.0f 19 默認列間距 10.0f 20 默認內邊距 top:10 left:10 bottom:10 right:10 21 */ 22 self.lineNumber = 3; 23 self.rowSpacing = 10.0f; 24 self.lineSpacing = 10.0f; 25 self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10); 26 _dicOfheight = [NSMutableDictionary dictionary]; 27 _array = [NSMutableArray array]; 28 } 29 return self; 30 } 31 32 /** 33 * 准備好布局時調用 34 */ 35 - (void)prepareLayout{ 36 [super prepareLayout]; 37 NSInteger count = [self.collectionView numberOfItemsInSection:0]; 38 //初始化好每列的高度 39 for (NSInteger i = 0; i < self.lineNumber ; i++) { 40 [_dicOfheight setObject:@(self.sectionInset.top) forKey:[NSString stringWithFormat:@"%ld",i]]; 41 } 42 //得到每個item的屬性值進行存儲 43 for (NSInteger i = 0 ; i < count; i ++) { 44 NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0]; 45 [_array addObject:[self layoutAttributesForItemAtIndexPath:indexPath]]; 46 } 47 } 48 /** 49 * 設置可滾動區域范圍 50 */ 51 - (CGSize)collectionViewContentSize{ 52 NSLog(@"collectionViewContentSize"); 53 __block NSString *maxHeightline = @"0"; 54 [_dicOfheight enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *obj, BOOL *stop) { 55 if ([_dicOfheight[maxHeightline] floatValue] < [obj floatValue] ) { 56 maxHeightline = key; 57 } 58 }]; 59 return CGSizeMake(self.collectionView.bounds.size.width, [_dicOfheight[maxHeightline] floatValue] + self.sectionInset.bottom); 60 } 61 /** 62 * 計算indexPath下item的屬性的方法 63 * 64 * @return item的屬性 65 */ 66 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{ 67 //通過indexPath創建一個item屬性attr 68 UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; 69 //計算item寬 70 CGFloat itemW = (self.collectionView.bounds.size.width - (self.sectionInset.left + self.sectionInset.right) - (self.lineNumber - 1) * self.lineSpacing) / self.lineNumber; 71 CGFloat itemH; 72 //計算item高 73 if (self.block != nil) { 74 itemH = self.block(indexPath, itemW); 75 }else{ 76 NSAssert(itemH != 0,@"Please implement computeIndexCellHeightWithWidthBlock Method"); 77 } 78 //計算item的frame 79 CGRect frame; 80 frame.size = CGSizeMake(itemW, itemH); 81 //循環遍歷找出高度最短行 82 __block NSString *lineMinHeight = @"0"; 83 [_dicOfheight enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *obj, BOOL *stop) { 84 if ([_dicOfheight[lineMinHeight] floatValue] > [obj floatValue]) { 85 lineMinHeight = key; 86 } 87 }]; 88 int line = [lineMinHeight intValue]; 89 //找出最短行后,計算item位置 90 frame.origin = CGPointMake(self.sectionInset.left + line * (itemW + self.lineSpacing), [_dicOfheight[lineMinHeight] floatValue]); 91 _dicOfheight[lineMinHeight] = @(frame.size.height + self.rowSpacing + [_dicOfheight[lineMinHeight] floatValue]); 92 attr.frame = frame; 93 94 return attr; 95 } 96 /** 97 * 返回視圖框內item的屬性,可以直接返回所有item屬性 98 */ 99 - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{ 100 return _array; 101 } 102 /** 103 * 設置計算高度block方法 104 * 105 * @param block 計算item高度的block 106 */ 107 - (void)computeIndexCellHeightWithWidthBlock:(CGFloat (^)(NSIndexPath *, CGFloat))block{ 108 if (self.block != block) { 109 self.block = block; 110 } 111 } 112 @end
至此一個簡單的collectionViewLayout瀑布流布局便設置完成,只需要在自己代碼中使用此布局便可以得到一個瀑布流了!
下圖是筆者的效果圖:
2列效果 3列效果
后記:
筆者本來開始還擔心如果item過多,那么設置的屬性就會過多,比如數組內存放一千或一萬個item的屬性,后來經過筆者測試后發現,系統應該每次都是事先計算好了所有item的屬性(通過tableView計算每行高度的代理方法來思考),因此直接初始化好所有item的屬性做法應該不會有太大弊端!如果筆者所做有什么錯誤或不妥之處望指出!謝謝!