UICollectionView详解(二):自定义UICollectionViewLayout


UICollectionViewLayout简介

(1)基本方法

在UICollectionViewLayout时,我们主要会重写它的以下几个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)prepareLayout;

//该方法返回collectionView的内容的大小
- (CGSize)collectionViewContentSize;

//该方法会返回rect范围内所有cell的布局属性UICollectionViewLayoutAttributes
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;

//该方法返回对应indexPath下的cell的布局属性UICollectionViewLayoutAttributes
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;

//该方法返回在界面发生变化是是否要重新布局,返回YES则会重新布局
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;

//返回滑动后的collectonView的偏移量(滑动所停止的点),默认返回proposedContentOffset参数的值,在这里我们可以手动设置实际需要的偏移量
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset;

prepareLayout会在三个时机调用:

1、初始化layout的时候

2、刷新layout的时候

3、方法- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 返回YES的时候

(2)UICollectionViewLayout与UICollectionViewFlowLayout

​ 在此之前,我们先来简单的关注一下UICollectionViewFlowLayoutUICollectionViewLayout的关系: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所有的布局属性都是要写到该类中的,那它到底都有哪些属性呢:

1
2
3
4
5
6
7
8
9
10
@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的子类

1
2
3
#import <UIKit/UIKit.h>
@interface My_1Layout : UICollectionViewLayout
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#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 ++) {
NSIndexPath*indexPath=[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{
UICollectionViewLayoutAttributes*attributes=[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就可以了:

1
2
My_1Layout *layout = [[My_1Layout alloc]init];
UICollectionView* collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 64, kScreenWidth, kScreenHeight-64) collectionViewLayout:layout];

下面是效果图:

(注:在开发中,对于需要在外部的控制器中设置layout属性的,包括内边距、行间距、列间距、列数以及cell的初始大小等,可以为layout添加代理,使用代理方法返回)。

下面我们再看一个例子:
首先,我们先创建一个继承于UICollectionViewFlowLayout的layout子类,layout类中不做任何实现,然后在控制器中赋值给collectionView,控制器中关于collectionView和数据源的设置和上例一样,然后运行程序,查看效果:

​ 我们发现,尽管layout没有做任何布局,但是collectionView任然可以显示,这就说明,UICollectionViewFlowLayout已经为我们做好了一个布局,就是我们现在看到的流水布局,所以,对于继承于UICollectionViewFlowLayout的 类,如果要改变cell的布局,只需要获取系统默认为cell写好的布局,然后再此基础上进行修改就可以了。那么怎样获取UICollectionViewFlowLayout为我们写好的布局呢,使用父类调用 -(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:

废话不多说,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
*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;
}

效果图:

总结:

基本上到这里,UICollectionVew的使用就结束了,如何能够将UICollectionVew使用的更好,关键就在于怎样更好的运用UICollectionViewLayout和UICollectionViewFlowLayout,这才是UICollectionVew的精髓所在。