IOS-自定义CollectionView的layout以及添加头部

  • 先看例子,再看知识点


    成型的collectionView
  • 在viewDidLoad里
- (void)viewDidLoad {
    [super viewDidLoad];
    //这里从本地获取数据存在数组self.Array里
    NSArray * shopsArray = [shopModel mj_objectArrayWithFilename:@"1.plist"];
    [self.Array addObjectsFromArray:shopsArray];
    
    //这是自定义瀑布流
    FlowLayout *flowlayout1 = [[FlowLayout alloc] init];
    //这个代理为了能够根据数组内图片的宽高等比缩放
    flowlayout1.delegate = self;
    self.mainView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) collectionViewLayout:flowlayout1];
    self.mainView.backgroundColor = [UIColor redColor];
    self.mainView.delegate = self;
    self.mainView.dataSource = self;
    self.mainView.scrollEnabled = YES;
    
    [self.view addSubview:self.mainView];
    //注册一个Cell
    [self.mainView registerClass:[CollectionCell class] forCellWithReuseIdentifier:@"CollectionCell"];
    //注册一个ReusableView,类型是UICollectionElementKindSectionHeader的(头部)
    [self.mainView registerClass:[headReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"headReusableView"];
    //注册一个ReusableView,类型是UICollectionElementKindSectionFooter的(尾部)
//    [self.mainView registerClass:[footReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footReusableView"];
    
}
  • collectionView的代理
#pragma mark  设置CollectionView的组数
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
    return 1;
}
#pragma mark  设置CollectionView每组所包含的个数
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return self.Array.count;
}
#pragma mark  设置CollectionView所展示出来的cell
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    CollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CollectionCell" forIndexPath:indexPath];
    cell.model = self.Array[indexPath.item];
    return cell;
}
#pragma mark  定义每个UICollectionView的大小
//- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
//    return CGSizeMake(100, 100);
//}
#pragma mark  定义整个CollectionViewCell与整个View的间距
//- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section{
//    return UIEdgeInsetsMake(10, 10, 10, 10);
//}
#pragma mark  设置CollectionViewCell是否可以被点击
//- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath{
//    return YES;
//}
#pragma mark  点击CollectionView触发事件
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"---------------------");
}
#pragma mark headView大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section{
    return CGSizeMake(self.view.frame.size.width, 100);
}
//#pragma mark footView大小
//- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section{
//    return CGSizeMake(self.view.frame.size.width, 80);
//}
#pragma mark headView和footView都在这里判断
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
    headReusableView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"headReusableView" forIndexPath:indexPath];
//    footReusableView *footerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footReusableView" forIndexPath:indexPath];
//    [footerView.btn1 addTarget:self action:@selector(btn1Click) forControlEvents:UIControlEventTouchUpInside];
    [headerView.Btn1 addTarget:self action:@selector(Btn1Click) forControlEvents:UIControlEventTouchUpInside];
//    if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
        return headerView;
//    }else{
//        return footerView;
//    }
}
  • 自己定义的代理(为了计算等比宽高)
#pragma mark wxzFlowLayoutDelegate
- (CGFloat)WXZWaterFlow:(UICollectionViewFlowLayout *)waterFlow heightForWidth:(CGFloat)width atIndexPath:(NSIndexPath *)indexPath{
    shopModel *model = self.Array[indexPath.item];
    return model.h * width / model.w ;
}
  • 在UICollectionViewFlowLayout.h中
#import <UIKit/UIKit.h>
@class UICollectionViewFlowLayout;

@protocol WXZWaterFlowDelegte <NSObject>
- (CGFloat)WXZWaterFlow:(UICollectionViewFlowLayout *)waterFlow heightForWidth:(CGFloat)width atIndexPath:(NSIndexPath *)indexPath;
@end

@interface FlowLayout : UICollectionViewFlowLayout

@property(weak, nonatomic)id<WXZWaterFlowDelegte>delegate;

@end
  • 在UICollectionViewFlowLayout.m中
#import "FlowLayout.h"

#define WXZCollectionW self.collectionView.frame.size.width

/** 每一行之间的间距 */
static const CGFloat  WXZDefaultRowMargin = 5;
/** 每一列之间的间距 */
static const CGFloat  WXZDefaultColumnMargin = 10;
/** 每一列之间的间距 top, left, bottom, right */
static const UIEdgeInsets  WXZDefaultInsets = {5, 15, 5, 5};
/** 默认的列数 */
static const int WXZDefaultColumsCount = 2;

@interface FlowLayout()
/** 每一列的最大Y值 */
@property (nonatomic, strong) NSMutableArray *columnMaxYs;
/** 存放所有cell的布局属性 */
@property (nonatomic, strong) NSMutableArray *attrsArray;

@property (nonatomic, strong) NSArray *heightArray;

@end

@implementation FlowLayout
#pragma mark -lazy
- (NSMutableArray *)columnMaxYs
{
    if (!_columnMaxYs) {
        _columnMaxYs = [[NSMutableArray alloc] init];
    }
    return _columnMaxYs;
}

- (NSMutableArray *)attrsArray
{
    if (!_attrsArray) {
        _attrsArray = [[NSMutableArray alloc] init];
    }
    return _attrsArray;
}

- (NSArray *)heightArray
{
    if (!_heightArray) {
        _heightArray = [[NSArray alloc] init];
        _heightArray = @[@85,@105,@115,@105];
    }
    return _heightArray;
}

#pragma mark - 实现内部的方法
/**
 * 决定了collectionView的contentSize。由于collectionView将item的布局任务委托给layout对象,那么滚动区域的大小对于它而言是不可知的。自定义的布局对象必须在这个方法里面计算出显示内容的大小,包括supplementaryView和decorationView在内。
 */
- (CGSize)collectionViewContentSize
{
    // 找出最长那一列的最大Y值
    CGFloat destMaxY = [self.columnMaxYs[0] doubleValue];
    for (NSUInteger i = 1; i<self.columnMaxYs.count; i++) {
        // 取出第i列的最大Y值
        CGFloat columnMaxY = [self.columnMaxYs[i] doubleValue];
        
        // 找出数组中的最大值
        if (destMaxY < columnMaxY) {
            destMaxY = columnMaxY;
        }
    }
    return CGSizeMake(0, destMaxY + WXZDefaultInsets.bottom);
}
/**
 * 系统在准备对item进行布局前会调用这个方法,我们重写这个方法之后可以在方法里面预先设置好需要用到的变量属性等。比如在瀑布流开始布局前,我们可以对存储瀑布流高度的数组进行初始化。有时我们还需要将布局属性对象进行存储,比如卡片动画式的定制,也可以在这个方法里面进行初始化数组。切记要调用[super prepareLayout];
 */
//
- (void)prepareLayout
{
    [super prepareLayout];
    
    // 重置每一列的最大Y值
    [self.columnMaxYs removeAllObjects];
    for (NSUInteger i = 0; i<WXZDefaultColumsCount; i++) {
        [self.columnMaxYs addObject:@(WXZDefaultInsets.top)];
    }
    
    // 计算所有cell的布局属性
    [self.attrsArray removeAllObjects];
    NSUInteger count = [self.collectionView numberOfItemsInSection:0];
    for (NSUInteger i = 0; i < count; ++i) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
        [self.attrsArray addObject:attrs];
    }
}
/**
 * 说明所有元素(比如cell、补充控件、装饰控件)的布局属性。个人觉得完成定制布局最核心的方法,没有之一。collectionView调用这个方法并将自身坐标系统中的矩形传过来,这个矩形代表着当前collectionView可视的范围。我们需要在这个方法里面返回一个包括UICollectionViewLayoutAttributes对象的数组,这个布局属性对象决定了当前显示的item的大小、层次、可视属性在内的布局属性。同时,这个方法还可以设置supplementaryView和decorationView的布局属性。合理使用这个方法的前提是不要随便返回所有的属性,除非这个view处在当前collectionView的可视范围内,又或者大量额外的计算造成的用户体验下降——你加班的原因。
 */
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    //找到collectionVIew的头部headReusableView并且添加到数组里,这样子就能够显示出头部里
    [self.attrsArray addObjectsFromArray:[super layoutAttributesForElementsInRect:rect]];
    return self.attrsArray;
}

/**
 * 说明cell的布局属性,相当重要的方法。collectionView可能会为了某些特殊的item请求特殊的布局属性,我们可以在这个方法中创建并且返回特别定制的布局属性。根据传入的indexPath调用[UICollectionViewLayoutAttributes layoutAttributesWithIndexPath: ]方法来创建属性对象,然后设置创建好的属性,包括定制形变、位移等动画效果在内
 */
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    
    /** 计算indexPath位置cell的布局属性 */
    
    // 水平方向上的总间距
    CGFloat xMargin = WXZDefaultInsets.left + WXZDefaultInsets.right + (WXZDefaultColumsCount - 1) * WXZDefaultColumnMargin;
    // cell的宽度
    
    CGFloat w = (WXZCollectionW - xMargin - 20) / WXZDefaultColumsCount;
    // cell的高度
    CGFloat h = [self.delegate WXZWaterFlow:self heightForWidth:w atIndexPath:indexPath];
    
    // 找出最短那一列的 列号 和 最大Y值  要判断第一个才+363
    CGFloat destMaxY = [self.columnMaxYs[0] doubleValue];
    NSUInteger destColumn = 0;
    for (NSUInteger i = 1; i<self.columnMaxYs.count; i++) {
        // 取出第i列的最大Y值
        CGFloat columnMaxY = [self.columnMaxYs[i] doubleValue];
        
        // 找出数组中的最小值
        if (destMaxY > columnMaxY) {
            destMaxY = columnMaxY;
            destColumn = i;
        }
    }
    
    // cell的x值
    CGFloat x = WXZDefaultInsets.left + destColumn * (w + WXZDefaultColumnMargin);
    
    CGFloat y = destMaxY + WXZDefaultRowMargin;
    
    // cell的frame
    attrs.frame = CGRectMake(x, y, w, h);
    
    // cell的y值
    if (destMaxY==5) {
        //手动增加第一个cell的高,如果不增加 ,那么cell是从顶部开始 。会与头部相叠
        attrs.frame = CGRectMake(x, 120, w, h);
    }
    
    // 更新数组中的最大Y值
    self.columnMaxYs[destColumn] = @(CGRectGetMaxY(attrs.frame));
    
    return attrs;
}

//- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
当collectionView的bounds改变的时候,我们需要告诉collectionView是否需要重新计算布局属性,通过这个方法返回是否需要重新计算的结果。简单的返回YES会导致我们的布局在每一秒都在进行不断的重绘布局,造成额外的计算任务。
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    //    NSLog(@"%s",__func__);
    return NO;
}
@end
  • 你可以在CollectionView的头部headReusableView里添加任何东西 ,他继承自UIView,如果你的CollectionView的FlowLayout是系统自定义的,那么你就不需要再将自定义的headReusableView添加进FlowLayout的数组里了。系统已经将你想要的做好了。
  • CollectionView非常好用,建议多多的学习。有如下扩展:对每一张图片的介绍文字有多有少。那么我们的高度还是图片高度+字体所对应的大小

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

推荐阅读更多精彩内容