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
一些可以根据 NSIndexPath
和 section
来定制独立的布局属性,该协议提供多个可选方法。
@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
的高度设置在40
到80
之间:
#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;
中,规划具体布局,接着,计算出瀑布流布局所需要滚动内容的高,以及在这么大的滚动内容大小下,每一个 cell
的 itemsize
要设置多大的平均值。
具体代码实现。
自定义瀑布流头文件 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);
UICollectionViewDelegate
和 UICollectionViewDataSource
的相关实现:
- (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
上一篇: 二:Redis快速入门及应用