UICollectionView的自定义布局和简单使用

前言

最近项目中使用了很多UICollectionView,发现对其了解真是太少了,UICollectionViewLayout的自定义布局真的可以实现很多效果,趁这次机会好好记录一下知识点,以前虽然用过,但没有系统的整理总结过。这两天我为UICollectionView做一个整理。包括基本使用,自定义布局,动画这块我会的不多后面有机会在再记下来。

UICollectionView的构成部分

Cells
Supplementary Views 追加视图 (类似Header或者Footer)
Decoration Views 装饰视图 (用作背景展示)

而在表面下,由两个方面对UICollectionView进行支持。其中之一和tableView一样,即提供数据的UICollectionViewDataSource以及处理用户交互的UICollectionViewDelegate和布局方面的UICollectionViewDelegateFlowLayout。另一方面,对于cell的样式和组织方式,由于collectionView比tableView要复杂得多,因此没有按照类似于tableView的style的方式来定义,而是专门使用了一个类来对collectionView的布局和行为进行描述,这就是UICollectionViewLayout。

UICollectionView的创建

UICollectionView的创建方式和UITableview相似都是init方法,不同的是UICollectionView初始化时需要传入一个UICollectionViewLayout对象用于对其Item进行布局,这个UICollectionViewLayout对象可以使用系统的UICollectionViewFlowLayout,也可以自定义,如果想要实现一些炫酷的动画或者特定的样式(例如瀑布流)都需要去自定义这个对象。下面是创建UICollectionView的代码。

-(DWCollectionView*)dwCollectionView{
        if (!_dwCollectionView) {
        UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
       layout.itemSize = CGSizeMake(100, 30);
        layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

        _dwCollectionView = [[DWCollectionView alloc] initWithFrame:CGRectMake(0, 64, self.view.bounds.size.width, 400) collectionViewLayout:layout];
        [_dwCollectionView registerClass:[DWCollectionViewCell class] forCellWithReuseIdentifier:@"UICollectionViewCellID"];
        _dwCollectionView.delegate = self;
        _dwCollectionView.dataSource = self;
        _dwCollectionView.backgroundColor = [UIColor yellowColor];
    }
    return _dwCollectionView;
}

UICollectionView的常用属性和代理方法

UICollectionView有三个代理,除去和UITableview一样的delegate和dataSource,还有一个ios
10新增的prefetchDataSource,三个代理中前两个delegate用于处理用户交互,dataSource用于提供数据源。

dataSource代理方法

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;

// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath:
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;

delegate常用代理方法

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;

UICollectionViewDelegateFlowLayout

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section;

属性方法

- (void)registerClass:(nullable Class)cellClass forCellWithReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(nullable UINib *)nib forCellWithReuseIdentifier:(NSString *)identifier;

///注册一个headerView或footerView
- (void)registerClass:(nullable Class)viewClass forSupplementaryViewOfKind:(NSString *)elementKind withReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(nullable UINib *)nib forSupplementaryViewOfKind:(NSString *)kind withReuseIdentifier:(NSString *)identifier;

///获取指定IndexPath的Cell和Supplementary Views
- (__kindof UICollectionViewCell *)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind withReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;

其他还有很多属性和tableview相似就不多说了

UICollectionView的布局

UICollectionView的布局可以分为三种方式:

1.初始化时传入的UICollectionViewLayout对象,通过设置UICollectionViewLayout对象属性的值可以设置item的基本布局,包括大小,间距等。
2.也可以实现UICollectionViewLayoutDelegate协议对应的方法,返回布局需要的值。
3.自定义一个UICollectionViewLayout对象重写对应方法返回自定义的布局。

注意:同时设置1和2,2的优先级更高。

UICollectionViewFlowLayout常用属性

//item的最小行间距
@property (nonatomic) CGFloat minimumLineSpacing;
//item之间的最小间距,这个数据设置的是最小的间距,当间距小于这个值时,item就会换行显示,但是如果你设置的是10,实际间距是20也是不会换行的只有小于这个值时才会换行。
@property (nonatomic) CGFloat minimumInteritemSpacing;
///item的大小
@property (nonatomic) CGSize itemSize;
//此属性8.0以后有效,作用:类似一个占位符,当加载item时会先加载这个size,显示的时候 根据 子控件的autolayout 的约束算出自适应内容的 size;
  /**
         *1.设置estimatedItemSize的值,随便给也行
         *2.对子控件进行约束
         *3.如需进一步操作size可以在cell中重写preferredLayoutAttributesFittingAttributes方法
  */
 
@property (nonatomic) CGSize estimatedItemSize NS_AVAILABLE_IOS(8_0); // defaults to CGSizeZero - setting a non-zero size enables cells that self-size via -preferredLayoutAttributesFittingAttributes:
//设置滑动方向
@property (nonatomic) UICollectionViewScrollDirection scrollDirection; // default is UICollectionViewScrollDirectionVertical
//区头的size,设置宽度值无效
@property (nonatomic) CGSize headerReferenceSize;
//区尾的size,设置宽度值无效
@property (nonatomic) CGSize footerReferenceSize;
//section之间的上左下右的间距(假设有三个区竖向滑动,则top代表每个区第一行的item距离这个区上边的距离,boom代表这个区的最后一行item距这个区下边的距离)不是item之间的上下左右的间距。
@property (nonatomic) UIEdgeInsets sectionInset;

estimatedItemSize属性的简单使用

只需设置estimatedItemSize替代itemSize即可实现简单的布局,如果自适应的label的长度会超出collectionview的宽度那需要label设置最大的宽度,否则会出现布局错误。

@implementation EstimatedItemCollectionViewCell

-(instancetype)initWithFrame:(CGRect)frame{
    if (self =[super initWithFrame:frame]) {
        
   ///设置contentView上下左右约束为0
        [self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.top.bottom.equalTo(self).with.offset(0);
            make.width.mas_equalTo([UIScreen mainScreen].bounds.size.width-20);
        }];
        
        UIImageView *topImageView = [[UIImageView alloc] init];
        topImageView.backgroundColor = [UIColor blueColor];
        [self.contentView addSubview:topImageView];
        
        [topImageView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.top.right.bottom.equalTo(self.contentView).with.offset(0);
//            make.right.equalTo(self.contentView.mas_right).with.offset(-250);
//            make.height.mas_equalTo(200);
        }];
        
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
        label.backgroundColor = [UIColor orangeColor];
        label.textAlignment = NSTextAlignmentCenter;
        label.numberOfLines = 0;
//        [label sizeToFit];
        [self.contentView addSubview:label];
        self.label = label;
        
        //2.设置好subviews的约束
        [label mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.right.bottom.equalTo(self.contentView).with.offset(0);
//            make.top.equalTo(topImageView.mas_bottom).with.offset(0);
            make.width.mas_lessThanOrEqualTo([UIScreen mainScreen].bounds.size.width-20);
//            make.width.mas_greaterThanOrEqualTo(@(100));

        }];
    }
    return self;
}


///3.如果不需要更多关于UICollectionViewLayoutAttributes的操作,只是向下面这样重新赋值给size建议不要重写
//preferredLayoutAttributesFittingAttributes: 方法默认调整Size属性来适应 self-sizing Cell,所以重写的时候需要先调用父类方法,再在返回的 UICollectionViewLayoutAttributes 对象上做你想要做的修改。
- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    ///此处调用父类所等到的attributes 和你最后得到的layoutAttributes的size完全一样,如果只是修改size完全没必要重写,如果layout没有设置layout.estimatedItemSize,则attributes和layoutAttributes的size一样
    UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
    NSLog(@"%f====%f",layoutAttributes.size.width,layoutAttributes.size.height);
    NSLog(@"%f********%f",attributes.size.width,attributes.size.height);
   ///父类已经调用
    CGSize size = [self.contentView systemLayoutSizeFittingSize:layoutAttributes.size];
    CGRect newFrame = layoutAttributes.frame;
    newFrame.size.width = size.width;
    layoutAttributes.frame = newFrame;
    NSLog(@"%f------%f",size.width,size.height);
    return attributes;
}

自定义UICollectionViewFlowLayout

先看一张图,下面这张图是使用estimatedItemSize来布局的一个collcetionview,现在我不想让第一行两个item之间的空隙太大,这时候使用系统的layout向缩小是不可行的,因为就算我们设置minimumInteritemSpacing为10,上面已经说了这个属性只是item之间的最小间隔,item之间的实际间隔是可以大于10的,这时候如果用系统的layout就不是很好实现,如果我们通过自定义layout则是很好实现的。

屏幕快照 2017-07-09 下午5.06.05.png

这张图就可以实现我们上面的需求了,这就是通过自定义layout来实现的布局

屏幕快照 2017-07-09 下午5.09.28.png

自定义layout

自定义一个layout实际上很简单,你只需要创建一个继承与UICollectionViewLayout的子类即可,然后重写一部分方法,在这些方法里去实现你想要的布局方式。

常用的方法

每次layout更新期间collectionview都会首先调用这个方法,为将要开始的更新做准备,可以在此准备要使用的,你想要的layout的布局数组。

///每次更新layout布局都会首先调用此方法
-(void)prepareLayout{
    NSLog(@"---------1");
    ///和init相似,必须call super的prepareLayout以保证初始化正确
    [super prepareLayout];
    ///1.首先被调用
    
    [self.attributesArray removeAllObjects];
    
    NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
    
    for (int i =0; i<itemCount; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
        UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
        [self.attributesArray addObject:attributes];
        if (i==self.widthArray.count-1) {
            [self loadOldAttributes:attributes.frame];
        }
    }
}

collectionViewContentSize方法返回collectionview的ContentSize,每次更新会调用两次,第一次是开始更新时调用,第二次是layoutAttributesForElementsInRect方法返回所有item的约束后调用。

///返回collectionView的内容的尺寸
-(CGSize)collectionViewContentSize{
    ///2.其次被调用(layoutAttributesForElementsInRect 调用后会在此调用此方法)
    NSLog(@"---%f------2",self.maxY);
    return CGSizeMake(self.collectionView.bounds.size.width, self.maxY);
}

返回rect中的所有的元素的布局属性,返回的是包含UICollectionViewLayoutAttributes的NSArray,UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的,初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    ///3.被调用
    NSLog(@"---------3");
    return self.attributesArray;
}

返回对应于indexPath的位置的cell的布局属性,返回指定indexPath的item的布局信息。子类必须重载该方法,该方法只能为cell提供布局信息,不能为补充视图和装饰视图提供。

///返回对应于indexPath的位置的cell的布局属性,返回指定indexPath的item的布局信息。子类必须重载该方法,该方法只能为cell提供布局信息,不能为补充视图和装饰视图提供。
-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath{
    UICollectionViewLayoutAttributes *attributs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    
    NSNumber *currentWidthNumber = self.widthArray[indexPath.row];
    CGFloat width = currentWidthNumber.floatValue;
    
    ///没有换行所以超出部分不显示(不写下面的代码也不会报错,不知道为啥)
    if (width>[UIScreen mainScreen].bounds.size.width-(self.left+self.right)) {
        width = [UIScreen mainScreen].bounds.size.width - (self.left+self.right);
    }
    
    CGFloat height = 30;
    CGRect currentFrame = CGRectZero;

    if (1) {
        if (self.attributesArray.count!=0) {
            ///1.取出上一个item的attributes
            UICollectionViewLayoutAttributes *lastAttributs = [self.attributesArray lastObject];
            CGRect lastFrame = lastAttributs.frame;
            
            ///判断当前item和上一个item是否在同一个row
            if (CGRectGetMaxX(lastAttributs.frame)+self.right==self.collectionView.bounds.size.width) {
                ///不在同一row
                currentFrame.origin.x = self.left;
                currentFrame.origin.y = CGRectGetMaxY(lastFrame) +self.top;
                currentFrame.size.width = width;
                currentFrame.size.height = height;
                attributs.frame = currentFrame;
                
            }else{
                ///上一个item的最大x值+当前item的宽度和左边距
                CGFloat totleWidth = CGRectGetMaxX(lastFrame)+(self.between+width+self.right);
                ///判断上一个item所在row的剩余宽度是否还够显示当前item
                if (totleWidth>=self.collectionView.bounds.size.width) {
                    ///不足以显示当前item的宽度
                    
                    ///将和上一个item在同一个row的item的放在同一个数组
                    NSMutableArray *sameYArray = [NSMutableArray array];
                    for (UICollectionViewLayoutAttributes *subAttributs in self.attributesArray) {
                        if (subAttributs.frame.origin.y==lastFrame.origin.y) {
                            [sameYArray addObject:subAttributs];
                        }
                    }
                    
                    ///判断出上一row还剩下多少宽度
                    CGFloat sameYWidth = 0.0;
                    for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
                        sameYWidth += sameYAttributs.size.width;
                    }
                    sameYWidth = sameYWidth + (self.left+self.right+(sameYArray.count-1)*self.between);
                    ///上一个row所剩下的宽度
                    CGFloat sameYBetween = (self.collectionView.bounds.size.width-sameYWidth)/sameYArray.count;
                    
                    for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
                        CGFloat sameAttributeWidth = sameYAttributs.size.width;
                        CGFloat sameAttributeHeight = sameYAttributs.size.height;
                        
                        CGRect sameYAttributsFrame = sameYAttributs.frame;
                        ///更新sameYAttributs宽度使之均衡显示
                        sameAttributeWidth += sameYBetween;
                        sameYAttributs.size = CGSizeMake(sameAttributeWidth, sameAttributeHeight);
                        NSInteger index = [sameYArray indexOfObject:sameYAttributs];
                        
                        sameYAttributsFrame.origin.x += (sameYBetween*index);
                        sameYAttributsFrame.size.width = sameAttributeWidth;
                        sameYAttributs.frame = sameYAttributsFrame;
                    }
                    currentFrame.origin.x = self.left;
                    currentFrame.origin.y = CGRectGetMaxY(lastFrame)+self.top;
                    currentFrame.size.width = width;
                    currentFrame.size.height = height;
                    attributs.frame = currentFrame;
                    
                }else{
                    currentFrame.origin.x = CGRectGetMaxX(lastFrame)+self.between;
                    currentFrame.origin.y = lastFrame.origin.y;
                    currentFrame.size.width = width;
                    currentFrame.size.height = height;
                    attributs.frame = currentFrame;
                }
            }
        }else{
            currentFrame.origin.x = self.left;
            currentFrame.origin.y = self.top;
            currentFrame.size.width = width;
            currentFrame.size.height = height;
            attributs.frame = currentFrame;
        }
    }
    
//    attributs.size = CGSizeMake(width, 30);
    self.maxY = CGRectGetMaxY(attributs.frame)+10;
    
    NSLog(@"%f===%f===%f===%f",attributs.frame.origin.x,attributs.frame.origin.y,attributs.frame.size.width,attributs.frame.size.height);
    
    return attributs;
}

///返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
-(UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
   return [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
}

///返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
-(UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)decorationViewKind atIndexPath:(NSIndexPath*)indexPath{
    return [super layoutAttributesForDecorationViewOfKind:decorationViewKind atIndexPath:indexPath];
}

///当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    return [super shouldInvalidateLayoutForBoundsChange:newBounds];
}

注意点: 另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIView的setNeedsLayout方法十分类似。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect来生成更新后的布局。

全部代码

#import "DWCollectionViewLayout.h"


#define DWScreenH = [UIScreen mainScreen].bounds.size.height
#define DWScreenW = [UIScreen mainScreen].bounds.size.width


@interface DWCollectionViewLayout ()

@property (nonatomic,strong) NSMutableArray *attributesArray;

@property (nonatomic,assign) CGFloat maxY;

@property (nonatomic,assign) CGFloat left;

@property (nonatomic,assign) CGFloat right;

@property (nonatomic,assign) CGFloat top;

@property (nonatomic,assign) CGFloat between;

@end




@implementation DWCollectionViewLayout


-(instancetype)initWithArray:(NSMutableArray*)widthArray edgeInsets:(UIEdgeInsets)insets{
    if (self = [super init]) {
        self.widthArray = widthArray;
        NSLog(@"==***==%p",self.widthArray);

        self.left = insets.left;
        self.right = insets.right;
        self.top = insets.top;
        self.between = insets.bottom;
    }
    return self;
}


/**
 *另外需要了解的是,在初始化一个UICollectionViewLayout实例后,会有一系列准备方法被自动调用,以保证layout实例的正确。
 
 *首先,将被调用,默认下该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。
 
 */

-(void)prepareLayout{
    
    NSLog(@"---------1");
    
    ///和init相似,必须call super的prepareLayout以保证初始化正确
    [super prepareLayout];
    ///1.首先被调用
    
    [self.attributesArray removeAllObjects];
    
    NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
    
    for (int i =0; i<itemCount; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
        UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
        [self.attributesArray addObject:attributes];
        if (i==self.widthArray.count-1) {
            [self loadOldAttributes:attributes.frame];
        }
    }
}


///返回collectionView的内容的尺寸
-(CGSize)collectionViewContentSize{
    ///2.其次被调用(layoutAttributesForElementsInRect 调用后会在此调用此方法)
    NSLog(@"---%f------2",self.maxY);
    return CGSizeMake(self.collectionView.bounds.size.width, self.maxY);
}

///返回rect中的所有的元素的布局属性,返回的是包含UICollectionViewLayoutAttributes的NSArray
///UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的
///初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。

///rect 为collectionview 的rect,(高度超出当前屏幕的高度后,rect的height会翻倍)
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    ///3.被调用
    NSLog(@"---------3");

    NSArray *array = [super layoutAttributesForElementsInRect:rect];
    
    for (UICollectionViewLayoutAttributes *attributes in array) {
        if (attributes.representedElementCategory == UICollectionElementCategoryCell) {
        }
    }
    return self.attributesArray;
}


///返回对应于indexPath的位置的cell的布局属性,返回指定indexPath的item的布局信息。子类必须重载该方法,该方法只能为cell提供布局信息,不能为补充视图和装饰视图提供。
-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath{
    UICollectionViewLayoutAttributes *attributs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    
    NSNumber *currentWidthNumber = self.widthArray[indexPath.row];
    CGFloat width = currentWidthNumber.floatValue;
    
    ///没有换行所以超出部分不显示(不写下面的代码也不会报错,不知道为啥)
    if (width>[UIScreen mainScreen].bounds.size.width-(self.left+self.right)) {
        width = [UIScreen mainScreen].bounds.size.width - (self.left+self.right);
    }
    
    CGFloat height = 30;
    CGRect currentFrame = CGRectZero;

    if (1) {
        if (self.attributesArray.count!=0) {
            ///1.取出上一个item的attributes
            UICollectionViewLayoutAttributes *lastAttributs = [self.attributesArray lastObject];
            CGRect lastFrame = lastAttributs.frame;
            
            ///判断当前item和上一个item是否在同一个row
            if (CGRectGetMaxX(lastAttributs.frame)+self.right==self.collectionView.bounds.size.width) {
                ///不在同一row
                currentFrame.origin.x = self.left;
                currentFrame.origin.y = CGRectGetMaxY(lastFrame) +self.top;
                currentFrame.size.width = width;
                currentFrame.size.height = height;
                attributs.frame = currentFrame;
                
            }else{
                ///上一个item的最大x值+当前item的宽度和左边距
                CGFloat totleWidth = CGRectGetMaxX(lastFrame)+(self.between+width+self.right);
                ///判断上一个item所在row的剩余宽度是否还够显示当前item
                if (totleWidth>=self.collectionView.bounds.size.width) {
                    ///不足以显示当前item的宽度
                    
                    ///将和上一个item在同一个row的item的放在同一个数组
                    NSMutableArray *sameYArray = [NSMutableArray array];
                    for (UICollectionViewLayoutAttributes *subAttributs in self.attributesArray) {
                        if (subAttributs.frame.origin.y==lastFrame.origin.y) {
                            [sameYArray addObject:subAttributs];
                        }
                    }
                    
                    ///判断出上一row还剩下多少宽度
                    CGFloat sameYWidth = 0.0;
                    for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
                        sameYWidth += sameYAttributs.size.width;
                    }
                    sameYWidth = sameYWidth + (self.left+self.right+(sameYArray.count-1)*self.between);
                    ///上一个row所剩下的宽度
                    CGFloat sameYBetween = (self.collectionView.bounds.size.width-sameYWidth)/sameYArray.count;
                    
                    for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
                        CGFloat sameAttributeWidth = sameYAttributs.size.width;
                        CGFloat sameAttributeHeight = sameYAttributs.size.height;
                        
                        CGRect sameYAttributsFrame = sameYAttributs.frame;
                        ///更新sameYAttributs宽度使之均衡显示
                        sameAttributeWidth += sameYBetween;
                        sameYAttributs.size = CGSizeMake(sameAttributeWidth, sameAttributeHeight);
                        NSInteger index = [sameYArray indexOfObject:sameYAttributs];
                        
                        sameYAttributsFrame.origin.x += (sameYBetween*index);
                        sameYAttributsFrame.size.width = sameAttributeWidth;
                        sameYAttributs.frame = sameYAttributsFrame;
                    }
                    currentFrame.origin.x = self.left;
                    currentFrame.origin.y = CGRectGetMaxY(lastFrame)+self.top;
                    currentFrame.size.width = width;
                    currentFrame.size.height = height;
                    attributs.frame = currentFrame;
                    
                }else{
                    currentFrame.origin.x = CGRectGetMaxX(lastFrame)+self.between;
                    currentFrame.origin.y = lastFrame.origin.y;
                    currentFrame.size.width = width;
                    currentFrame.size.height = height;
                    attributs.frame = currentFrame;
                }
            }
        }else{
            currentFrame.origin.x = self.left;
            currentFrame.origin.y = self.top;
            currentFrame.size.width = width;
            currentFrame.size.height = height;
            attributs.frame = currentFrame;
        }
    }
    
//    attributs.size = CGSizeMake(width, 30);
    self.maxY = CGRectGetMaxY(attributs.frame)+10;
    
    NSLog(@"%f===%f===%f===%f",attributs.frame.origin.x,attributs.frame.origin.y,attributs.frame.size.width,attributs.frame.size.height);
    
    return attributs;
}

///返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
-(UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
   return [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
}

///返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
-(UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)decorationViewKind atIndexPath:(NSIndexPath*)indexPath{
    return [super layoutAttributesForDecorationViewOfKind:decorationViewKind atIndexPath:indexPath];
}

///当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    return [super shouldInvalidateLayoutForBoundsChange:newBounds];
}


/**
  另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIView的setNeedsLayout方法十分类似。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect来生成更新后的布局。
 */



-(void)loadOldAttributes:(CGRect)lastFrame{
    ///将和上一个item在同一个row的item的放在同一个数组
    NSMutableArray *sameYArray = [NSMutableArray array];
    for (UICollectionViewLayoutAttributes *subAttributs in self.attributesArray) {
        if (subAttributs.frame.origin.y==lastFrame.origin.y) {
            [sameYArray addObject:subAttributs];
        }
    }
    
    ///判断出上一row还剩下多少宽度
    CGFloat sameYWidth = 0.0;
    for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
        sameYWidth += sameYAttributs.size.width;
    }
    sameYWidth = sameYWidth + (self.left+self.right+(sameYArray.count-1)*self.between);
    ///上一个row所剩下的宽度
    CGFloat sameYBetween = (self.collectionView.bounds.size.width-sameYWidth)/sameYArray.count;
    
    for (UICollectionViewLayoutAttributes *sameYAttributs in sameYArray) {
        CGFloat sameAttributeWidth = sameYAttributs.size.width;
        CGFloat sameAttributeHeight = sameYAttributs.size.height;
        
        CGRect sameYAttributsFrame = sameYAttributs.frame;
        ///更新sameYAttributs宽度使之均衡显示
        sameAttributeWidth += sameYBetween;
        sameYAttributs.size = CGSizeMake(sameAttributeWidth, sameAttributeHeight);
        NSInteger index = [sameYArray indexOfObject:sameYAttributs];
        
        sameYAttributsFrame.origin.x += (sameYBetween*index);
        sameYAttributsFrame.size.width = sameAttributeWidth;
        sameYAttributs.frame = sameYAttributsFrame;
    }
}

-(NSMutableArray*)attributesArray{
    if (!_attributesArray) {
        _attributesArray = [NSMutableArray array];
    }
    return _attributesArray;
}

@end

区头悬浮

#import "DWReusableLayout.h"

@interface DWReusableLayout ()

@property (nonatomic,assign) CGFloat naviHeight;

@end

@implementation DWReusableLayout


-(instancetype)init
{
    self = [super init];
    if (self)
    {
        self.naviHeight = 0.0;
    }
    return self;
}
/*
 
 // 作用:返回指定区域的cell布局对象
 // 什么时候调用:指定新的区域的时候调用
 (<__kindof UICollectionViewLayoutAttributes *>   iOS9之后的泛型 )
 - (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
 */

/** iOS9 之前的写法 作用第24行代码有写*/
//UICollectionViewLayoutAttributes:我称它为collectionView中的item(包括cell和header、footer这些)的《结构信息》
- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect
{
    
    //截取到父类所返回的数组(里面放的是当前屏幕所能展示的item的结构信息),并转化成不可变数组
    NSMutableArray *superArray = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
    
    //创建存索引的数组,无符号(正整数),无序(不能通过下标取值),不可重复(重复的话会自动过滤)
    NSMutableIndexSet *noneHeaderSections = [NSMutableIndexSet indexSet];
    //遍历superArray,得到一个当前屏幕中所有的section数组
    for (UICollectionViewLayoutAttributes *attributes in superArray)
    {
        //如果当前的元素分类是一个cell,将cell所在的分区section加入数组,重复的话会自动过滤
        if (attributes.representedElementCategory == UICollectionElementCategoryCell)
        {
            [noneHeaderSections addIndex:attributes.indexPath.section];
        }
    }
    
    //遍历superArray,将当前屏幕中拥有的header的section从数组中移除,得到一个当前屏幕中没有header的section数组
    //正常情况下,随着手指往上移,header脱离屏幕会被系统回收而cell尚在,也会触发该方法
    for (UICollectionViewLayoutAttributes *attributes in superArray)
    {
        //如果当前的元素是一个header,将header所在的section从数组中移除
        if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader])
        {
            [noneHeaderSections removeIndex:attributes.indexPath.section];
        }
    }
    
    //遍历当前屏幕中没有header的section数组
    [noneHeaderSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){
        
        //取到当前section中第一个item的indexPath
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
        //获取当前section在正常情况下已经离开屏幕的header结构信息
        UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
        
        //如果当前分区确实有因为离开屏幕而被系统回收的header
        if (attributes)
        {
            //将该header结构信息重新加入到superArray中去
            [superArray addObject:attributes];
        }
    }];
    
    //遍历superArray,改变header结构信息中的参数,使它可以在当前section还没完全离开屏幕的时候一直显示
    for (UICollectionViewLayoutAttributes *attributes in superArray) {
        
        //如果当前item是header
        if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader])
        {
            //得到当前header所在分区的cell的数量
            NSInteger numberOfItemsInSection = [self.collectionView numberOfItemsInSection:attributes.indexPath.section];
            //得到第一个item的indexPath
            NSIndexPath *firstItemIndexPath = [NSIndexPath indexPathForItem:0 inSection:attributes.indexPath.section];
            //得到最后一个item的indexPath
            NSIndexPath *lastItemIndexPath = [NSIndexPath indexPathForItem:MAX(0, numberOfItemsInSection-1) inSection:attributes.indexPath.section];
            //得到第一个item和最后一个item的结构信息
            UICollectionViewLayoutAttributes *firstItemAttributes, *lastItemAttributes;
            if (numberOfItemsInSection>0)
            {
                //cell有值,则获取第一个cell和最后一个cell的结构信息
                firstItemAttributes = [self layoutAttributesForItemAtIndexPath:firstItemIndexPath];
                lastItemAttributes = [self layoutAttributesForItemAtIndexPath:lastItemIndexPath];
            }else
            {
                //cell没值,就新建一个UICollectionViewLayoutAttributes
                firstItemAttributes = [UICollectionViewLayoutAttributes new];
                //然后模拟出在当前分区中的唯一一个cell,cell在header的下面,高度为0,还与header隔着可能存在的sectionInset的top
                CGFloat y = CGRectGetMaxY(attributes.frame)+self.sectionInset.top;
                firstItemAttributes.frame = CGRectMake(0, y, 0, 0);
                //因为只有一个cell,所以最后一个cell等于第一个cell
                lastItemAttributes = firstItemAttributes;
            }
            
            //获取当前header的frame
            CGRect rect = attributes.frame;
            
            //当前的滑动距离 + 因为导航栏产生的偏移量,默认为64(如果app需求不同,需自己设置)
            CGFloat offset = self.collectionView.contentOffset.y + _naviHeight;
            //第一个cell的y值 - 当前header的高度 - 可能存在的sectionInset的top
            
            ///firstItemAttributes.frame.origin.y-self.sectionInset.top = CGRectGetMaxY(attributes)(即紧贴header的最大Y值 = header.frame.origin.y+header.bounds.size.height)
            ///firstItemAttributes.frame.origin.y-self.sectionInset.top = header.frame.origin.y+header.bounds.size.height
            
            ///header.frame.origin.y = firstItemAttributes.frame.origin.y-self.sectionInset.top-header.bounds.size.height
            CGFloat headerY = firstItemAttributes.frame.origin.y - rect.size.height - self.sectionInset.top;
            
            //哪个大取哪个,保证header悬停
            //针对当前header基本上都是offset更加大,针对下一个header则会是headerY大,各自处理
            CGFloat maxY = MAX(offset,headerY);
            
            //最后一个cell的y值 + 最后一个cell的高度 + 可能存在的sectionInset的bottom - 当前header的高度
            //当当前section的footer或者下一个section的header接触到当前header的底部,计算出的headerMissingY即为有效值
            CGFloat headerMissingY = CGRectGetMaxY(lastItemAttributes.frame) + self.sectionInset.bottom - rect.size.height;
            
            //给rect的y赋新值,因为在最后消失的临界点要跟谁消失,所以取小
            ///两个区头没有接触之前,offset<headerMissingY,所以rect.origin.y==偏移量,接触时offset=headerMissingY,接触后,offset>headerMissingY
            ///所以接触后最小值就是headerMissingY(即上一个区的最大Y值-rect.size.height)
            rect.origin.y = MIN(maxY,headerMissingY);
            
            NSLog(@"%f-----%f----%f---%f",offset,headerY,headerMissingY,rect.origin.y);
            
            //给header的结构信息的frame重新赋值
            attributes.frame = rect;
            
            //如果按照正常情况下,header离开屏幕被系统回收,而header的层次关系又与cell相等,如果不去理会,会出现cell在header上面的情况
            //通过打印可以知道cell的层次关系zIndex数值为0,我们可以将header的zIndex设置成1,如果不放心,也可以将它设置成非常大,这里随便填了个7
            attributes.zIndex = 7;
        }
    }
    //转换回不可变数组,并返回
    return [superArray copy];
    
}

//return YES;表示一旦滑动就实时调用上面这个layoutAttributesForElementsInRect:方法
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound
{
    return YES;
}

@end

屏幕快照 2017-07-09 下午5.40.17.png

瀑布流

#import "DWWateFallLayout.h"

@interface DWWateFallLayout ()

@property (nonatomic,strong) NSMutableArray *attributesArray;

@property (nonatomic,strong) NSArray<NSNumber*> *itemHeightArray;

@property (nonatomic,strong) NSMutableArray<UICollectionViewLayoutAttributes*> *itemArray;

@end

@implementation DWWateFallLayout

-(instancetype)initWithHeightArray:(NSArray*)heightArray{
    if (self = [super init]) {
        self.itemHeightArray = heightArray;
    }
    return self;
}


-(void)prepareLayout{
        
    ///和init相似,必须call super的prepareLayout以保证初始化正确
    [super prepareLayout];
    ///1.首先被调用
    
    [self.attributesArray removeAllObjects];
    [self.itemArray removeAllObjects];
        
    ///获取当前collectionView对应区的item
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    
    for (int i =0; i<count; i++) {
       UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
        [self.attributesArray addObject:attributes];
    }
    
}

///返回collectionView的内容的尺寸
-(CGSize)collectionViewContentSize{
    ///2.其次被调用(layoutAttributesForElementsInRect 调用后会在此调用此方法)
    
    
    CGFloat maxContentHeight = CGRectGetMaxY([self.itemArray firstObject].frame);
    
    for (UICollectionViewLayoutAttributes *attributes in self.itemArray) {
        if (maxContentHeight<CGRectGetMaxY(attributes.frame)) {
            maxContentHeight = CGRectGetMaxY(attributes.frame);
        }
    }
    return CGSizeMake(self.collectionView.bounds.size.width, maxContentHeight);
}


///返回rect中的所有的元素的布局属性,返回的是包含UICollectionViewLayoutAttributes的NSArray
///UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的
///初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。

///rect 为collectionview 的rect,(高度超出collectionview高度后,rect的height会翻倍)
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    ///3.被调用
    return self.attributesArray;
}

///返回对应于indexPath的位置的cell的布局属性,返回指定indexPath的item的布局信息。子类必须重载该方法,该方法只能为cell提供布局信息,不能为补充视图和装饰视图提供。
-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath{
    UICollectionViewLayoutAttributes *attributs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    
    ///item的宽度,根据左右间距和中间间距算出item宽度
    CGFloat itemWidth = (self.collectionView.bounds.size.width - (10+10+10+10))/3.0;
    ///item的高度
    CGFloat itemHeight = self.itemHeightArray[indexPath.row].floatValue;
    
    if (self.itemArray.count<3) {
        [self.itemArray addObject:attributs];
        CGRect itemFrame = CGRectMake(10+(itemWidth+10)*(self.itemArray.count-1), 10, itemWidth, itemHeight);
        attributs.frame = itemFrame;
    }else{
        UICollectionViewLayoutAttributes *fristAttri = [self.itemArray firstObject];
        CGFloat minY = CGRectGetMaxY(fristAttri.frame);
        CGFloat Y = minY;
        NSInteger index=0;
        CGRect itemFrame = CGRectMake(fristAttri.frame.origin.x,CGRectGetMaxY(fristAttri.frame)+10, itemWidth, itemHeight);
        for (UICollectionViewLayoutAttributes *attri in self.itemArray) {
            if (minY>CGRectGetMaxY(attri.frame)) {
                minY = CGRectGetMaxY(attri.frame);
                Y = minY;
                itemFrame = CGRectMake(attri.frame.origin.x,Y+10, itemWidth, itemHeight);
                NSInteger currentIndex = [self.itemArray indexOfObject:attri];
                index = currentIndex;
            }
        }
        attributs.frame = itemFrame;
        [self.itemArray replaceObjectAtIndex:index withObject:attributs];
    }
    
    return attributs;
}

-(NSMutableArray*)attributesArray{
    if (!_attributesArray) {
        _attributesArray = [NSMutableArray array];
    }
    return _attributesArray;
}

-(NSMutableArray*)itemArray{
    if (!_itemArray) {
        _itemArray = [NSMutableArray array];
    }
    return _itemArray;
}

@end

屏幕快照 2017-07-09 下午5.40.02.png

这些只是本人在使用过程中,了解到的一些知识点仅供参考,如有错误欢迎指正。其他的就不多说了,具体的实现过程代码里都有详细的注释,还有一些注意点懒得写了都在代码里面有兴趣的可以看看。

demo地址:https://github.com/dachongdouniwan/DWCollectionView

写的不好大家随便看看吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容