//主控制器中
#import "ViewController.h"
#import "WaterFallCollectionViewCell.h"
#import "WaterfallFlowLayout.h"
static const NSInteger imageCount = 18;//图片的数量
static NSString * identifier = @"cellID";//重用机制
@interface ViewController ()
//定义一个数组用来装图片
@property (nonatomic,strong)NSArray * imageArray;
//定义一个collctionView
@property (nonatomic,strong)UICollectionView * colletionView;
@end
@implementation ViewController
#pragma mark - 懒加载
-(NSArray *)imageArray{
if (!_imageArray) {
NSMutableArray * tempArray = [NSMutableArray array];
for (int i = 0; i < imageCount; i++) {
UIImage * image = [UIImage imageNamed:[NSString stringWithFormat:@"image%d",i+1]];
[tempArray addObject:image];
}
_imageArray = tempArray;
}
return _imageArray;
}
- (void)viewDidLoad {
[super viewDidLoad];
//首先创建一个布局对象,这个对象的是自己写的布局类的对象
WaterfallFlowLayout * layout = [[WaterfallFlowLayout alloc]init] ;
self.colletionView = [[UICollectionView alloc]initWithFrame:[UIScreen mainScreen].bounds collectionViewLayout:layout];
self.colletionView.backgroundColor = [UIColor orangeColor];
//两个代理的设置,代理一般就是控制器
self.colletionView.delegate = self;
self.colletionView.dataSource = self;
//注册cell,以便重用,tableview中不用注册,但是collectionview中需要注册
//这里注册的类应该是自己所使用的cell的类,就是自定义的或者系统提供的
[self.colletionView registerClass:[WaterFallCollectionViewCell class] forCellWithReuseIdentifier:identifier];
[self.view addSubview:self.colletionView];
}
我们将collectionview定义为一个属性变量,并在viewDidLoad中对其进行设置:首先我们创建了一个布局对象(layout),类型是我们自己定义的布局类(WaterfallFlowLayout),接着我们又对属性变量collectionview进行了创建,设置了他的frame。然后就是对其代理的设置,collectionview的代理有三个,除了和tableview相同的代理和数据源之外,还有一个布局的代理(UICollectionViewDelegateFlowLayout),这里只设置了两个代理,就是数据源和处理事件的代理。这里需要注意的是tableview的重用机制不需要注册,但是collectionview必须要注册,注册的类是自己定义的cell的类(WaterFallCollectionViewCell),然后再跟上标识。值得一提的是collectionview只能采用重用的方式来加载cell。
#pragma mark - 设置数据源
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return self.imageArray.count;
}
//collectionview的重用和tableview不同,后者可以不用重用,但是前者必须使用重用机制
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
WaterFallCollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
cell.image = [self.imageArray objectAtIndex:indexPath.item];
return cell;
}
我们这里只有一个段,所以直接返回图片数组的数量就可以了,然后我们又重写了cellForItemAtIndexPath方法,在这个方法里面我们创建了一个自定义的cell (WaterFallCollectionViewCell) ,我们在自定义这个类的时候,给了cell一个属性image,设置好之后,我们就将它返回给代理。
#pragma mark - item的size
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
UIImage * image = [self.imageArray objectAtIndex:indexPath.item];
CGSize newSize = [self imageNewSizeWitholdWidth:image.size.width andoldHeight:image.size.height];
return newSize;
}
#pragma mark - 图片压缩后的新的高度
-(CGSize)imageNewSizeWitholdWidth:(CGFloat)width andoldHeight:(CGFloat)height{
/*
图片高度/图片宽度 = 压缩后图片高度/压缩后图片宽度 图片的宽高比不变
(我们规定压缩后图片宽度是固定的)
*/
CGFloat newWidth = (self.colletionView.bounds.size.width - kColSpacing*(columnCount + 1))/columnCount;
CGFloat newHeight = height/width * newWidth;
return CGSizeMake(newWidth, newHeight);
}
由于我们要展示的是图片,所以非常关心图片压缩后的宽高比。第一个方法是用来返回item的size的,它是UICollectionViewDelegateFlowLayout的可选方法,它里面有一个我们自己定义的 imageNewSizeWitholdWidth 的方法,我们用这个方法来对图片按照我们的想法进行压缩,具体公式在代码的注释块中。基本思想就是保持宽高比不变。方法中定义的一些如 kcolspacing,columncount 等常量在后面介绍。
#import
static CGFloat kColSpacing = 10;//列与列之间的间隔
static CGFloat const columnCount = 3;//列的个数
static UIEdgeInsets edgeInsets = {10,10,10,10};//内边距的集合,依次是上,左,下,右(逆时针)
@interface WaterfallFlowLayout : UICollectionViewFlowLayout
@end
可以从上面的代码中看到,我们自定义的布局类是继承于流式布局类的。
#import "WaterfallFlowLayout.h"
@interface WaterfallFlowLayout()
@property (nonatomic,assign)id delegate;
@property (nonatomic,strong)NSMutableArray * columnHeightArray;//存放列高度的数组,用于后面比较从最小列开始排
@property (nonatomic,strong)NSMutableDictionary * cellInfoDic;//用于存放cell位置信息的字典
@property (nonatomic,assign)NSInteger cellCount;//cell的总个数
@end
在属性变量中我们定义了一个 一个delegate,一个用于存放列高度的数组,一个存放cell位置信息的字典,一个cell的总个数。具体的每个变量的作用在后面会介绍。
#pragma mark - 准备布局
-(void)prepareLayout{
//准备布局
[super prepareLayout];
//初始化列高度的数组
_columnHeightArray = [NSMutableArray array];
//初始化cell信息的字典
_cellInfoDic = [NSMutableDictionary dictionary];
//设置代理为主控制器,我们的瀑布流是继承于系统的流式布局,而流式布局又继承于collectioinviewlayout,它里面有collectionview
self.delegate = (id)self.collectionView.delegate;
//获取cell的总个数
_cellCount = [self.collectionView numberOfItemsInSection:0];
if (_cellCount == 0) {
return;
}
float top = 0;
for (int i = 0 ; i < columnCount; i++) {
//先将里面初始化为0,因为开始的时候item距离最上面的距离是0
[_columnHeightArray addObject:[NSNumber numberWithFloat:top]];
}
//循环调用layoutForItemAtIndexPath方法,为每个cell布局,将indexPath传入,作为布局字典的key
//layoutAttributesForItemAtIndexPath方法的实现,这里用到了一个布局字典,其实就是将每个cell的位置信息与indexPath相对应,将它们放到字典中,方便后面视图的检索
for (int i = 0; i < _cellCount; i++) {
[self layoutItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
}
}
我们重写了准备布局的方法,我们在里面设置了这个类的代理是主控制器。如果不设置的话,布局就不能体现出来,就是说最终的界面是显示不出来的,因为没有通过主控制器显示出来。对于高度的数组,我们首先将里面的元素都设置为0,因为在刚开始的时候第一个item距离顶端的距离就是0。然后我们注意到最后调用了一个我们自己写的方法 layoutItemAtIndexPath ,它的实现如下:
#pragma mark - 将各个cell的fream等信息放入字典中
-(void)layoutItemAtIndexPath:(NSIndexPath *)indexPath{
//通过delegate获取item的大小,之前在主控制器中设置过了,其中layout是uicollectionview类的,所以传自身就可以了
CGSize itemSize = [self.delegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath];
//然后比较列数组中的列的高度,找出最小高度的列
float colindex = 0;
float shorterHeight = [[self.columnHeightArray objectAtIndex:colindex]floatValue];
for (int i = 0; i < columnCount; i++) {
float height = [[self.columnHeightArray objectAtIndex:i]floatValue];
if (height < shorterHeight) {
shorterHeight = height;
//记下此时是第几列
colindex = i;
}
}
//把上一排最小的列的高度取出来便于后面确定坐标
float lastMinHeight = [[self.columnHeightArray objectAtIndex:colindex]floatValue];
//确定当前cell的frame
CGRect frame = CGRectMake(edgeInsets.left +colindex*(kColSpacing + itemSize.width), edgeInsets.top+lastMinHeight, itemSize.width, itemSize.height);
//更新列高度表中的数据
[self.columnHeightArray replaceObjectAtIndex:colindex withObject:[NSNumber numberWithFloat: frame.origin.y +itemSize.height]];
//设置cell信息的字典,每个frame对应一个indexpath
[self.cellInfoDic setObject:indexPath forKey:NSStringFromCGRect(frame)];
}
在这个方法中,我们首先通过delegate获得了在主控制器中设置过的itemsize。既然要实现实现瀑布流,就需要比较每一列的高度,然后把要插入的item插入到高度最小的那一列去。因此,我们需要比较每一列的高度,找出最小列。找出了高度最小的那一列之后,我们需要设置要插入的item的的位置,所以取出高度最小的那一列的高度,用它来确定item是插入到最小高度那一列的。高度取出来之后,我们就要设置所要插入item的frame了,这里就需要我们之前得到的额itemsize了,我们设想的是,四周都距离10。设置好这一个item的frame之后,我们需要更新列高度数组中的数据,以便于下一次比较还是找出高度最小的列进行插入。最后,我们按照每一个frame对应的indexPath方式将一个cell对应的位置,frame信息存入字典。以便于后面显示的时候直接从字典中取就可以了。
这个方法在 prepareLayout 中进行了循环调用,循环了18次,也就是说每一个cell都需要进行布局,以便字典中存储了每一个cell的布局信息。
对于通过滑动来获取更多的信息的机制,我们不应该一次性把信息加载完,这样的效率很低,应该在滑动时再去加载应该加载的信息。
要实现这种效果,我们需要重写两个方法: layoutAttributesForElementsInRect 和 layoutAttributesForItemAtIndexPath
#pragma mark - 返回可见cell的indexPath
-(NSArray *)indexPathsOfItem:(CGRect)rect{
//遍历布局字典通过CGRectIntersectsRect方法确定每个cell的rect与传入的rect是否有交集,如果结果为true,则此cell应该显示,将布局字典中对应的indexPath加入数组
//NSLog(@"indexPathsOfItem");
NSMutableArray *array = [NSMutableArray array];
for (NSString *rectStr in _cellInfoDic) {
CGRect cellRect = CGRectFromString(rectStr);
if (CGRectIntersectsRect(cellRect, rect)) {
NSIndexPath *indexPath = _cellInfoDic[rectStr];
[array addObject:indexPath];
}
}
//返回的这个数组只是屏幕中可以看得见的cell,看不见的那些cell在滑动时会重新计算
return array;
}
//返回cell的布局信息,如果忽略传入的rect一次性将所有的cell布局信息返回,图片过多时性能会很差
//这个方法返回的一定数量的cell的属性的数组,一定数量指的是出现在屏幕中的cell,不包括滑动出现的cell
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
NSMutableArray *muArr = [NSMutableArray array];
//indexPathsOfItem方法,根据传入的frame值计算当前应该显示的cell
NSArray *indexPaths = [self indexPathsOfItem:rect];
//NSLog(@"hhh%@",indexPaths);
for (NSIndexPath *indexPath in indexPaths) {
// UICollectionViewLayoutAttributes *attribute = [self layoutAttributesForItemAtIndexPath:indexPath];
UICollectionViewLayoutAttributes *attribute = [self layoutAttributesForItemAtIndexPath:indexPath];
[muArr addObject:attribute];
}
return muArr;
}
//代理方法返回每个cell属性
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
//这个cell的属性
//NSLog(@"hhhh%@",indexPath);
UICollectionViewLayoutAttributes * attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
for (NSString * rectStr in self.cellInfoDic) {
if ([self.cellInfoDic valueForKey:rectStr] == indexPath) {
//说明是对应的
attributes.frame = CGRectFromString(rectStr);
}
}
return attributes;
}
第一个方法有一个参数 rect 这个rect的大小是 {0,-736,414,1472}(iphon8plus下),意思就是说这个rect就是两倍的屏幕大,但是屏幕下端的就没有在他那个范围之内。那如何判断cell的rect是否在参数的rect之内呢?我们可以用 CGRectIntersectsRect 这个方法来判断,这个方法的意思是如果两个rect没有交集的话,就返回 no,如果有交集就返回yes。我们写了一个方法 indexPathsOfItem 来将需要显示出来的cell的indexPath放入数组中,以便于在后面取某个cell方便。然后我们在下面的那个方法里面调用这个方法,他返回给我们需要显示的cell对应的indexPath的数组,接着我们通过遍历这个数组取出其中的cell对应的indexPath,然后调用 layoutAttributesForItemAtIndexPath这个方法来得到对应cell的属性(attribute)。这个 layoutAttributesForItemAtIndexPath 方法我们也是重写了的,在这个方法里面既然我们返回对应indexpath的cell的属性 ,就应该判断这个indexpath是不是对应的这个cell,所以我们写了一个判断的方法,获取cell的属性当然还是通过之前的cellInfoDic来获取。
//实现这个方法,跟前面找出最小的列高度的方法如出一辙
-(CGSize)collectionViewContentSize{
CGSize size = self.collectionView.frame.size;
float maxHeight = [[self.columnHeightArray objectAtIndex:0]floatValue];
for (int i = 0; i < columnCount; i++) {
float height = [[self.columnHeightArray objectAtIndex:i]floatValue];
if (height > maxHeight) {
maxHeight = height;
}
}
size.height = maxHeight + 10;
return size;
}
还是和之前一样,滑动的高度是由最长的那一列的高度决定的,所以我们需要找出最长的那一列。这就和之前找最短一列道理是一样的,这里直接上代码了。
由于我们的cell是用来展示图片的,所以自然就有image这个属性
#import
@interface WaterFallCollectionViewCell : UICollectionViewCell
@property (nonatomic,strong)UIImage * image;
@end
可以看到,我们自定义的cell是继承于原始的cell的。
#import "WaterFallCollectionViewCell.h"
#import "WaterfallFlowLayout.h"
#define WIDTH ((self.superview.bounds.size.width - kColSpacing*(columnCount + 1))/3)
@implementation WaterFallCollectionViewCell
-(void)setImage:(UIImage *)image{
if (_image != image) {
_image = image;
}
}
-(void)drawRect:(CGRect)rect{
float newHeight = _image.size.height / _image.size.width * WIDTH;
[_image drawInRect:CGRectMake(0, 0, WIDTH, newHeight)];
self.backgroundColor = [UIColor grayColor];
}
@end
drawRect 方法由系统调用,drawInRect 方法将图片显示到参数rect中。