在上一篇文章中,为大家介绍了UICollectionView的基本使用方法,过程和内容都比较简单,有兴趣看的同学可以点击这里,本次将为大家介绍的是如何使用UICollectionLayout自定义复杂的布局。
1、UICollectionViewLayout简介
(1)基本方法
在UICollectionViewLayout时,我们主要会重写它的以下几个方法
- (void)prepareLayout;
prepareLayout会在三个时机调用,第一次初始化layout的时候,刷新layout的时候以及方法- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
返回YES的时候
- (CGSize)collectionViewContentSize;
该方法返回collectionView的内容的大小
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
该方法会返回rect范围内所有cell的布局属性UICollectionViewLayoutAttributes
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
该方法返回对应indexPath下的cell的布局属性UICollectionViewLayoutAttributes
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;
该方法返回在界面发生变化是是否要重新布局,返回YES则会重新布局
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset;
返回滑动后的collectonView的偏移量(滑动所停止的点),默认返回proposedContentOffset参数的值,在这里我们可以手动设置实际需要的偏移量
#####(2)UICollectionViewLayout与UICollectionViewFlowLayout
在此之前,我们先来简单的关注一下`UICollectionViewFlowLayout`和`UICollectionViewLayout`的关系:`UICollectionViewFlowLayout`是系统为我们封装的一个继承于`UICollectionViewLayout`的子类,系统已经写好了布局,所以如果我们在```- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
```方法中调用 ``` NSArray *attributesArr = [super layoutAttributesForElementsInRect:rect];```,可以得到系统为我们写好的布局,但是如果直接继承于UICollectionViewLayout,上述方法得不到任何布局,所以我们必须要重写`- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
`方法,在方法中写好布局并调用,这样才能为cell布局。
#####(3)UICollectionViewLayoutAttributes
关于cell的布局,我们还需要着重看一个类:UICollectionViewLayoutAttributes,它就是我们上面一直所说的cell的布局类,cell所有的布局属性都是要写到该类中的,那它到底都有哪些属性呢:
@property (nonatomic) CGRect frame;
@property (nonatomic) CGPoint center;
@property (nonatomic) CGSize size;
@property (nonatomic) CATransform3D transform3D;
@property (nonatomic) CGRect bounds );
@property (nonatomic) CGAffineTransform transform ;
@property (nonatomic) CGFloat alpha;
@property (nonatomic) NSInteger zIndex; // default is 0
@property (nonatomic, getter=isHidden) BOOL hidden;
@property (nonatomic, strong) NSIndexPath *indexPath;
改变了这些属性,并传递给layout,就可以改变cell的布局,所以归根到底,不管多复杂的布局,都是在改变这些属性。
###自定义UICollectionViewLayout具体实现
下面,我们就在具体的实例中看一下,如果使用自定义layout:
####先为大家贴出代码:
######创建一个继承于UICollectionViewLayout的子类
>.h
import <UIKit/UIKit.h>
@interface My_1Layout : UICollectionViewLayout
@end
>.m
import "My_1Layout.h"
@interface My_1Layout()
{
UIEdgeInsets _edgeInset;//内边距
CGFloat _lineSpacing;//行间距
CGFloat _columnsSpacing;//列间距
NSInteger _columnsNum;//列数
NSMutableArray *_columnsHeightArray;//用来存放所有列的高度
CGFloat _maxHeight;//collectionContent最大高度
}
@property (nonatomic,strong) NSMutableArray *attributesArray;//用来存放所有的cell的布局
@end
@implementation My_1Layout
-
(instancetype)init{
if ([super init]) {
_edgeInset = UIEdgeInsetsMake(5, 10, 5, 10);
_lineSpacing = 10;
_columnsSpacing = 10;
_columnsNum = 3;
_maxHeight = _edgeInset.top;
_columnsHeightArray = [NSMutableArray new];
_columnsHeightArray = [NSMutableArray arrayWithCapacity:_columnsNum];
}
return self;
} -
(void)prepareLayout{
/**
切记,一定要先调用父类的prepareLayout
*/
[super prepareLayout];[_columnsHeightArray removeAllObjects];
for (int i = 0; i < _columnsNum ; i ++) {
[_columnsHeightArray addObject:[NSNumber numberWithInteger:_edgeInset.top]];
}[self.attributesArray removeAllObjects];
/**
调用layoutAttributesForItemAtIndexPath:方法,根据collectionView中cell的个数,使用for循环,创建对应个数的cell的attributes,并存放到_columnsHeightArray数组中(也可以将该过程放到layoutAttributesForElementsInRect:中去执行)
/
NSInteger cellNum = [self.collectionView numberOfItemsInSection:0];
for (int i = 0; i < cellNum; i ++) {
NSIndexPathindexPath=[NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attri = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attributesArray addObject:attri];
}
}
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{return YES;
}
-(NSArray<UICollectionViewLayoutAttributes > )layoutAttributesForElementsInRect:(CGRect)rect{
/
直接返回之前存放好的所有cell的attributes(也可以将prepareLayout方法中for循环创建attributes的过程放到这里执行)
/
return self.attributesArray;
}
-(UICollectionViewLayoutAttributes)layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath{
UICollectionViewLayoutAttributesattributes=[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];CGFloat cellW = (kScreenWidth-_edgeInset.left-_edgeInset.right-(_columnsNum-1)*_columnsSpacing)/_columnsNum;
CGFloat cellH = indexPath.item%2==0?160:125;
// 应该添加cell的列号
NSInteger minHeightColumn = 0;
// 应该添加cell的列的高度
CGFloat minColumnHeight = [_columnsHeightArray[minHeightColumn] doubleValue];
//循环 获取最小的列的高度和该列的列号
for (int i = 1; i < _columnsHeightArray.count; i ++ ) {
CGFloat tempH = [_columnsHeightArray[i] floatValue];
if (minColumnHeight > tempH) {
minColumnHeight = tempH;
minHeightColumn = i;
}
}
//为高度最小的列添加cell
CGFloat cellY = [_columnsHeightArray[minHeightColumn] floatValue]+_lineSpacing;
CGFloat cellX = _edgeInset.left + minHeightColumn * (cellW + _columnsSpacing);
attributes.frame = CGRectMake(cellX, cellY, cellW, cellH);
//保存最新的高度
CGFloat newHeight = cellY+cellH;
[_columnsHeightArray replaceObjectAtIndex:minHeightColumn withObject:[NSNumber numberWithInteger:newHeight]];
//返回布局信息
return attributes;
} -
(CGSize)collectionViewContentSize{
//根据最高的列 设置collectionContentSize
[_columnsHeightArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
CGFloat maxHeight = [_columnsHeightArray[idx]floatValue];
if (maxHeight > _maxHeight) {
_maxHeight = maxHeight;
}
}];
return CGSizeMake(kScreenWidth, _maxHeight);
} -
(NSMutableArray *)attributesArray{
if (!_attributesArray) {
_attributesArray = [NSMutableArray new];
}
return _attributesArray;
}
大概思路就是:首先初始化layout的各种属性和变量,在- (void)prepareLayout中循环调用-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:方法,为所有的cell添加布局,最后从-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:中将其返回即可。本例中,
调用layout的方法也很简单,就正常创建layout,并赋给collectionView就可以了:
My_1Layout layout = [[My_1Layout alloc]init];
UICollectionView collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 64, kScreenWidth, kScreenHeight-64) collectionViewLayout:layout];
下面是效果图:
![效果图-1.gif](http:https://img.haomeiwen.com/i1914621/6c873fb7927b595e.gif?imageMogr2/auto-orient/strip)
(注:在开发中,对于需要在外部的控制器中设置layout属性的,包括内边距、行间距、列间距、列数以及cell的初始大小等,可以为layout添加代理,使用代理方法返回)。
下面我们再看一个例子:
首先,我们先创建一个继承于UICollectionViewFlowLayout的layout子类,layout类中不做任何实现,然后在控制器中赋值给collectionView,控制器中关于collectionView和数据源的设置和上例一样,然后运行程序,查看效果:
![效果图-2.PNG](http:https://img.haomeiwen.com/i1914621/315258ac335f087d.PNG?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
我们发现,尽管layout没有做任何布局,但是collectionView任然可以显示,这就说明,UICollectionViewFlowLayout已经为我们做好了一个布局,就是我们现在看到的流水布局,所以,对于继承于UICollectionViewFlowLayout的 类,如果要改变cell的布局,只需要获取系统默认为cell写好的布局,然后再此基础上进行修改就可以了。那么怎样获取UICollectionViewFlowLayout为我们写好的布局呢,使用父类调用-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:。
废话不多说,直接上代码:
/**
*prepareLayout会频繁调用,所以只做一些简单的初始化操作
*/
- (void)prepareLayout{
[super prepareLayout];
// 水平滚动
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
//设置cell大小(可放到外面的控制器中)
self.itemSize = CGSizeMake(200, 200);
//设置内边距
CGFloat inset = (self.collectionView.frame.size.width - self.itemSize.width) * 0.5;
self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset);
}
/**
*设置为YES,collectionView的显示范围发生变化时,就要刷新布局
*/
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
return YES;
}
/**
*返回所有cell的布局属性
*/
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
//获取UICollectionViweFlowLayout已经做好的布局
NSArray attrbutesArray = [super layoutAttributesForElementsInRect:rect];
//计算collectionView可视范围的中心点所对应的collectionView的x值
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;
//以每个cell中心点到centerX的距离为参考,对cell进行缩放
for (UICollectionViewLayoutAttributes attributes in attrbutesArray) {
//cell的中心点到centerX的距离
CGFloat distance = ABS(attributes.center.x - centerX);
//根据distance计算cell的缩放比例
CGFloat scale = 1 - (distance / self.collectionView.frame.size.width);
//设置缩放比例
attributes.transform3D = CATransform3DMakeScale(scale, scale, scale);
}
// 返回调整之后的布局属性数组
return attrbutesArray;
}
/
- 在重新刷新布局时调用
*/
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath {
UICollectionViewLayoutAttributes attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
return attr;
}
/
- collectionView停止滑动时调用,可以手动设置collectionView的偏移量
- proposedContentOffset collectionView原本的偏移量
*/
-
(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
//计算出最终显示的矩形框
CGRect endRect;
endRect.origin.x = proposedContentOffset.x;
endRect.origin.y = 0;
endRect.size = CGSizeMake(self.collectionView.frame.size.width, self.collectionView.frame.size.height);//获得所有cell的布局属性
NSArray *attributesArr = [super layoutAttributesForElementsInRect:endRect];//计算collectionView最中心点的x值
CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;
//循环所有的布局属性,得到距离中心点最近的cell到中心点的距离
CGFloat minDelta = MAXFLOAT;
for (UICollectionViewLayoutAttributes *attr in attributesArr) {
if(ABS(minDelta) > ABS(attr.center.x - centerX)) {
minDelta = attr.center.x - centerX;
}
}
//原来偏移量的x+距离中心点最近的cell到中心点的距离 将其设置为偏移量,该cell就会到中心点
proposedContentOffset.x += minDelta;
return proposedContentOffset;
}
效果图:
![效果图-3.gif](http:https://img.haomeiwen.com/i1914621/bc10257d76409d4d.gif?imageMogr2/auto-orient/strip)
###总结:
基本上到这里,UICollectionVew的使用就结束了,如何能够将UICollectionVew使用的更好,关键就在于怎样更好的运用UICollectionViewLayout和UICollectionViewFlowLayout,这才是UICollectionVew的精髓所在。
有兴趣的同学,可以移步到GitHub中下载[Demo](https://github.com/muzesunshine/UICollectionViewDemo),里面我为大家做了非常详细的注释。
网友评论