1 GitHub
博主已经写好了一份瀑布流的框架,如果你想直接使用,可以点击进入CFWaterFlowView的项目主页。
CFWaterFlowView是博主已经封装好的瀑布流框架,轻量级、简单、易用,希望你喜欢。
2 简介
瀑布流是一种非常常用的UI布局,可以为用户带来沉浸式的体验,不需要打断用户的阅读。非常适合带图片信息的展示。
3 思路与实现
3.1 选择什么作为基类
-> 首先你要想到这个能滚动的布局一定是通过UIScrollView实现的
-> 那么现在有三种选择
- 通过UITableView实现
- 通过UICollectionView实现
- 通过UIScrollView实现
-> 但是UITableView只能实现每行一个cell,而UICollectionView的每个cell的大小又是相同的
-> 那么最后选择通过最基础的UIScrollView实现瀑布流
3.2 如何提供更好的接口
一般来说,为了使你提供的API更易用,可以参考官方是如何构建自己的类的。
既然瀑布流是通过UIScrollView实现的,又类似于UITableView,有cell的概念,那么久可以参考UITableView的API
所以你需要:
- 定义瀑布流数据源协议
CFWaterFlowViewDataSource
- 定义瀑布流代理协议
CFWaterFlowViewDelegate
- 定义并实现realoadData方法
仿照UITableView做好数据源和代理协议
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
| #pragma mark - ========================代理定义======================= @protocol CFWaterFlowViewDelegate <UIScrollViewDelegate>
@optional
- (CGFloat)waterFlowView:(CFWaterFlowView *)waterFlowView heightAtIndex:(NSUInteger)index;
- (void)waterFlowView:(CFWaterFlowView *)waterFlowView didSelectCellAtIndex:(NSUInteger)index;
- (CGFloat)waterFlowView:(CFWaterFlowView *)waterFlowView marginForType:(CFWaterFlowViewMarginType)type;
@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
| #pragma mark - ========================数据源定义======================== @class CFWaterFlowView; @protocol CFWaterFlowViewDataSource <NSObject>
@required
- (NSUInteger)numberOfCellsInWaterFlowView:(CFWaterFlowView *)waterFlowView;
- (CFWaterFlowViewCell *)waterFlowView:(CFWaterFlowView *)waterFlowView cellAtIndex:(NSUInteger)index;
@optional
- (NSUInteger)numberOfColumnsInWaterFlowView:(CFWaterFlowView *)waterFlowView;
@end
|
需要注意的是数据源协议中比UITableViewDataSource多出一个方法
1 2 3 4 5 6 7 8 9 10 11
| @optional
- (NSUInteger)numberOfColumnsInWaterFlowView:(CFWaterFlowView *)waterFlowView;
@end
|
通过这个方法可以向数据源索取各种类型的间距,间距类型是一个枚举类型,定义如下:
1 2 3 4 5 6 7 8 9 10 11
| #pragma mark - ========================枚举定义======================== typedef enum { CFWaterFlowViewMarginTypeTop, CFWaterFlowViewMarginTypeBottom, CFWaterFlowViewMarginTypeLeft, CFWaterFlowViewMarginTypeRight, CFWaterFlowViewMarginTypeColumn, CFWaterFlowViewMarginTypeRow } CFWaterFlowViewMarginType;
|
定义reloadData方法和代理、数据源引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #pragma mark - ========================类定义======================= @interface CFWaterFlowView : UIScrollView
@property (nonatomic, weak) id<CFWaterFlowViewDataSource> dataSource;
@property (nonatomic, weak) id<CFWaterFlowViewDelegate> delegate;
- (void)reloadData;
@end
|
3.3 reloadData中应该做什么
reloadData中我们需要:
- 计算每一个cell的尺寸位置,并存放到一个数组
@property (nonatomic, strong) NSMutableArray *cellFrames;
中
- 设置contentSize使CFWaterFlowView能够滚动
为了计算每一个cell的frame需要获得:
- cell总数
- 瀑布流列数
- 各个类型的边距
- 每个cell的高度
因为代理方法并不是强制实现的,所以我们要设定几个默认值,避免在代理没有实现定义的方法时瀑布流能够正常显示:
1 2 3 4 5 6
| #define CFWaterFlowViewDefaultCellH 50
#define CFWaterFlowViewDefaultColumnsCount 3
#define CFWaterFlowViewDefaultMargin 10
|
3.4 reloadData的实现
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
| - (void)reloadData { NSUInteger cellsCount = [self.dataSource numberOfCellsInWaterFlowView:self]; NSUInteger columnsCount = [self numberOfColumns]; CGFloat marginTop = [self marginForType:CFWaterFlowViewMarginTypeTop]; CGFloat marginBottom = [self marginForType:CFWaterFlowViewMarginTypeBottom]; CGFloat marginLeft = [self marginForType:CFWaterFlowViewMarginTypeLeft]; CGFloat marginRight = [self marginForType:CFWaterFlowViewMarginTypeRight]; CGFloat marginRow = [self marginForType:CFWaterFlowViewMarginTypeRow]; CGFloat marginColumn = [self marginForType:CFWaterFlowViewMarginTypeColumn]; CGFloat cellW = (self.width - marginLeft - marginRight - (columnsCount - 1) * marginColumn) / columnsCount; CGFloat maxYOfColumns[columnsCount]; for (int i = 0; i < columnsCount; i++) { maxYOfColumns[i] = 0.0; } for (int i = 0; i < cellsCount; i++) { NSUInteger cellColumn = 0; NSUInteger maxYOfColumn = maxYOfColumns[cellColumn]; for (int j = 0; j < columnsCount; j++) { if (maxYOfColumns[j] < maxYOfColumn) { cellColumn = j; maxYOfColumn = maxYOfColumns[j]; } } CGFloat cellH = [self heightAtIndex:i]; CGFloat cellX = marginLeft + cellColumn * (cellW + marginColumn); CGFloat cellY = 0; if (maxYOfColumn == 0.0) { cellY = marginTop; } else { cellY = maxYOfColumn + marginRow; } CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH); [self.cellFrames addObject:[NSValue valueWithCGRect:cellFrame]]; maxYOfColumns[cellColumn] = CGRectGetMaxY(cellFrame); } CGFloat contentH = maxYOfColumns[0]; for (int i = 0; i < columnsCount; i++) { if (maxYOfColumns[i] > contentH) { contentH = maxYOfColumns[i]; } } contentH += marginBottom; self.contentSize = CGSizeMake(0, contentH); }
|
4 缓存池实现
4.1 内存浪费(1)
在上一步骤中,计算了每个cell的frame,但是并没有予以显示,那么如何显示呢?
最简单的方法当然是:计算好frame -> 新建一个CFWaterFlowViewCell -> 的frame -> 添加到CFWaterFlowView上
但这显然会造成性能问题:因为处在屏幕之外的cell不需要显示,如果过早创建cell对象,会造成大量的内存浪费
正确的这做法是:先判断cell是否在屏幕显示范围内
- 如果在:创建cell对象 -> 设置frame -> 显示到CFWaterFlowView上
- 如果不在:暂时不做操作
按照以上思想,可以开始实现显示cell。
那么在什么时候可以[self addSubview:cell]
呢?
-> 经过上面的分析reloadData中只应该计算每一个cell的frame,不能马上添加。
-> 进一步考虑UIScrollView特性,UIScrollView可以滚动,每次滚动都应该判断当前有哪些cell在屏幕范围内,并予以显示。所以可以考虑在UIScrollView滚动的时候判断、添加cell。
-> UIScrollView在滚动时会调用- (void)layoutSubviews
方法,所已在layoutSubviews
中对cell进行布局最合适。
4.2 解决内存浪费(1)代码实现:
- 首先编写一个方法,能够判断一个frame是否在当前屏幕显示范围内
1 2 3 4 5 6 7 8 9 10 11
|
- (BOOL)isInScreen:(CGRect)frame { return (CGRectGetMaxY(frame) > self.contentOffset.y) && (CGRectGetMinY(frame) < self.contentOffset.y + self.height); }
|
- 实现
layoutSubviews
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| - (void)layoutSubviews { NSUInteger cellsCount = self.cellFrames.count; for (int i = 0; i < cellsCount; i++) { CGRect cellFrame = [self.cellFrames[i] CGRectValue]; CFWaterFlowViewCell *cell = self.displayingCells[@(i)]; if ([self isInScreen:cellFrame]) { if (cell == nil) { cell = [self.dataSource waterFlowView:self cellAtIndex:i]; cell.frame = cellFrame; [self addSubview:cell]; } } else { if (cell != nil) { [cell removeFromSuperview]; } } } }
|
4.3 内存浪费(2)
上一步骤我们实现了按需求添加cell,但是layoutSubviews
会在CFWaterFlowView滚动时候不断的调用(哪怕只有微小的滚动),这样会导致cell被疯狂的重复添加。
-> 为了解决这一问题,我们需要保证在cell从未添加到CFWaterFlowView时才执行[self addSubview:cell]
-> 为此我们需要设立一个属性保存已经添加到CFWaterFlowView的cell,为了避免重复,使用一个Dictionary@property (nonatomic, strong) NSMutableDictionary *displayingCells;
来保存,key值为cell的索引(index)
-> 如果在添加前检索到已经有对应索引cell在Dictionary中,就不再重复添加
-> 更改layoutSubviews
代码
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
| - (void)layoutSubviews { NSUInteger cellsCount = self.cellFrames.count; for (int i = 0; i < cellsCount; i++) { CGRect cellFrame = [self.cellFrames[i] CGRectValue]; CFWaterFlowViewCell *cell = self.displayingCells[@(i)]; if ([self isInScreen:cellFrame]) { if (cell == nil) { cell = [self.dataSource waterFlowView:self cellAtIndex:i]; cell.frame = cellFrame; [self addSubview:cell]; self.displayingCells[@(i)] = cell; } } else { if (cell != nil) { [cell removeFromSuperview]; [self.displayingCells removeObjectForKey:@(i)]; } } } }
|
4.4 缓存池
按照一个步骤,能对性能做了一些优化,但仍然再存在问题:没有类似UITableView的cell缓存池功能,并提供接口让用户能够使用缓存池中的cell
`
对于缓存池,我们很容易想到用一个Set来作为缓存池。
建立属性@property (nonatomic, strong) NSMutableSet *reusableCells;
作为缓存池
仿照UITableView,定义并实现缓存池方法`- (CFWaterFlowViewCell )dequeueReusableCellWithIdentifier:(NSString )identifier;
在dequeueReusableCellWithIdentifier
方法中我们需要:
- 遍历缓存池,检索符合
(NSString *)identifier
的cell
- 如果检索到,从缓存池中移除该cell -> 返回该cell
- 如果没有检索到,返回nil
4.5 缓存池代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
- (CFWaterFlowViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier { __block CFWaterFlowViewCell *reusableCell = nil; [self.reusableCells enumerateObjectsUsingBlock:^(CFWaterFlowViewCell *cell, BOOL * stop) { if ([cell.identifier isEqualToString:identifier]) { reusableCell = cell; *stop = YES; } }]; if (reusableCell != nil) { [self.reusableCells removeObject:reusableCell]; } return reusableCell; }
|
5 反馈
以上已经完成了瀑布流框架的完整实现
如果你需要直接使用该框架,访问GitHub项目地址:https://github.com/summertian4/iOS-CFWaterFlowView
希望你能够喜欢本套框架以及博主这个萌萌哒大四女程序员^_^
如果有什么修改建议,可以发送邮件到lotty.lingyu@gmail.com,也欢迎到我的博客
有什么问题都可以在博文后面留言,或者微博上私信我。
博主是 iOS 妹子一枚。
希望大家一起进步。
我的微博:Lotty周小鱼