思路
- 瀑布流核心思想是,每张图片都是从高度最低的一列去拼接,同时图片的尺寸取决于需求,而不是统一的尺寸,进而形成参差不齐的效果
- 通过UICollectionView的自定义UICollectionViewLayout(布局)去实现
- 相比用ScrollerView去实现瀑布流,UICollectionView更为简单,因为它已经具备循环利用的功能,如果使用ScrollerView还需要自己去实现循环利用
步骤
- 新建一个UICollectionViewWaterLayout,继承UICollectionViewLayout
- 设置代理UICollectionViewWaterLayoutDelegate,用于获取每个item的高度、列数、列间距、行间距等,其中item的高度是必须的,外界通过内部提供的宽度,按比例计算出高度。之所以用代理的方式去获取这些属性,而不是直接用property属性,是因为代理什么时候调用是内部决定的,当用property属性去获取这些属性时,外界可以在任何时候去改变这些属性,这就意味着内部要随时针对这种改变去做响应处理(重写set方法)比较麻烦。
protocol UICollectionViewWaterLayoutDelegate <NSObject>
@required
// 返回item的高度(根据width按比例去计算)
- (CGFloat)waterLayout:(UICollectionViewWaterLayout *)waterLayout heightForItemAtIndexPath:(NSIndexPath *)indexPath andWidth:(CGFloat)width;
@optional
// 返回列数
- (NSInteger)numberOfColumnsInWaterLayout:(UICollectionViewWaterLayout *)waterLayout;
// 返回行间距
- (CGFloat)rowMarginForWaterLayout:(UICollectionViewWaterLayout *)waterLayout;
// 返回列间距
- (CGFloat)columnMarginForWaterLayout:(UICollectionViewWaterLayout *)waterLayout;
// 返回内边距
- (UIEdgeInsets)edgeInsetsForWaterLayout:(UICollectionViewWaterLayout *)waterLayout;
@end
/** 缓存每个item的UICollectionViewLayoutAttributes属性,避免重复计算 */
property (nonatomic, strong) NSMutableArray *attributesArr;
/** 记录每列的高度 */
property (nonatomic, strong) NSMutableArray *columnHeights;
- 重写prepareLayout方法去初始化布局,注意要先调用父类同名方法
- (void)prepareLayout {
// 要先调用父类的prepareLayout
[super prepareLayout];
// 清空原来缓存的列的高度和cell的未知属性,否则当重新调用prepareLayout,会累加
[self.attributesArr removeAllObjects];
[self.columnHeights removeAllObjects];
// 设置列高度初始值为顶部内边距
for (int i = 0; i < layoutColumn; i++) {
[self.columnHeights addObject:[NSNumber numberWithDouble:layoutInsets.top]];
}
// 设置每个item的布局属性,并缓存
NSInteger itemsTotal = [self.collectionView numberOfItemsInSection:0];
for (int i = 0; i < itemsTotal; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.frame = [self getItemFrameWithIndexPath:indexPath]; [self.attributesArr addObject:attributes];
}
}
- 瀑布流核心计算方法,之所以自己定义一个方法去计算,而不是直接在layoutAttributesForItemAtIndexPath:实现计算方法,并调用它来获取每个item的位置属性,是因为layoutAttributesForItemAtIndexPath:会被系统重复调用,导致列的高度计算错误
/** 每个item都是放到当前高度最低的列下面
* 要计算x,y,就要找到高度最低的列
* 宽度 = (collectionView宽度 - 左内边距 - 右内边距 - (列数 - 1)*列间距)/列数 * 高度通过代理获取
* x = 左内边距 + 列号 * (宽度 + 列间距)
* y = 列的高度 + 行间距
*/
- (CGRect)getItemFrameWithIndexPath:(NSIndexPath *)indexPath {
// 高度最低的列
CGFloat minHeight = [self.columnHeights[0] doubleValue];
NSInteger minHeightCol = 0;
for (int i = 1; i < self.columnHeights.count; i++) {
CGFloat columnHeight = [self.columnHeights[i] doubleValue];
if (minHeight > columnHeight) {
minHeight = columnHeight; minHeightCol = i;
}
}
// 计算item的位置
CGFloat collectionViewW = self.collectionView.frame.size.width;
CGFloat itemW = (collectionViewW - self.insets.left - self.insets.right - (self.columns - 1)*self.columnMargin) / self.columns;
// 通过使用者实现的代理拿到item的高度
CGFloat itemH = [self.delegate waterLayout:self heightForItemAtIndexPath:indexPath andWidth:itemW];
CGFloat itemX = (itemW + self.columnMargin) * minHeightCol;
CGFloat itemY = minHeight + self.rowMargin; CGRect frame = CGRectMake(itemX, itemY, itemW, itemH);
// 更新列的高度
self.columnHeights[minHeightCol] = [NSNumber numberWithDouble:CGRectGetMaxY(frame)];
return frame;
}
- 重写layoutAttributesForElementsInRect:,告诉collectionView每个item以什么形式排布,这个方法会被反复调用,所以不适合将计算过程写在这个里面,而是直接用上面计算好的缓存数据
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
return self.attributesArr;
}
- 重写layoutAttributesForItemAtIndexPath:方法,如果不重写,UICollectionView在调用切换布局方式的方法时会崩溃
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
return self.attributesArr[indexPath.item];
}
- 重写collectionViewContentSize,否则UICollectionView无法滚动,返回的就是滚动范围
- (CGSize)collectionViewContentSize {
// 找到高度最高的列
// 滚动高度 = 高度最高的列的高度 + 底部内边距
// 记录最低高度
CGFloat maxHeight = [self.columnHeights[0] doubleValue];
for(int i = 1; i < self.columnHeights.count; i++) {
CGFloat columnHeight = [self.columnHeights[i] doubleValue];
if (maxHeight < columnHeight) {
maxHeight = columnHeight;
}
}
return CGSizeMake(0, maxHeight + layoutInsets.bottom);
}