瀑布流有几种实现方式,这里只说我写的这一种
首先瀑布流直观可以看出是块分布,不难想到用 UICollectionView
去实现。
其次就是需要我们自定义一个类继承 UICollectionViewFlowLayout
来计算瀑布流的显示方式。
在写代码以前,要想好瀑布流到底是什么原理排列的
在写代码以前,要想好瀑布流到底是什么原理排列的
在写代码以前,要想好瀑布流到底是什么原理排列的
重要的事情说三遍: )
其实总结出来就是一个问题:
每次Add一个Object的时候,要加在哪一列
没错,就是加在上一排最短的那一列
那么我们要怎么知道每个item高度呢?
因为每个使用这个Layout
的人的需求都不一样,我们不能要求每个item都只是一个图片(这里用图片作为例子的原因是image.size
)
所以我们需要Coder在使用这个Layout
的时候告诉我们这个item的高度
//这里类似UITableView的方法
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
而且这个高度是强制性要获取的,so。。。(@required
你懂得)
UICollectionViewWaterfallLayout.h
#import <UIKit/UIKit.h>
@class UICollectionViewWaterfallLayout;
@protocol UICollectionViewWaterfallLayoutDelegate <NSObject>
@required
- (CGFloat)waterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout heightForRowAtIndexPath:(NSInteger)index itemWidth:(CGFloat)itemWidth;
@optional
//列数
- (int)columnCountInWaterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout;
//每列间距(不算边距)
- (CGFloat)columnMarginInWaterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout;
//行间距(不算边距)
- (CGFloat)rowMarginInWaterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout;
//边距
- (UIEdgeInsets)edgeInsetsInWaterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout;
@end
@interface UICollectionViewWaterfallLayout : UICollectionViewLayout
@property (nonatomic ,weak) id<UICollectionViewWaterfallLayoutDelegate> delegate;
@end
同时我也定义了4个Optional方法,Coder有需求就对号入座去实现就好了
UICollectionViewWaterfallLayout.m
既然有4个可修改项,那么我们就需要一些Default属性(不了解UICollectionView有哪些可以改的东西,例如边距,可以自己Com+点进去自己看看,这里就不多做解释了)
//默认的列数
static const NSInteger DefaultColumnCount = 3;
//每一列之间的间距
static const CGFloat DefaultColumnMargin = 10;
//没一行之间的间距
static const CGFloat DefaultRowMargin = 10;
//边缘间距
static const UIEdgeInsets DefaultEdgeInsets = {10, 10, 10, 10};
- (CGFloat)rowMargin
{
if ([self.delegate respondsToSelector:@selector(rowMarginInWaterfallLayout:)]) {
return [self.delegate rowMarginInWaterfallLayout:self];
} else {
return DefaultRowMargin;
}
}
- (CGFloat)columnMargin
{
if ([self.delegate respondsToSelector:@selector(columnMarginInWaterfallLayout:)]) {
return [self.delegate columnMarginInWaterfallLayout:self];
} else {
return DefaultColumnMargin;
}
}
- (NSInteger)columnCount
{
if ([self.delegate respondsToSelector:@selector(columnCountInWaterfallLayout:)]) {
return [self.delegate columnCountInWaterfallLayout:self];
} else {
return DefaultColumnCount;
}
}
- (UIEdgeInsets)edgeInsets
{
if ([self.delegate respondsToSelector:@selector(edgeInsetsInWaterfallLayout:)]) {
return [self.delegate edgeInsetsInWaterfallLayout:self];
} else {
return DefaultEdgeInsets;
}
}
前面已经说了如何布局的思想
这里需要创建3个属性来实现我们的想法
//存放所有cell的布局属性
@property (nonatomic, strong) NSMutableArray *attrsArray;
//存放所有列的当前高度
@property (nonatomic, strong) NSMutableArray *columnHeights;
//内容的高度
@property (nonatomic, assign) CGFloat contentHeight;
重写其中两个简单的方法 我就不多做解释了 注释已经写的很详细了
//方法是返回UICollectionView的可滚动范围
- (CGSize)collectionViewContentSize {
return CGSizeMake(0, self.contentHeight + self.edgeInsets.bottom);
}
//方法返回的是一个装着UICollectionViewLayoutAttributes的数组
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
return self.attrsArray;
}
划重点了!!!
重写核心方法
- (void)prepareLayout;
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
首先reload的时候不难发现会走- (void)prepareLayout;
方法,所以每次我们都要清空之前的计算记录再重新计算
注:这个方法必须调用一下父类的实现,不然你可以试试会怎么样🙃
[super prepareLayout];
//清除之前计算的所有高度,因为刷新的时候回调用这个方法
self.contentHeight = 0;
[self.columnHeights removeAllObjects];
[self.attrsArray removeAllObjects];
第一件事,我们是要确定瀑布流的列数
int forCount = [self.delegate respondsToSelector:@selector(columnCountInWaterfallLayout:)] ? [self.delegate columnCountInWaterfallLayout:self] : DefaultColumnCount;
然后向我们的数组中加入初始值(这里不加会崩溃)
for (NSInteger i = 0; i < forCount; i++) {
[self.columnHeights addObject:@(self.edgeInsets.top)];
}
开始创建每一个cell对应的布局属性
for (int section = 0; section < self.collectionView.numberOfSections; section++) {
for (NSInteger i = 0; i < [self.collectionView numberOfItemsInSection:section]; i++) {
// 创建位置
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section];
// 获取indexPath位置cell对应的布局属性
UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attrsArray addObject:attrs];
}
}
这样- (void)prepareLayout;
就算是完成了,这里面调用了另一个需要重写的方法- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
里面大部分都是计算和存储高度的code,就不多做解释了
//方法返回indexPath位置的UICollectionViewLayoutAttributes
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGFloat collectionViewW = self.collectionView.frame.size.width;
CGFloat w = (collectionViewW - self.edgeInsets.left - self.edgeInsets.right -(self.columnCount - 1) * self.columnMargin) / self.columnCount;
CGFloat h = [self.delegate waterfallLayout:self heightForRowAtIndexPath:indexPath.item itemWidth:w];
NSInteger destColumn = 0;
CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];
for (NSInteger i = 0; i < self.columnCount; i++) {
CGFloat columnHeight = [self.columnHeights[i] doubleValue];
if (minColumnHeight > columnHeight) {
minColumnHeight = columnHeight;
destColumn = i;
}
}
CGFloat x = self.edgeInsets.left + destColumn * (w + self.columnMargin);
CGFloat y = minColumnHeight;
if (y != self.edgeInsets.top) {
y += self.rowMargin;
}
attrs.frame = CGRectMake(x, y, w, h);
self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
CGFloat columnHeight = [self.columnHeights[destColumn] doubleValue];
if (self.contentHeight < columnHeight) {
self.contentHeight = columnHeight;
}
return attrs;
}
到此为止,这个类算是完成了
然后到Controller里
<UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewWaterfallLayoutDelegate>
UICollectionViewWaterfallLayout *layout = [[UICollectionViewWaterfallLayout alloc] init];
layout.delegate = self;
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
同UITableView一样把该写的都写上
到了要实现那个必须实现的@required
的方法的时候,也许真正用到项目里的Coder就会发现一个问题,大部分的情况下瀑布流是用来展示 图片+Something,而图片是网络请求加载,异步获取到图片才能拿到宽高,这里我们项目也遇到过,也实现过这种需求,但体验不太好,不!是非常不好!
也许是因为我对于写PlaceHolderCell的理解不够(个人觉得微博的效果就不错,但我还不会😓),经过跟别的公司的小朋友讨论+用户体验的考虑下,90%的小朋友们的做法是后台在返回图片Url的同时给你图片的W+H,这样就能完美解决问题了,由于我们后台云存储获取图片的时候可在Url后控制返回图片的大小,所以比较容易的实现了。
搞一个Model
@interface ImageModel : NSObject
//为了用户体验,异步加载网络图片,我们公司讨论结果是后台存储好图片宽高,与json数据一起返回,这样用户体验比较好
@property (nonatomic, readonly) CGFloat imageWidth;
@property (nonatomic, readonly) CGFloat imageHeight;
@property (nonatomic, strong) NSString *imageUrl;
@end
返回去实现协议方法
- (CGFloat)waterfallLayout:(UICollectionViewWaterfallLayout *)waterfallLayout heightForRowAtIndexPath:(NSInteger)index itemWidth:(CGFloat)itemWidth {
ImageModel *model = self.dataArray[index];
return itemWidth * model.imageHeight / model.imageWidth;
}
至此如果没什么问题运行应该就能看到效果了
有问题可以留言😯
有帮助可以❤️一下