使用UITableView
作为表格来展示数据完全没有问题,但仍有许多局限性,对于一些更加复杂的布局样式,就有些力不从心了
比如,UITableView
只允许表格每一行只能显示一个cell
,而不能在一行中显示多个cell
,对于这种更为复杂的布局需求,UICollectionView可以提供更好的支持,有着更大的灵活性和扩展性,其主要优势有以下几点:
- 支持横向 + 纵向两个方向的布局
- 更加灵活的布局方式、动画
- 可以动态对布局进行重设(切换layout)
目录
UICollectionView的基础使用
作为升级版的UITableView,UICollectionView有许多与UITableView相似的点,可以通过对比UITableView总结来进行学习:
row
--->item
:由于一行可以展示多个视图,row不能准确表达- (void)registerClass:(nullable Class)cellClass forCellWithReuseIdentifier:(NSString *)identifier;
:注册cell类型,并设置重用ID- (__kindof UICollectionViewCell *)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
:cell复用,与UITableViewCell的注册机制一样,每次调用这个方法时,如果复用池中没有可复用的cell(cell为空),会根据重用ID自动创建一个新的cell并返回
显示UICollectionView
下面展示一个最基本的CollectionView:
objectivec
@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor blueColor];
//创建布局策略
UICollectionViewFlowLayout* flowLayOut = [[UICollectionViewFlowLayout alloc] init];
//第二个参数flowLayout用于生成UICollectionView的布局信息,后面会解释这个布局类
UICollectionView* collectionView = [[UICollectionView alloc] initWithFrame: self.view.bounds collectionViewLayout: flowLayOut];
collectionView.dataSource = self;
//注册cell
[collectionView registerClass: [UICollectionViewCell class] forCellWithReuseIdentifier: @"UICollectionViewCell"];
[self.view addSubview: collectionView];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 21;
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier: @"UICollectionViewCell" forIndexPath: indexPath];
//cell的颜色
cell.backgroundColor = [UIColor cyanColor];
//cell默认是50X50的大小
return cell;
}
UICollectionViewLayout布局策略(UICollectionViewLayoutAttributes)
layout
管理多个attributes
,一个cell
就会对应一个布局信息attribute
UICollectionViewFlowLayout流式布局
作为一个生成布局信息(item的大小、位置、3D变换等)的抽象类,要实际使用需要继承,比如系统提供的继承于UICollectionViewLayout
的一个流式布局类UICollectionViewFlowLayout,下面使用这个类来进行布局:
objectivec
UICollectionViewFlowLayout* flowLayOut = [[UICollectionViewFlowLayout alloc] init];
//设置布局方向
flowLayOut.scrollDirection = UICollectionViewScrollDirectionVertical;
flowLayOut.minimumLineSpacing = 10; //行间距
flowLayOut.minimumInteritemSpacing = 10; //列间距
//设置每个item的尺寸
flowLayOut.itemSize = CGSizeMake(self.view.frame.size.width / 2 - 5, 300);
// flowLayOut.itemSize = CGSizeMake(self.view.frame.size.width / 2 - 50, 300);
scrollDirection属性
该类的scrollDirection
属性用于设置布局的方向,支持的布局方向枚举如下:
若将枚举值改为UICollectionViewScrollDirectionHorizontal
,将会这样排列:
前者当一行满时,另起一行;后者当一列满时,另起一列
minimumInteritemSpacing最小列间距属性
系统通过minimumInteritemSpacing
属性计算一行可以放多少个item,当发现放不下计算好的item个数时,为了撑满所在行,此值就会变大,比如:
objectivec
flowLayOut.itemSize = CGSizeMake(self.view.frame.size.width / 2 - 12, 300);
minimumLineSpacing
最小行间距同理
九宫格布局
和UITableView
类似,UICollectionView 不仅内部也有一套复用机制来对注册的cell
进行复用,而且也是通过dataSource
和delegate
协议来进行数据填充 和相关属性设置的:
objectivec
@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor blueColor];
UICollectionViewFlowLayout* flowLayOut = [[UICollectionViewFlowLayout alloc] init];
flowLayOut.scrollDirection = UICollectionViewScrollDirectionVertical;
CGFloat side = (self.view.bounds.size.width - 12) / 3;
flowLayOut.minimumLineSpacing = 6;
flowLayOut.minimumInteritemSpacing = 6;
flowLayOut.itemSize = CGSizeMake(side, side);
UICollectionView* collectionView = [[UICollectionView alloc] initWithFrame: self.view.bounds collectionViewLayout: flowLayOut];
collectionView.dataSource = self;
collectionView.delegate = self;
[collectionView registerClass: [UICollectionViewCell class] forCellWithReuseIdentifier: @"UICollectionViewCell"];
[self.view addSubview: collectionView];
}
//设置分区数
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
//设置每个分区的
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 10;
}
//每条item上cell的UI展现
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier: @"UICollectionViewCell" forIndexPath: indexPath];
//随机颜色
cell.backgroundColor = [UIColor colorWithRed: arc4random() % 255 / 255.0 green: arc4random() % 255 / 255.0 blue: arc4random() % 255 / 255.0 alpha: 1.0];
return cell;
}
更加灵活的流式布局
objectivec
//实现delegate,自定义任何位置上cell的样式
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.item % 2) {
return CGSizeMake((self.view.bounds.size.width - 12) / 3, (self.view.bounds.size.width - 12) / 3);
} else {
return CGSizeMake((self.view.bounds.size.width - 12) / 6, (self.view.bounds.size.width - 12) / 6);
}
}
参差瀑布流布局
在很多应用程序中都有瀑布流效果,即分成两列或者多列进行数据的展示 ,每条数据item
上cell
的高度也随数据多少不同而显示得参差不齐
使用系统提供的原生UICollectionViewFlowLayout
类进行布局设置很难实现这样的效果,开发者可以自定义一个它的子类来实现瀑布流式的效果
流布局又称瀑布流布局,是一种比较流行的网页布局模式,视觉效果多表现为参差不齐的多栏布局。
声明MyLayout类
创建一个布局类MyLayout
,使其继承于UICollectionViewFlowLayout ,并新增一个属性itemCount
用于设置要布局的item的数量:
MyLayout.h
objectivec
@interface MyLayout : UICollectionViewFlowLayout
@property (nonatomic, assign)NSInteger itemCount;
@end
设置MyLayout相关属性
UICollectionViewLayout
类提供了prepareLayout
来做布局前的准备,这个时机会调用此方法,可以在其中设置布局配置数组:
MyLayout.m
objectivec
@implementation MyLayout {
//自定义的布局配置数组,保存每个cell的布局信息attribute
NSMutableArray* _attributeArray;
}
//布局前的准备会调用这个方法
- (void)prepareLayout {
_attributeArray = [[NSMutableArray alloc] init];
[super prepareLayout];
//为方便演示,设置为静态的2列
//计算每一个Item的宽度
//sectionInset表示item距离section四个方向的内边距 UIEdgeInsetsMake(top, left, bottom, right)
CGFloat WIDTH = ([UIScreen mainScreen].bounds.size.width - self.sectionInset.left - self.sectionInset.right - self.minimumInteritemSpacing) / 2;
//创建数组保存每一列的高度(实际是总高度),这样就可以在布局时始终将下一个Item放在最短的列下面
CGFloat colHeight[2] = {self.sectionInset.top, self.sectionInset.bottom};
//遍历每一个Item来设置布局
for (int i = 0; i < self.itemCount; ++i) {
//每个Item在CollectionView中的位置
NSIndexPath* indexPath = [NSIndexPath indexPathForItem: i inSection: 0];
//通过indexPath创建一个布局属性类
UICollectionViewLayoutAttributes* attris = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
//随机一个高度,在77~200之间
CGFloat height = arc4random() % 123 + 77;
//那一列高度小,则放到哪一列下面
int indexCol = 0; //标记短的列
if (colHeight[0] < colHeight[1]) {
//将新的Item高度加入到短的一列
colHeight[0] = colHeight[0] + height + self.minimumLineSpacing;
indexCol = 0;
} else {
colHeight[1] = colHeight[1] + height + self.minimumLineSpacing;
indexCol = 1;
}
//设置Item的位置
attris.frame = CGRectMake(self.sectionInset.left + (self.minimumInteritemSpacing + WIDTH) * indexCol, colHeight[indexCol] - height - self.minimumLineSpacing, WIDTH, height);
[_attributeArray addObject: attris];
}
//给itemSize赋值,确保滑动范围在正确区间,这里是通过将所有的Item高度平均化计算出来的
//(以最高的列为标准)
if (colHeight[0] > colHeight[1]) {
self.itemSize = CGSizeMake(WIDTH, (colHeight[0] - self.sectionInset.top) * 2 / self.itemCount - self.minimumLineSpacing);
} else {
self.itemSize = CGSizeMake(WIDTH, (colHeight[1] - self.sectionInset.top) * 2 / self.itemCount - self.minimumLineSpacing);
}
}
//此系统提供的方法会返回设置好的布局数组
- (NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect: (CGRect)rect {
return _attributeArray;
}
@end
- 先声明一个
_attributeArray
数组存放每个item的布局信息 - 在UICollectionView进行布局时,首先会调用其Layout布局类的
prepareLayout
方法,在这个方法中可以进行每个item
布局属性的相关计算操作 - 具体每个item的布局属性实际是保存在UICollectionViewLayoutAttributes 类对象中的,其中包括
size
、frame
等信息,并与每个item一一对应 - 在
prepareLayout
方法准备好所有item的Attributes布局属性后,以数组的形式调用layoutAttributesForElementsInRect:
方法来返回给UICollectionView进行界面的布局
ViewController.m引入MyLayout文件并使用这个类:
objectivec
#import "MyLayout.h"
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
//使用自定义的layout类
MyLayout* myLayout = [[MyLayout alloc] init];
myLayout.itemCount = 21;
UICollectionView* collectionView = [[UICollectionView alloc] initWithFrame: self.view.bounds collectionViewLayout: myLayout];
collectionView.dataSource = self;
collectionView.delegate = self;
[self.view addSubview: collectionView];
[collectionView registerClass: [UICollectionViewCell class] forCellWithReuseIdentifier: @"MyUICollectionView"];
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 21;
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell* myCell = [collectionView dequeueReusableCellWithReuseIdentifier: @"MyUICollectionView" forIndexPath: indexPath];
myCell.backgroundColor = [UIColor colorWithRed: arc4random() % 255 / 255.0 green: arc4random() % 255 / 255.0 blue: arc4random() % 255 / 255.0 alpha: 1.0];
return myCell;
}
运行结果:
圆环布局
通过上面的代码我们可知,UICollectionView的布局原理是采用Layout
类进行每个item布局信息的配置的,具体的配置信息由UICollectionViewLayoutAttributes
存储
明白了这个机制后,我们可以发挥想象实现更加炫酷复杂的布局效果
与参差瀑布式布局一样,这里附上圆环布局代码:
CircleLayout.h
objectivec
@interface CircleLayout : UICollectionViewFlowLayout
@property (nonatomic, assign)NSInteger itemCount;
@end
CircleLayout.m
objectivec
@implementation CircleLayout {
NSMutableArray* _attributeArray;
}
- (void)prepareLayout {
[super prepareLayout];
//获取item的个数
self.itemCount = (int)[self.collectionView numberOfItemsInSection: 0];
_attributeArray = [[NSMutableArray alloc] init];
//先设定大圆的半径,取长和宽的最小值
CGFloat radius = MIN(self.collectionView.frame.size.width, self.collectionView.frame.size.height) / 2;
//计算圆心位置
CGPoint center = CGPointMake(self.collectionView.frame.size.width / 2, self.collectionView.frame.size.height / 2);
//每个item大小为50*50,即半径为25
for (int i = 0; i < self.itemCount; ++i) {
NSIndexPath* indexPath = [NSIndexPath indexPathForItem: i inSection: 0];
UICollectionViewLayoutAttributes* attris = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
//设置item大小
attris.size = CGSizeMake(50, 50);
//计算每个item中心坐标(圆心位置)
float x = center.x + cosf(2 * M_PI / self.itemCount * i) * (radius - 25);
float y = center.y + sinf(2 * M_PI / self.itemCount * i) * (radius - 25);
attris.center = CGPointMake(x, y);
[_attributeArray addObject: attris];
}
}
//设置内容区域的大小
//作用同赋值contentSize属性一样,返回一个CollectionView可以滑动的范围尺寸
- (CGSize)collectionViewContentSize {
return self.collectionView.frame.size;
}
- (NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
return _attributeArray;
}
@end
ViewController.m
objectivec
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
//使用自定义的layout类
// MyLayout* myLayout = [[MyLayout alloc] init];
// myLayout.itemCount = 21;
CircleLayout* circleLayout = [[CircleLayout alloc] init];
UICollectionView* collectionView = [[UICollectionView alloc] initWithFrame: self.view.bounds collectionViewLayout: circleLayout];
collectionView.backgroundColor = [UIColor blackColor];
collectionView.dataSource = self;
collectionView.delegate = self;
[self.view addSubview: collectionView];
// [collectionView registerClass: [UICollectionViewCell class] forCellWithReuseIdentifier: @"MyUICollectionView"];
[collectionView registerClass: [UICollectionViewCell class] forCellWithReuseIdentifier: @"CircleUICollectionView"];
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 11;
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
// UICollectionViewCell* myCell = [collectionView dequeueReusableCellWithReuseIdentifier: @"MyUICollectionView" forIndexPath: indexPath];
UICollectionViewCell* circleCell = [collectionView dequeueReusableCellWithReuseIdentifier: @"CircleUICollectionView" forIndexPath: indexPath];
circleCell.layer.masksToBounds = YES;
circleCell.layer.cornerRadius = 25;
circleCell.backgroundColor = [UIColor colorWithRed: arc4random() % 255 / 255.0 green: arc4random() % 255 / 255.0 blue: arc4random() % 255 / 255.0 alpha: 1.0];
return circleCell;
}
@end
运行结果:
总结
UICollectionView其实算是特殊Flow布局的UITableView,但简单的列表仍可以使用UITableView
UICollectionView最大的优势就是通过自定义Layout
,实现cell的布局,整体的思路就是:通过一些几何计算,设置好每个item
的布局位置和大小
在之后的学习中,编者也将参考这片文章:一篇较为详细的 UICollectionView 使用方法总结