iOS UICollectionViewFlowLayout 与瀑布流

UICollectionView

当我们使用代码行对 UICollectionView 进行初始化时,都不忘在前面创建一个 UICollectionViewFlowLayout 对象。因为我们可以通过UICollectionViewFlowLayout 来设定符合我们需求的 UICollectionView 布局。接下来,就让我们先来谈谈 UICollectionViewFlowLayout 的使用。

一、UICollectionViewFlowLayout 的使用

首先初始化一个 UICollectionViewFlowLayout 对象:

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];

UICollectionViewCell 的相关布局约束:

 // 最小行间距,默认是0
 layout.minimumLineSpacing = 5;
 // 最小左右间距,默认是10
 layout.minimumInteritemSpacing = 5;
 // 区域内间距,默认是 UIEdgeInsetsMake(0, 0, 0, 0)
 layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

UICollectionViewCell 上添加 UILabel 以便测试:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    
    // cell 序号
    UILabel *label = (UILabel *)[cell viewWithTag:10];
    
    if (label == nil) {
        
        [cell setBackgroundColor:[UIColor cyanColor]];
        
        label = [[UILabel alloc]init];
        label.textAlignment = NSTextAlignmentCenter;
        label.font = [UIFont systemFontOfSize:80];
        label.adjustsFontSizeToFitWidth = YES;
        label.tag = 10;
        [cell addSubview:label];
    }
    [label setFrame:CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height)];
    label.text = [[NSString alloc] initWithFormat:@"%ld", indexPath.row + 1];

    return cell;
}

其中行间距和左右间距只能设置最小值。
所谓的最小值,就是系统在设置布局时,首先按 cell 的宽和最小左右间距横着排版下去一行最多可以排多少个,如果还有剩余空间的话,系统会将剩下的宽度平均分配该行的每一个左右间距,所以实际效果的左右间距会比设置的左右间距大的多。而行间距也是类似,在高度不一样的一行 cell 中,只有最高的 cell 是最小行间距,其余高度小的 cell 还会分配到与该行最高 cell 的高度差值“补贴”,具体情况后面有演示。

8 plus 模拟器上,cell 的最小左右间距设置为 5
cell 设置大小为 120 x 120 时,

layout.itemSize = CGSizeMake(120, 120);

效果如下:


cell 大小 120x120

cell 设置大小为 100 x 100 时,

layout.itemSize = CGSizeMake(100, 100);

效果如下:


cell 大小 100x100

cell 设置大小为 80 x 80 时,

layout.itemSize = CGSizeMake(80, 80);

效果如下:


cell 大小 80x80

从模拟器的展示结果来看,排版的剩余宽度越大,左右间距也随之分配到更多的空间。比较过程中,我们也可以看到区域内间距 sectionInset 始终都是不变的,但是左右间距会因排版剩余宽度过多而变大,导致与不变的区域内间距差距就更大了。
有时候为了美观,我们需要将内间距和左右间距设置成一样的大小。这时,我们需要像系统一样先预算出在 cell 大小固定和左右间距取最小值的情况下,在一行中可以排版多少个 cell 。只不过,在分配剩余的宽度空间时,我们不仅将其分配给左右间距,还有区域内间距。

cell 大小 80x80 为例:

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(80, 80);
// 以最小间距为10计算间距
// 每行可放多少 cell
NSInteger nCountCell = (kScreenWidth - 10) / (layout.itemSize.width + 10);
// 平均后的间距
CGFloat fSpacing = (kScreenWidth - layout.itemSize.width * nCountCell) / (nCountCell + 1);
layout.minimumInteritemSpacing = fSpacing;
layout.minimumLineSpacing = fSpacing;
layout.sectionInset = UIEdgeInsetsMake(fSpacing, fSpacing, fSpacing, fSpacing);

执行结果:

等间距

以上讲的 UICollectionViewFlowLayout 使用是基于 cell 的大小是统一的情况。当有不同大小的 cell 放在同一个 UICollectionView 里,还要符合我们的心意排版,这个时候就需要自定义 UICollectionViewFlowLayout 类了,而专门适用于这种参差不齐 cell 的布局有个专业名词--“瀑布流”。

以下来自百度百科关于瀑布流的介绍:

瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布局的网站是Pinterest,逐渐在国内流行开来。国内大多数清新站基本为这类风格。

二、UICollectionViewFlowLayout 实现瀑布流

有句谚语是这样讲的:

巧妇难为无米之炊

要想实现瀑布流布局,就需要先实现大小参差不齐的 cell,而要实现大小参差不齐的 cell ,就需要用到 UICollectionViewDelegate 的扩展协议 UICollectionViewDelegateFlowLayout,这个协议是在 UICollectionViewDelegate 的基础上增加了 UICollectionViewFlowLayout 一些可以根据 NSIndexPathsection 来定制独立的布局属性,该协议提供多个可选方法。

@protocol UICollectionViewDelegateFlowLayout <UICollectionViewDelegate>
@optional

- (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;

@end

而我们要实现大小参差不齐的 cell ,就需要用到这个方法:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;

我们使用随机数 arc4random()cell 的高度设置在4080之间:

#pragma mark - UICollectionViewDelegateFlowLayout
//  cell 大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    // 随机高度
    return CGSizeMake(80,  40 + arc4random() % 40);
}

执行结果:

高度在40~80之间的 cell

从执行结果图中,我们可以看出系统在布局参差不齐的 cell 时 ,将会以每行高度最高的 cell 为基准,其余矮的 cell 的中心和这个高 cell 的中心保持在同一条水平线上。因此我们实现瀑布流,就需要先打破这个规则(每个 cell 依赖于它所在行的最高 cell 来布局它的位置),让每一个 cell 往已经排好且最短的列排版。
而要让每一个 cell 像人一样能思考地进行排版,我们需要重载 UICollectionViewFlowLayout 对所有 cell 属性的布局方法:

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

因为 UICollectionView 的滚动内容大小是由 cell 的大小和数量决定的,系统又会以每一行最高 cell 的高度定为该行的高,然后逐个排版下去,导致重载布局方法之后,滚动到底部会出现一大堆空白。

布局之后剩余的空白区

虽然 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;方法可以为我们定制不同 cell 的大小,但是 UICollectionView 滚动内容大小是根据系统的排版标准去算的,会导致滚动空间的溢出,所以这个方法在这种有上下缩进的情况下不适用。
因此为了准确地计算出 UICollectionView 合理的滚动内容大小,首先,我们需要在每一次更新布局前就引入各个 cell 的大小到自定义的 UICollectionViewFlowLayout 中,然后在准备方法 - (void)prepareLayout; 中,规划具体布局,接着,计算出瀑布流布局所需要滚动内容的高,以及在这么大的滚动内容大小下,每一个 cellitemsize 要设置多大的平均值。

具体代码实现。
自定义瀑布流头文件 MyFlowLayout.h

#import <UIKit/UIKit.h>

/**
 自定义瀑布流布局
 */
@interface MyFlowLayout : UICollectionViewFlowLayout

/**
 瀑布流布局方法
 
 @param itemWidth item 的宽度
 @param itemHeightArray item 的高度数组
 */
- (void)flowLayoutWithItemWidth:(CGFloat)itemWidth itemHeightArray:(NSArray<NSNumber *> *)itemHeightArray;

@end

实现文件 MyFlowLayout.m

#import "MyFlowLayout.h"



@interface MyFlowLayout ()

/**
 item 的高度数组
 */
@property (nonatomic, copy) NSArray<NSNumber *> *arrItemHeight;

/**
 cell 布局属性集
 */
@property (nonatomic, strong) NSArray<UICollectionViewLayoutAttributes *> *arrAttributes;

@end

@implementation MyFlowLayout

/**
 瀑布流布局方法
 
 @param itemWidth item 的宽度
 @param itemHeightArray item 的高度数组
 */
- (void)flowLayoutWithItemWidth:(CGFloat)itemWidth itemHeightArray:(NSArray<NSNumber *> *)itemHeightArray {
    
    self.itemSize = CGSizeMake(itemWidth, 0);
    self.arrItemHeight = itemHeightArray;
    [self.collectionView reloadData];
}

- (void)prepareLayout {
    
    [super prepareLayout];
    
    // item 数量为零不做处理
    if ([self.arrItemHeight count] == 0) {
        
        return;
    }
    
    // 计算一行可以放多少个项
    NSInteger nItemInRow = (self.collectionViewContentSize.width - self.sectionInset.left - self.sectionInset.right + self.minimumInteritemSpacing) / (self.itemSize.width + self.minimumInteritemSpacing);
    // 对列的长度进行累计
    NSMutableArray *arrmColumnLength = [NSMutableArray arrayWithCapacity:100];
    for (NSInteger i = 0; i < nItemInRow; i++) {
        
        [arrmColumnLength addObject:@0];
    }
    
    NSMutableArray *arrmTemp = [NSMutableArray arrayWithCapacity:100];
    // 遍历设置每一个item的布局
    for (NSInteger i = 0; i < [self.arrItemHeight count]; i++) {
        
        // 设置每个item的位置等相关属性
        NSIndexPath *index = [NSIndexPath indexPathForItem:i inSection:0];
        // 创建每一个布局属性类,通过indexPath来创建
        UICollectionViewLayoutAttributes *attris = [self layoutAttributesForItemAtIndexPath:index];
        CGRect recFrame = attris.frame;
        // 有数组得到的高度
        recFrame.size.height = [self.arrItemHeight[i] doubleValue];
        // 最短列序号
        NSInteger nNumShort = 0;
        // 最短的长度
        CGFloat fShortLength = [arrmColumnLength[0] doubleValue];
        // 比较是否存在更短的列
        for (int i = 1; i < [arrmColumnLength count]; i++) {

            CGFloat fLength = [arrmColumnLength[i] doubleValue];
            if (fLength < fShortLength) {

                nNumShort = i;
                fShortLength = fLength;
            }
        }
        // 插入到最短的列中
        recFrame.origin.x = self.sectionInset.left + (self.itemSize.width + self.minimumInteritemSpacing) * nNumShort;
        recFrame.origin.y = fShortLength + self.minimumLineSpacing;
        // 更新列的累计长度
        arrmColumnLength[nNumShort] = [NSNumber numberWithDouble:CGRectGetMaxY(recFrame)];
        
        // 更新布局
        attris.frame = recFrame;
        [arrmTemp addObject:attris];
    }
    self.arrAttributes = arrmTemp;
    
    // 因为使用了瀑布流布局使得滚动范围是根据 item 的大小和个数决定的,所以以最长的列为基准,将高度平均到每一个 cell 中
    // 最长列序号
    NSInteger nNumLong = 0;
    // 最长的长度
    CGFloat fLongLength = [arrmColumnLength[0] doubleValue];
    // 比较是否存在更短的列
    for (int i = 1; i < [arrmColumnLength count]; i++) {

        CGFloat fLength = [arrmColumnLength[i] doubleValue];
        if (fLength > fLongLength) {

            nNumLong = i;
            fLongLength = fLength;
        }
    }
    // 在大小一样的情况下,有多少行
    NSInteger nRows = ([self.arrItemHeight count] + nItemInRow - 1) / nItemInRow;
    self.itemSize = CGSizeMake(self.itemSize.width, (fLongLength + self.minimumLineSpacing) / nRows - self.minimumLineSpacing);
    
}

// 返回所有的 cell 布局数组
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    
    return self.arrAttributes;
}
@end

在视图控制器上的调用:

MyFlowLayout *layout = [[MyFlowLayout alloc] init];
// 创建随机高度的数组
NSMutableArray *arrmHeight = [NSMutableArray arrayWithCapacity:100];
for (NSInteger i = 0; i < 50; i++) {
    
    // 40~80 的随机高度
    [arrmHeight addObject:[NSNumber numberWithDouble:40 + arc4random() % 40]];
}
[layout flowLayoutWithItemWidth:80 itemHeightArray:arrmHeight];
// 以最小间距为10计算间距
// 每行可放多少 cell
NSInteger nCountCell = (kScreenWidth - 10) / (layout.itemSize.width + 10);
// 平均后的间距
CGFloat fSpacing = (kScreenWidth - layout.itemSize.width * nCountCell) / (nCountCell + 1);
layout.minimumInteritemSpacing = fSpacing;
layout.minimumLineSpacing = fSpacing;
layout.sectionInset = UIEdgeInsetsMake(fSpacing, fSpacing, fSpacing, fSpacing);

self.collection = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 64, kScreenWidth, kScreenHeight - 64) collectionViewLayout:layout];
_collection.backgroundColor = [UIColor whiteColor];
_collection.delegate = self;
_collection.dataSource = self;
[self.view addSubview:_collection];

// 注册 cell
[_collection registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:cellId];

执行结果:


平均 cell 的 itemSize 之后

除了这种高度参差不齐的 cell,我们在开发过程中,还会遇到宽度参差不齐的布局。

三、UICollectionViewFlowLayout 的靠左布局

因为在宽度参差不齐的布局中,不会影响滚动内容的高度,所以我们可以通过 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath; 来实现 itemSize 的不规则:

//  cell 大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    // 随机宽度
    return CGSizeMake(40 + arc4random() % 4 * 20, 50);
}

UICollectionViewFlowLayout 的属性设置:

//创建流水布局对象
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

UICollectionViewDelegateUICollectionViewDataSource 的相关实现:

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

    return 70;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
        
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    
    // Configure the cell
    UILabel *label = (UILabel *)[cell viewWithTag:10];
    
    if (label == nil) {

        // 添加子控件
        label = [[UILabel alloc]init];
        label.textAlignment = NSTextAlignmentCenter;
        label.font = [UIFont systemFontOfSize:30];
        label.adjustsFontSizeToFitWidth = YES;
        label.tag = 10;
        [cell addSubview:label];
    }
    [label setFrame:CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height)];
    label.text = [[NSString alloc] initWithFormat:@"%ld", indexPath.row + 1];

    return cell;
}

执行结果:

宽度不规则的 cell

从结果中可以看出,因为宽度不一样导致每一行排版剩余的空间不一样,导致左右间距也就相差较大了。
为了排版美观,我们有时候需要靠左布局。之前因为项目有过这样的产品需求,在网络上找到了一个第三方 UICollectionViewFlowLayout 自定义类--UICollectionViewLeftAlignedLayout
UICollectionViewLeftAlignedLayout 下载地址

在视图控制器上的使用:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

执行结果:

靠左布局的 cell

因为这次写得比较久,思路有些乱,希望有问题帮忙指出来,谢谢!

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

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,728评论 1 92
  • 本人总是很“二”具体表现很多,比如从前坐车会坐反,出门忘带钱,把家里的钥匙锁在里面….诸多事情,现在想来实在是好笑...
    孙润阅读 301评论 6 3
  • 感恩今天上午可以在家收拾东西,归类放置。 感恩先生中午帮忙把凳子从店里带回来,他说了一句很暖心的话:“只要你高兴就...
    遇见念念相续阅读 112评论 0 0
  • 我在蛋糕店里做了个蛋糕。 上面有水果 黄桃、弥猴桃,还有葡萄...
    竹青奶茶阅读 185评论 0 3
  • 《遗落的南境——湮灭》被称作是有克鲁苏神话精神的和新怪谭式。与《三体》的硬科幻是完全不一样的种类,也有完全不同的阅...
    晏灯阅读 2,110评论 0 0