UICollectionView

学习一个东西,要先搞清楚三个问题:UICollectionView 是什么,UICollectionView 能干什么,如何使用 UICollectionView。

如果我们对于这三个问题都能回答的游刃有余了,基本就掌握这个东西了。

UICollectionView 是什么?

首先回顾一下 Collection View 的构成,我们能看到的有三个元素:

  1. Cells
  2. Supplementary Views 追加视图 (类似Header或者Footer)
  3. Decoration Views 装饰视图 (用作背景展示)

在其内部,我们找到了

1
2
3
@property (nonatomic, strong) UICollectionViewLayout *collectionViewLayout;
@property (nonatomic, weak, nullable) id <UICollectionViewDelegate> delegate;
@property (nonatomic, weak, nullable) id <UICollectionViewDataSource> dataSource;

可见其内部有提供数据的 UICollectionViewDataSource 以及处理用户交互的UICollectionViewDelegate 支持。另一方面,对于cell的样式和组织方式,由于collectionView 比 tableView 要复杂得多,因此没有按照类似于 tableView 的style 的方式来定义,而是专门使用了一个类来对 collectionView 的布局和行为进行描述,这就是 UICollectionViewLayout。collectionView 更像是一个复杂且灵活多变的 tableView。

UICollectionView 能干什么?

UICollectionView 可以搭建漂亮的布局页面,比如淘宝展示商品的 app:

这样的页面使用 UITableView 很难实现,但是使用 UICollectionView 是很容易搭建的。你可以自定义布局,让你的控件视图更加灵活。你可以用很少的代码实现非常炫酷的布局效果。

我们如何使用 UICollectionView

UICollectionView 的简单使用

先从最基础的开始:

首先,它是一个 UIView 控件,我们要想再屏幕上把它展示出来需要:

1
2
3
4
5
6
CGFloat collectionWH = self.view.frame.size.width;
CGRect frame = CGRectMake(0, 150, collectionWH, collectionWH);

UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame];

[self.view addSubview:collectionView];

当我们运行的时候会发现程序崩了,这是由于 UICollectionView 是用来进行屏幕布局的。我不能这样简简单单的创建出来。你会发现 alloc init 方法中,有一个initWithFrame:frame collectionViewLayout:; 方法,我们必须给 collectionView 传入一个 UICollectionViewLayout 对象,这个对象也叫布局对象。

目前来说,苹果官方给我们的能用的布局就一个:UICollectionViewFlowLayout,也叫流水布局。项目中,我们需要自定义布局满足自己项目的需要。

当我们使用下面代码创建 collectionView 时,运行已经可以看到 collectionView 了。如果我们想看到东西的话,还需要设置 collectionView 的数据源方法,这和 UITableView 比较类似。

1
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];

这里就不过多说明了,可以看代码的注释,我们运行下面代码:

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
@interface ViewController ()<UICollectionViewDataSource>

@end

@implementation ViewController

NSString * const AMCellId = @"AMCell";

- (void)viewDidLoad {
[super viewDidLoad];
CGFloat collectionWH = self.view.frame.size.width;
CGRect frame = CGRectMake(0, 150, collectionWH, collectionWH);

UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];

// 注册
[collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:AMCellId];

collectionView.dataSource = self;

[self.view addSubview:collectionView];
}

#pragma mark - UICollectionViewDataSource
// 告诉 collectionView 一共有多少个 cell
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 50;
}

// 告诉 collectionView 每个 cell 的内容
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:AMCellId forIndexPath:indexPath];

cell.backgroundColor = [UIColor greenColor];

return cell;
}

于是我们已经完成了一次 collectionView 的简单使用。会出现以下效果:

PS:所谓的流水布局,flow 的意思就是当一行装满了就会流到下一行吧,其实就是按顺序排列的九宫格布局。

掌握 UICollectionViewFlowLayout 类的使用

想要自定义布局,我们需要写一个类,这个类必须继承自 UICollectionViewLayout 或者 UICollectionViewFlowLayout。

我们先学习如何通过继承 UICollectionViewFlowLayout 类来做一些与流水效果类似的小例子,便于我们理解 UICollectionViewFlowLayout 是怎样做 collectionView 的布局的。

效果图如下:

首先,创建 AMLineLayout 文件并继承自 UICollectionViewFlowLayout。需要用自定义的布局。然后进行一些初始化操作,比如只允许水平滚动,cell 的 size 等等的初始化操作。

注意:

初始化操作不能像其他类的初始化一样写在 -init,中 UICollectionViewLayout 专门提供了我们初始化方法,而且这个代码一定要调用 super 的方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 用来做布局的初始化操作(不建议在init方法中进行布局的初始化操作)
*/
- (void)prepareLayout
{
[super prepareLayout];
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.itemSize = CGSizeMake(100, 100);
// 初始是需要一定的偏移量
CGFloat inset = self.collectionView.frame.size.width * 0.5 - self.itemSize.width * 0.5;
self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset);
}

实现滑动远离中心点的 cell 缩小,靠近中心点的 cell 放大,在中心点的 cell 最大。我们需要重写父类方法:

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

/**
* 当collectionView的显示范围发生改变的时候,是否需要重新刷新布局
* 一旦重新刷新布局,就会重新调用下面的方法:
* 1.prepareLayout
* 2.layoutAttributesForElementsInRect:方法
*/
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}


/**
* UICollectionViewLayoutAttributes *attrs;
* 1.一个cell对应一个UICollectionViewLayoutAttributes对象
* 2.UICollectionViewLayoutAttributes对象决定了cell的frame
*/
/**
* 这个方法的返回值是一个数组(数组里面存放着rect范围内所有元素的布局属性)
* 这个方法的返回值决定了rect范围内所有元素的排布(frame)
*/
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
// 计算collectionView最中心点的x值
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;

// 获取 super 计算好的布局属性
NSArray *array = [super layoutAttributesForElementsInRect:rect];

// 遍历属性,在原有的属性上进行微调
for (UICollectionViewLayoutAttributes *attrs in array) {
// 计算每个 cell 距离中心点的间距
CGFloat delta = ABS(attrs.center.x - centerX);

// 根据每个 cell 距离中心点的距离计算他们的 scale
CGFloat scale = 1 - delta / self.collectionView.frame.size.width;
// 设置缩放比例
attrs.transform = CGAffineTransformMakeScale(scale, scale);
}
return array;
}
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

/**
* 这个方法的返回值,就决定了collectionView停止滚动时的偏移量
* 参数 proposedContentOffset 不受外界干扰的情况下,本来应该滚动的偏移量
* velocity 表示滚动的速度
*/
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
// 计算出滚动完成后的 collectionView 的中心的 x 值
CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;

// 计算 cell 的矩形框
CGRect rect = CGRectMake(proposedContentOffset.x, 0, self.collectionView.frame.size.width, self.collectionView.frame.size.height);

// 获取 super 计算好的属性值
NSArray *array = [super layoutAttributesForElementsInRect:rect];

// 用于记录最小的 delta
CGFloat minDelta = MAXFLOAT;

// 遍历矩形框内的 cell 的中心值
for (UICollectionViewLayoutAttributes *attrs in array) {
if (ABS(minDelta) > ABS(attrs.center.x - centerX)) {
minDelta = attrs.center.x - centerX;
}
}

// 让最近的 cell 显示在中间
return CGPointMake(proposedContentOffset.x + minDelta, proposedContentOffset.y);
}

此时,你运行程序,就可以发现每个 cell 就像之前的图片那里的样子一样运动了。想要给 cell 添加图片的话,就和我们的 AMLineLayout 类无关了。AMLineLayout 只管布局,不管内容。

此时创建自定义的 UICollectionViewCell 然后设置 imageView 控件添加图片控件,然后在 dataSource 数据源方法中给图片控件赋值并返回我们自定义的 cell 即可,这里就不过多叙述了。具体可见线性布局

接下来,如果我们需求的布局不是流水布局,而是其他布局排布,要解决这样的问题,就需要我们自己来实现 UICollectionViewLayout 类中的很多方法了。它是一个抽象类,有很多抽象方法。下面就来详细看看我们应该如何使用它来自定义元素的排布。

掌握 UICollectionViewLayout 类的使用

UICollectionViewLayout 的功能为向 UICollectionView 提供布局信息,不仅包括 cell 的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义 layout 的常规做法是继承 UICollectionViewLayout 类,然后重载下列方法:

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

+ (Class)layoutAttributesClass; // override this method to provide a custom class to be used when instantiating instances of UICollectionViewLayoutAttributes
+ (Class)invalidationContextClass NS_AVAILABLE_IOS(7_0); // override this method to provide a custom class to be used for invalidation contexts

// 对于初始化只算一次的东西放在这个方法内。根据苹果 discussion 的说法,
// 会在初始化布局时调用一次,然后会在布局无效以后和请求布局信息时调用
// The collection view calls -prepareLayout again after layout is invalidated and before requerying the layout information.
// 这个方法必须调用 super 如果被重写。
- (void)prepareLayout;

// UICollectionView calls these four methods to determine the layout information.
// UICollectionView 调用下面4个方法来确定布局信息


/**
* 这个方法的返回值是一个数组(数组里面存放着rect范围内所有元素的布局属性)
* 这个布局属性可以是 cell 的,或者追加视图或装饰视图的,这是根据
* 创建 UICollectionViewLayoutAttributes 对象使用的方法决定的:
* layoutAttributesForCellWithIndexPath:
* layoutAttributesForSupplementaryViewOfKind:withIndexPath:
* layoutAttributesForDecorationViewOfKind:withIndexPath:
*
* 而每一个元素对应一个 UICollectionViewLayoutAttributes 对象
* 这个类后面有介绍
*/

- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;

/**
* 给定元素在数组中的位置,返回该位置的元素的布局信息:
* UICollectionViewLayoutAttributes 对象,一般配合上面的方法一起使用
*/
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;

/**
* 返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
*/
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;

/**
* 返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
*/
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath;

/**
* 当collectionView的显示范围发生改变的时候,是否需要重新刷新布局
* 一旦重新刷新布局,就会重新调用下面的方法:
* 1.prepareLayout
* 2.layoutAttributesForElementsInRect:方法
* 返回 No 则显示范围发生改变时不会随时调用以上方法,而是在改变后调用一次
* 如果 YES 则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
*/
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds; // return YES to cause the collection view to requery the layout for geometry information

- (UICollectionViewLayoutInvalidationContext *)invalidationContextForBoundsChange:(CGRect)newBounds NS_AVAILABLE_IOS(7_0);

- (BOOL)shouldInvalidateLayoutForPreferredLayoutAttributes:(UICollectionViewLayoutAttributes *)preferredAttributes withOriginalAttributes:(UICollectionViewLayoutAttributes *)originalAttributes NS_AVAILABLE_IOS(8_0);

- (UICollectionViewLayoutInvalidationContext *)invalidationContextForPreferredLayoutAttributes:(UICollectionViewLayoutAttributes *)preferredAttributes withOriginalAttributes:(UICollectionViewLayoutAttributes *)originalAttributes NS_AVAILABLE_IOS(8_0);


/**
* 这个方法的返回值,就决定了collectionView停止滚动时的偏移量
*/
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity; // return a point at which to rest after scrolling - for layouts that want snap-to-point scrolling behavior

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset NS_AVAILABLE_IOS(7_0); // a layout can return the content offset to be applied during transition or update animations

/**
* 这个方法确定你布局的内容尺寸,设置了以后才可以滚动。
*/
- (CGSize)collectionViewContentSize; // Subclasses must override this method and use it to return the width and height of the collection view’s content. These values represent the width and height of all the content, not just the content that is currently visible. The collection view uses this information to configure its own content size to facilitate scrolling.

另外需要了解的是,在初始化一个 UICollectionViewLayout 实例后,会有一系列准备方法被自动调用,以保证 layout 实例的正确。

首先,- (void)prepareLayout 将被调用,默认下该方法什么都不做,但是在自己的子类实现中,一般在该方法中设定一些必要的 layout 的结构和初始需要的参数等。

之后,- (CGSize) collectionViewContentSize 将被调用,以确定 collection 应该占据的尺寸。注意这里的尺寸不是指可视部分的尺寸,而应该是所有内容所占的尺寸。collectionView 的本质是一个 scrollView,因此需要这个尺寸来配置滚动行为。

接下来 - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 被调用,初始的 layout 的外观将由该方法返回的 UICollectionViewLayoutAttributes 来决定。

最后,在需要更新 layout 时,需要给当前 layout 发送 - invalidateLayout 消息。该消息会立即返回,并且预约在下一个 loop 的时候刷新当前 layout ,这一点和 UIView 的 setNeedsLayout 方法十分类似。在 - invalidateLayout 后的下一个 collectionView 的刷新 loop 中,又会从prepareLayout开始,依次再调用- collectionViewContentSize- layoutAttributesForElementsInRect 来生成更新后的布局。

UICollectionViewLayoutAttributes

UICollectionViewLayoutAttributes是一个非常重要的类,其 property 列表:

1
2
3
4
5
6
7
@property (nonatomic) CGRect frame
@property (nonatomic) CGPoint center
@property (nonatomic) CGSize size
@property (nonatomic) CATransform3D transform3D
@property (nonatomic) CGFloat alpha
@property (nonatomic) NSInteger zIndex
@property (nonatomic, getter=isHidden) BOOL hidden

可以看到,UICollectionViewLayoutAttributes 的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。当 UICollectionView 在获取布局时将针对每一个当前 indexPath 上的元素(包括cell,追加视图和装饰视图),向其 UICollectionViewLayout 实例询问该部件的布局信息,而这个布局信息就由每个元素对应的 UICollectionViewLayoutAttributes 类的实例对象给出。

Demo 瀑布流

自定义 UICollectionViewLayout ,然后重写下列4个方法就可以展示内容了。

详见代码:瀑布流框架

补充

如果我们想要删除、插入或者移动元素,删除、插入或者移动组需要用到 UICollectionView 的方法:

1
2
3
4
5
6
7
8
9
- (void)insertSections:(NSIndexSet *)sections;
- (void)deleteSections:(NSIndexSet *)sections;
- (void)reloadSections:(NSIndexSet *)sections;
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection;

- (void)insertItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)deleteItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)reloadItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath;

如果我们想更换布局,一行代码就可以实现,而且是动态实现:

1
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated; // transition from one layout to another

如果我们想在删除、插入或者移动元素时或者之后做一些事情,则需要使用这个方法:

1
- (void)performBatchUpdates:(void (^ __nullable)(void))updates completion:(void (^ __nullable)(BOOL finished))completion; // allows multiple insert/delete/reload/move calls to be animated simultaneously. Nestable.
文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2015/06/23/UICollectionView/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论