自定义UICollectionView布局

前言

相对于UITableView而言,UICollectionView具有更高的定制性和灵活性。它将其子视图的位置,大小和外观的控制权委托给一个单独的布局对象。通过提供这样一个自定义的布局对象,你几乎可以实现任何你能想想到的布局。布局继承自UICollectionViewLayout这几个抽象基类。IOS6中以UICollectionViewFlowLayout类的形式提出了一个具体的布局实现。

自定义布局

一般有两种类型的collection view布局:

1.独立于内容的布局计算。

每个cell的位置和外观不是基于其显示的内容,但是所有的cell的显示顺序是基于内容的顺序。可以把默认的flowlayout作为例子。每个cell都基于前一个cell放置(或者如果没有足够的空间,则从下一行开始)。布局对象不必访问实际数据来计算布局。

2.基于内容的布局计算。

布局对象不仅需要取出当前可见cell的数据,还需要从所有记录中取出一些决定当前哪些cell可见的数据。

如果有一个依赖内容的布局,那就是暗示你需要写自定义的布局类了,同时需要自定义一个类继承自UICollectionViewFlowLayout。重写子类的方法来重新布局。

步骤分解

我们今天要完成的例子的效果如下。是不是还比较炫酷。(哈哈,跟着本文敲到最后,你也可以的,相信我!)
自定义collectionView
其实UICollectionView布局非常简单。通过对程序打上断点,你会发现只需重写以下几个UICollectionViewLayout方法就能实现。
-(void)prepareLayout

首先做布局的准备工作,我们主要做两件事:1.用一个数组保存每列cell的origin.x。2.随机生成所有cell的高度,并用数组保存

-(CGSize)collectionViewContentSize

提供collectionView的滚动区域大小。第一次进来的时候是0。

-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect

为每一个cell加一个layout属性

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath

通过给每个cell的frame重新赋值来改变cell的坐标

-(CGSize)collectionViewContentSize

最终根据cell的布局排列来确定collectionView的滚动区域大小

干完以上的活后,运行你的程序,当当当当。。。成功了。。。简单吧!下面我们来具体分解以下以上的几个方法。

详细分析

-(void)prepareLayout:

首先我们通过以下的方法获取collctionView有多少组,每组有多少列的数据

//初始化数据
    _numberOfSections = [self.collectionView numberOfSections];//多少组
    _numberOfCellsInSections = [self.collectionView numberOfItemsInSection:0];//每组多少个cell

再计算出每列cell的origin.x,并用数组保存起来

//计算出每个cell的宽度
    _cellWidth = (SCREEN_WIDTH - (_columnCount - 1)*_padding)/_columnCount;
//为每个cell计算x坐标
    _cellXArray = [NSMutableArray arrayWithCapacity:_columnCount];
    for (int i = 0; i<_columnCount; i++) {
        CGFloat tempX = i * (_cellWidth + _padding);
        [_cellXArray addObject:@(tempX)];
    }

最后随机生成所有cell的高度,保存到数组中

 //随机生成cell的高度
    _cellHeightArray = [NSMutableArray arrayWithCapacity:_numberOfCellsInSections];
    for (int i = 0; i<_numberOfCellsInSections; i++) {
        CGFloat cellHeight = arc4random() % (_cellMaxHeight - _cellMinHeight) + _cellMinHeight;
        [_cellHeightArray addObject:@(cellHeight)];
    }
-(CGSize)collectionViewContentSize

在刚进来的时候会走一次,为每个可见cell重新赋值frame之后又会走一次。maxCellYArrayWithArray:方法是用来求最大的cell的y坐标。以此来确定collection view 的滚动区域

  -(CGSize)collectionViewContentSize{
        CGFloat height = [self maxCellYArrayWithArray:_cellYArray];
        return CGSizeMake(SCREEN_WIDTH, height);
    }
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect

为每一个cell加一个layout属性,返回一个数组。
initCellYArray方法是创建一个数组用来保存每列即将要排列的cell的origin.y

 -(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
    
    [self initCellYArray];
    
    NSMutableArray *array = [NSMutableArray array];
    
    for (int i = 0; i<_numberOfCellsInSections; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        
        UICollectionViewLayoutAttributes *attribute = [self layoutAttributesForItemAtIndexPath:indexPath];
        
        [array addObject:attribute];
    }
        return array;
    }
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath

通过给每个cell的frame重新赋值来改变cell的坐标。你需要布局多少个cell,那么这个方法就会走多少次。每次都为每个cell重新赋上属性。本文里面只包含了它的frame。你还可以添加其他的属性,建议点开UICollectionViewLayoutAttributes的源文件查看。

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
    UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    
    CGRect frame = CGRectZero;
    CGFloat cellHeight = [_cellHeightArray[indexPath.row] floatValue];//cell的高度
    
    NSInteger minYIndex = [self minCellYArrayWithArray:_cellYArray];//cellY最小值的索引
    CGFloat tempX = [_cellXArray[minYIndex] floatValue];//x坐标
    CGFloat tempY = [_cellYArray[minYIndex] floatValue];//y坐标
    
    frame = CGRectMake(tempX, tempY, _cellWidth, cellHeight);//通过哪列最新的Y最大坐标,判断哪列的cell最短,就在哪列加一个cell

    //修改列里面的Y坐标
    _cellYArray[minYIndex] = @(tempY + cellHeight + _padding);
    
    attribute.frame = frame;
    
    return attribute;
    }

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

推荐阅读更多精彩内容