很簡答的瀑布流,用collectionview實現(xiàn)cell高度不一致的情況下的排列。
主要就是自定義layout。
先看效果:

AAA-BBB.gif
自定義layout必須實現(xiàn)下面5個方法:
//完成布局前的初始工作
-(void)prepareLayout;
//collectionView的內(nèi)容尺寸
-(CGSize)collectionViewContentSize;
//為每個item設(shè)置屬性(被下面的那個方法調(diào)用)
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
//獲取制定范圍的所有item的屬性(調(diào)用的是上面的那個方法,存到數(shù)組)
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
//在collectionView的bounds發(fā)生改變的時候刷新布局
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;
一、原理
layout文件負責collectionview的布局。所以在layout中計算完每個cell的坐標信息,就實現(xiàn)了瀑布流。所以瀑布流的重點是計算每個cell的位置信息。
cell的順序:從左上角是第0個cell,接著要放置第1個,這個怎么放?為了把屏幕鋪滿,只能放到最短的那列里。接著放第2個cell,也只放到最短的那一列里。以此類推,直到數(shù)據(jù)源里的最后一個。保證每次新增的cell放在最短的那一列。
經(jīng)過上面簡單的分析,cell的順序很重要。每次存放cell,都需要判斷最短是哪一列,然后再放。如圖:

排列順序.png
直接上代碼:
layout的h文件
#import <UIKit/UIKit.h>
//繼承自UICollectionViewFlowLayout
@interface PhotoLayout : UICollectionViewFlowLayout
//需要顯示幾列
@property (nonatomic, assign) NSInteger lines;
//左右cell的間距
@property (nonatomic, assign) CGFloat leftRSpace;
//上下cell的間距
@property (nonatomic, assign) CGFloat topBSpace;
//存放高度的數(shù)組,可以根據(jù)實際需求處理
@property (nonatomic, strong) NSMutableArray *heightArr;
@end
m文件:為了代碼看著連貫,直接全部復(fù)制了。
#import "PhotoLayout.h"
#define SCREENW [UIScreen mainScreen].bounds.size.width
@interface PhotoLayout ()
@property (nonatomic, strong) NSMutableArray *attrsArray; //cell的屬性數(shù)組,存放cell位置信息
@property (nonatomic, strong) NSMutableArray *columnHeights;//高度數(shù)組,存放每列高度。有幾列這個數(shù)組就包含幾個對象
@property(nonatomic,assign)CGFloat cellWidth; //cell的寬度
@end
@implementation PhotoLayout
//布局的準備
- (void)prepareLayout {
[super prepareLayout];
// 清空cell位置信息數(shù)據(jù)和高度數(shù)組
[self.attrsArray removeAllObjects];
[self.columnHeights removeAllObjects];
// 計算cell的寬度
self.cellWidth = (SCREENW - self.sectionInset.left - self.sectionInset.right - (self.lines-1)*self.leftRSpace ) * 0.5;
// 存放最原始高度,這個時候還沒有cell的高度,其實就是section的上邊距
for (int i = 0; i < self.lines; i ++) {
[self.columnHeights addObject:@(self.sectionInset.top)];
}
// 這里沒什么可注釋的,一看就明白。循環(huán)計算各個cell屬性。
NSInteger count = [self.collectionView numberOfItemsInSection:0];
for (NSInteger i = 0; i < count; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attrsArray addObject:attrs];
}
}
//cell屬性數(shù)組的懶加載
- (NSMutableArray *)attrsArray{
if (_attrsArray == nil) {
_attrsArray = [[NSMutableArray alloc] init];
}
return _attrsArray;
}
//高度數(shù)組的懶加載。根據(jù)自己需求,可以不用數(shù)組一次性計算所有數(shù)據(jù)的高度。也可以實時計算高度。
- (NSMutableArray *)columnHeights {
if (_columnHeights == nil) {
_columnHeights = [[NSMutableArray alloc] init];
}
return _columnHeights;
}
//計算每個cell的屬性信息
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
// 初始化屬性
UICollectionViewLayoutAttributes *atts = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
// 根據(jù)高度數(shù)組,循環(huán)一次,找出最短的那一列。就可以確定這個cell放到哪一列了。
// 取出第0個元素的高度
NSInteger columnIndex = 0;
CGFloat minHeight = [self.columnHeights[0] floatValue];
for (NSInteger i = 1; i < self.lines; i ++) {
// 根據(jù)第0個高度,和其他高度對比
CGFloat cellHeight = [self.columnHeights[i] floatValue];
// 確定最短的那個高度是多少,并記錄是哪一列的高度
if (minHeight > cellHeight) {
minHeight = cellHeight;
columnIndex = i;
}
}
// 根據(jù)section邊距,cell左右間隙和上面確定的cell所在的列,計算出cell的x坐標
CGFloat cellX = self.sectionInset.left + columnIndex * (self.leftRSpace + self.cellWidth);
// 根據(jù)確定的最短列的高度和cell的上線間隙,確定cell的y坐標
CGFloat cellY = minHeight + self.topBSpace;
// 根據(jù)高度數(shù)組,確定cell的高度
// --PS根據(jù)自己需求,可以不用數(shù)組一次性計算所有數(shù)據(jù)的高度。也可以實時計算高度。
CGFloat cellH = [self.heightArr[indexPath.row] floatValue];
// 最后確定cell的frame
atts.frame = CGRectMake(cellX, cellY, self.cellWidth, cellH);
// 由于新增了一個cell,更新這一列的高度
self.columnHeights[columnIndex] = @(minHeight + self.topBSpace + cellH);
return atts;
}
//通過調(diào)用上面的方法,把所有cell的屬性方法一個數(shù)組里
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
return self.attrsArray;
}
//實時更新collectionview的contentsize,由于每次屏幕上新出現(xiàn)cell就重新布局方法,contentsize也會變化
- (CGSize)collectionViewContentSize{
// 還是先取出第0列的高度
NSNumber *longestNum = self.columnHeights[0];
CGFloat longest = [longestNum floatValue];
// 循環(huán)一下哈,得到最長的高度。為了顯示完整,所以contentsize是有最長的列的決定的。
for (NSInteger i = 1; i < self.columnHeights.count; i++) {
NSNumber* rolHeight = self.columnHeights[i];
if(longest < rolHeight.floatValue){
longest = rolHeight.floatValue;
}
}
// 最長的列的高度,最后再加上section的底部間距
return CGSizeMake(self.collectionView.frame.size.width, longest + self.sectionInset.bottom);
}
//在collectionView的bounds發(fā)生改變的時候刷新布局
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
return !CGRectEqualToRect(self.collectionView.bounds, newBounds);
}
@end
二、使用
以上layout文件全局說完了。在控制器的調(diào)用就簡單多了。
// layout對象最好寫成全局變量,實際開發(fā)中,數(shù)據(jù)是在網(wǎng)絡(luò)請求后得到,所以高度的確定也在網(wǎng)絡(luò)請求后,
self.layout = [[PhotoLayout alloc] init];
// 為了代碼的重復(fù)使用方便,這些數(shù)據(jù)由控制器決定。
self.layout.sectionInset = UIEdgeInsetsMake(0, 25, 0, 25);
self.layout.lines = 2;
self.layout.leftRSpace = 15;
self.layout.topBSpace = 15;
self.layout.heightArr = [[NSMutableArray alloc] init];
self.collView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.layout];
self.collView.alwaysBounceVertical = YES;
self.collView.backgroundColor = [UIColor whiteColor];
self.collView.delegate = self;
self.collView.dataSource = self;
[self.view addSubview:self.collView];
//網(wǎng)絡(luò)請求
- (void)loadNetData {
for (int i = 0; i < 50; i++) {
//模擬數(shù)據(jù),得到不一樣的高度而已。
if (i % 3 == 0) {
[self.layout.heightArr addObject:@(150)];
}else{
[self.layout.heightArr addObject:@(100)];
}
}
[self.collView reloadData];
}
OVER!