用UICollectionView实现瀑布流详解


UICollectionView简介

关于UICollectionView,苹果是这样解释的:管理数据项的有序集合,并使用可定制的布局呈现它们。在iOS中最简单的UICollectionView就是GirdView(网格视图),可以以多列的方式将数据进行展示。标准的UICollectionView包含以下3个部分,他们都是UIView的子类:

  • Cell:用于展示内容的主体,可以定制其尺寸和内容。
  • Supplementary view :用于追加视图,和UITableView里面的Header和Footer的作用类似。
  • Decoration View : 用于装饰视图,是每个section的背景.
    标准UICollectionView的构成

UICollectionView和UITableView对比

  • 相同点:
    • 都是继承自UIScrollView,支持滚动。
    • 都支持数据单元格的重用机制。
    • 都是通过代理方法和数据源方法来实现控制和显示。
  • 不同点:
    • UICollectionView的section里面的数据单元叫做item,UITableView的叫做cell
    • UICollectionView的布局使用UICollectionViewLayou或者其子类UICollectionViewFlowLayout和容易实现自定义布局。

实现一个简单的UICollectionView的步骤:

由于UICollectionView和UITableView类似,所以实现一个UICollectionView的步骤也和UITableView相同,最大的区别在与UICollectionView的布局。

  1. 创建布局

    • 使用UICollectionViewFlowLayout或者UICollectionViewLayou实现布局
    • 布局里面实现每个通过itemSize属性设置item的的尺寸。
    • 用scrollDirection属性设置item的滚动方向,垂直或者横向。

      1
      2
      3
      4
      typedef NS_ENUM(NSInteger, UICollectionViewScrollDirection) {
      UICollectionViewScrollDirectionVertical,
      UICollectionViewScrollDirectionHorizontal
      };
    • 其他自定义布局

  2. 创建UICollectionView

    • - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout方法进行初始化

      1
      UICollectionView * collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 100, collectionViewW, collectionViewH) collectionViewLayout:layout];
    • 设置UICollectionView的代理为控制器

  3. 实现代理协议
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
@protocol UICollectionViewDataSource <NSObject>
@required
/**
* 返回每个section里面的item的数量
*/
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;

/**
* 返回每个item的具体样式
*/
- ( UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;

@optional
/**
* 返回有多少个section
*/
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView;

/**
* 返回UICollectionReusableView
*/
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;

/**
* 设置某个Item是否可以移动
*/
- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath ;

/**
*移动item的使用调用的方法
*/
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath ;

- (nullable NSArray<NSString *> *)indexTitlesForCollectionView:(UICollectionView *)collectionView ;

- (NSIndexPath *)collectionView:(UICollectionView *)collectionView indexPathForIndexTitle:(NSString *)title atIndex:(NSInteger)index ;

@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
#import "ViewController.h"

static NSString * const cellID = @"cellID";

@interface ViewController ()<UICollectionViewDataSource>

@end

@implementation ViewController


- (void)viewDidLoad {
[super viewDidLoad];

// 创建布局
UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc]init];
layout.itemSize = CGSizeMake(50, 50);
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;

// 创建collectionView
CGFloat collectionViewW = self.view.frame.size.width;
CGFloat collectionViewH = 200;
UICollectionView * collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 100, collectionViewW, collectionViewH) collectionViewLayout:layout];
collectionView.backgroundColor = [UIColor blackColor];
collectionView.dataSource = self;
[self.view addSubview:collectionView];

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


#pragma mark - <UICollectionViewDataSource>
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return 50;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{

UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath];

// 设置圆角
cell.layer.cornerRadius = 5.0;
cell.layer.masksToBounds = YES;
cell.backgroundColor = [UIColor redColor];


return cell;
}

@end

实现效果

可以水平滚动的uicollectionView

如果将layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;改为layout.scrollDirection = UICollectionViewScrollDirectionVertical;就能实现垂直滚动
可以垂直滚动的uicollectionView]

UICollectionViewDelegate

同样的UICollectionView也有代理方法,在实现代理协议之后通过代理方法来实现和用户的交互操作。具体来说主要负责一以下三分工作:

  • cell的高亮效果显示
1
2
3
- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath;
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath;
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath;
  • cell的选中状态
1
2
3
- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath; // called when the user taps on an already-selected item in multi-select mode
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath;
  • 支持长按后的菜单
1
2
3
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath;
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender;
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender;

UICollectionViewLayout和UICollectionViewFlowLayout

UICollectionView的精髓就是UICollectionViewLayout,这也是UICollectionView和UITableView最大的不同。UICollectionViewLayout决定了UICollectionView是如何显示在界面上的。在展示之间,一般需要生成合适的UICollectionViewLayout的子类对象,并将其赋值到UICollectionView的布局属性上。
UICollectionViewFlowLayout是UICollectionViewLayout的子类。这个布局是最简单最常用的。它实现了直线对其的布局排布方式,Gird View就是用UICollectionViewFlowLayout布局方式。

UICollectionViewLayout布局的具体思路:

  • 设置itemSzie属性,它定义了每一个item的大小。在一个示例中通过设置layout的itemSize属性全局的设置了cell的尺寸。如果想要对某个cell定制尺寸,可以使用- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath方法实现
  • 设置间隔
    间隔可以指定item之间的间隔和每一行之间的间隔。间隔和itemSzie一样,既有全局属性,也可以对每一个item设定:
1
2
3
4
@property (nonatomic) CGFloat minimumLineSpacing;
@property (nonatomic) CGFloat minimumInteritemSpacing;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
  • 设定滚动方向
1
2
3
4
typedef NS_ENUM(NSInteger, UICollectionViewScrollDirection) {
UICollectionViewScrollDirectionVertical,
UICollectionViewScrollDirectionHorizontal
};
  • 设置Header和Footer的尺寸
    设置Header和Footer的尺寸也分为全局和局部。在这里需要注意滚动的方向,滚动方向不同,header和footer的宽度和高度只有一个会起作用。垂直滚动时section间宽度为尺寸的高。
1
2
3
4
@property (nonatomic) CGSize headerReferenceSize;
@property (nonatomic) CGSize footerReferenceSize;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section;
  • 设置内边距
1
2
@property (nonatomic) UIEdgeInsets sectionInset;
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;

用UICollectionView实现瀑布流

瀑布流很常用,尤其是在电商类app中用于展示商品信息,比如某宝:

实现瀑布流的方式有几种,但是比较简单的是通过UICollectionView,因为collectionView自己会实现cell的循环利用,所以自己不用实现循环利用的机制。瀑布就最重要的就是布局,要选取最短的那一列来排布,保证每一列之间的间距不会太大。

实现步骤

  • 自定义继承自UICollectionViewLayout的子类来进行实现布局
    • 调用- (void)prepareLayout进行初始化
    • 重载- (CGSize)collectionViewContentSize返回内容的大小
    • 重载- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect方法返回rect中所有元素的布局属性,返回的是一个数组
    • 重载- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath方法返回对应的indexPath的位置的cell的布局属性。
    • 重载- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;方法返回对应indexPath的位置的追加视图的布局属性,如果没有就不用重载
    • 重载- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath;方法返回对应indexPath的位置的装饰视图的布局属性,如果没有也不需要重载
    • 重载- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;当边界发生改变时,是否应该刷新。

####自定义UICollectionViewLayout布局的示例代码
用代理来实现对item的布局属性的控制
.h文件

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
#import <UIKit/UIKit.h>

@class LMHWaterFallLayout;

@protocol LMHWaterFallLayoutDeleaget<NSObject>

@required
/**
* 每个item的高度
*/
- (CGFloat)waterFallLayout:(LMHWaterFallLayout *)waterFallLayout heightForItemAtIndexPath:(NSUInteger)indexPath itemWidth:(CGFloat)itemWidth;

@optional
/**
* 有多少列
*/
- (NSUInteger)columnCountInWaterFallLayout:(LMHWaterFallLayout *)waterFallLayout;

/**
* 每列之间的间距
*/
- (CGFloat)columnMarginInWaterFallLayout:(LMHWaterFallLayout *)waterFallLayout;

/**
* 每行之间的间距
*/
- (CGFloat)rowMarginInWaterFallLayout:(LMHWaterFallLayout *)waterFallLayout;

/**
* 每个item的内边距
*/
- (UIEdgeInsets)edgeInsetdInWaterFallLayout:(LMHWaterFallLayout *)waterFallLayout;


@end

@interface LMHWaterFallLayout : UICollectionViewLayout
/** 代理 */
@property (nonatomic, weak) id<LMHWaterFallLayoutDeleaget> delegate;

@end

.m文件

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#import "LMHWaterFallLayout.h"

/** 默认的列数 */
static const CGFloat LMHDefaultColunmCount = 3;
/** 每一列之间的间距 */
static const CGFloat LMHDefaultColunmMargin = 10;

/** 每一行之间的间距 */
static const CGFloat LMHDefaultRowMargin = 10;

/** 内边距 */
static const UIEdgeInsets LMHDefaultEdgeInsets = {10,10,10,10};


@interface LMHWaterFallLayout()
/** 存放所有的布局属性 */
@property (nonatomic, strong) NSMutableArray * attrsArr;
/** 存放所有列的当前高度 */
@property (nonatomic, strong) NSMutableArray *columnHeights;
/** 内容的高度 */
@property (nonatomic, assign) CGFloat contentHeight;

- (NSUInteger)colunmCount;
- (CGFloat)columnMargin;
- (CGFloat)rowMargin;
- (UIEdgeInsets)edgeInsets;

@end

@implementation LMHWaterFallLayout



#pragma mark 懒加载
- (NSMutableArray *)attrsArr{
if (!_attrsArr) {
_attrsArr = [NSMutableArray array];
}

return _attrsArr;
}

- (NSMutableArray *)columnHeights{
if (!_columnHeights) {
_columnHeights = [NSMutableArray array];
}

return _columnHeights;
}

#pragma mark - 数据处理
/**
* 列数
*/
- (NSUInteger)colunmCount{

if ([self.delegate respondsToSelector:@selector(columnCountInWaterFallLayout:)]) {
return [self.delegate columnCountInWaterFallLayout:self];
}else{
return LMHDefaultColunmCount;
}
}

/**
* 列间距
*/
- (CGFloat)columnMargin{
if ([self.delegate respondsToSelector:@selector(columnMarginInWaterFallLayout:)]) {
return [self.delegate columnMarginInWaterFallLayout:self];
}else{
return LMHDefaultColunmMargin;
}
}

/**
* 行间距
*/
- (CGFloat)rowMargin{
if ([self.delegate respondsToSelector:@selector(rowMarginInWaterFallLayout:)]) {
return [self.delegate rowMarginInWaterFallLayout:self];
}else{
return LMHDefaultRowMargin;
}
}

/**
* item的内边距
*/
- (UIEdgeInsets)edgeInsets{
if ([self.delegate respondsToSelector:@selector(edgeInsetdInWaterFallLayout:)]) {
return [self.delegate edgeInsetdInWaterFallLayout:self];
}else{
return LMHDefaultEdgeInsets;
}
}



/**
* 初始化
*/
- (void)prepareLayout{

[super prepareLayout];

self.contentHeight = 0;

// 清除之前计算的所有高度
[self.columnHeights removeAllObjects];

// 设置每一列默认的高度
for (NSInteger i = 0; i < LMHDefaultColunmCount ; i ++) {
[self.columnHeights addObject:@(LMHDefaultEdgeInsets.top)];
}


// 清楚之前所有的布局属性
[self.attrsArr removeAllObjects];

// 开始创建每一个cell对应的布局属性
NSInteger count = [self.collectionView numberOfItemsInSection:0];

for (int i = 0; i < count; i++) {

// 创建位置
NSIndexPath * indexPath = [NSIndexPath indexPathForItem:i inSection:0];

// 获取indexPath位置上cell对应的布局属性
UICollectionViewLayoutAttributes * attrs = [self layoutAttributesForItemAtIndexPath:indexPath];

[self.attrsArr addObject:attrs];
}

}


/**
* 返回indexPath位置cell对应的布局属性
*/
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{

// 创建布局属性
UICollectionViewLayoutAttributes * attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

//collectionView的宽度
CGFloat collectionViewW = self.collectionView.frame.size.width;

// 设置布局属性的frame

CGFloat cellW = (collectionViewW - self.edgeInsets.left - self.edgeInsets.right - (self.colunmCount - 1) * self.columnMargin) / self.colunmCount;
CGFloat cellH = [self.delegate waterFallLayout:self heightForItemAtIndexPath:indexPath.item itemWidth:cellW];


// 找出最短的那一列
NSInteger destColumn = 0;
CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];

for (int i = 1; i < LMHDefaultColunmCount; i++) {

// 取得第i列的高度
CGFloat columnHeight = [self.columnHeights[i] doubleValue];

if (minColumnHeight > columnHeight) {
minColumnHeight = columnHeight;
destColumn = i;
}
}

CGFloat cellX = self.edgeInsets.left + destColumn * (cellW + self.columnMargin);
CGFloat cellY = minColumnHeight;
if (cellY != self.edgeInsets.top) {

cellY += self.rowMargin;
}

attrs.frame = CGRectMake(cellX, cellY, cellW, cellH);

// 更新最短那一列的高度
self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));

// 记录内容的高度 - 即最长那一列的高度
CGFloat maxColumnHeight = [self.columnHeights[destColumn] doubleValue];
if (self.contentHeight < maxColumnHeight) {
self.contentHeight = maxColumnHeight;
}

return attrs;
}

/**
* 决定cell的布局属性
*/
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{

return self.attrsArr;
}

/**
* 内容的高度
*/
- (CGSize)collectionViewContentSize{

// CGFloat maxColumnHeight = [self.columnHeights[0] doubleValue];
// for (int i = 0; i < LMHDefaultColunmCount; i++) {
//
// // 取得第i列的高度
// CGFloat columnHeight = [self.columnHeights[i] doubleValue];
//
// if (maxColumnHeight < columnHeight) {
// maxColumnHeight = columnHeight;
// }
//
// }

return CGSizeMake(0, self.contentHeight + self.edgeInsets.bottom);
}
@end

示例demo

接下来在控制器里面就只需要按照第一个示例的步骤,创建布局,创建collectionView。在这里,瀑布流中每个cell的图片和尺寸是后台传过来的,所以只需在布局的代理方法里面将这些数据传入,就可以实现一个简单的瀑布流了。
实现效果

附上代码下载链接:用UICollectionView实现瀑布