前言
相对于UITableView而言,UICollectionView具有更高的定制性和灵活性。它将其子视图的位置,大小和外观的控制权委托给一个单独的布局对象。通过提供这样一个自定义的布局对象,你几乎可以实现任何你能想想到的布局。布局继承自UICollectionViewLayout这几个抽象基类。IOS6中以UICollectionViewFlowLayout类的形式提出了一个具体的布局实现。
自定义布局
一般有两种类型的collection view布局:
1.独立于内容的布局计算。
每个cell的位置和外观不是基于其显示的内容,但是所有的cell的显示顺序是基于内容的顺序。可以把默认的flowlayout作为例子。每个cell都基于前一个cell放置(或者如果没有足够的空间,则从下一行开始)。布局对象不必访问实际数据来计算布局。
2.基于内容的布局计算。
布局对象不仅需要取出当前可见cell的数据,还需要从所有记录中取出一些决定当前哪些cell可见的数据。
如果有一个依赖内容的布局,那就是暗示你需要写自定义的布局类了,同时需要自定义一个类继承自UICollectionViewFlowLayout。重写子类的方法来重新布局。
步骤分解
我们今天要完成的例子的效果如下。是不是还比较炫酷。(哈哈,跟着本文敲到最后,你也可以的,相信我!)
其实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;
}