瀑布流可以在保证图片原始比例的情况下,灵活的展现内容,相对于传统的使用相同大小的网格展现大量图片,要好上很多,而实现瀑布流的方式有很多种,网上比较流行的有三种实现方式。
1,使用UIScrollView(也就是本文所要写的),主要技术点在于视图的重用。(偏难)
2,使用UITableView,这种方式应该是最易想到的,因为需要展现几列就用几个tabelview就ok了,而且不需要考虑重用,应为苹果已经做好了,只需要考虑如何在几列tabelView滑动的时候,保持同步不出现BUG。
3,使用UICollectionView,UICollectionView在iOS6中第一次被介绍,它与UITableView有许多相似点,但它多了一个布局类,而实现瀑布流,就与这个布局类有关。此种方式实现,也不需要考虑视图重用。(最简单)
参考链接:collection实现瀑布流
1、首先创建一个自定义View叫DSWaterFlowView,继承自UIScrollView(因为瀑布流必须滚动),然后自定义cell叫DSWaterFlowViewCell,继承自UIView
2.分别写出dataSource协议和delegate协议中的接口
#pragma mark - 数据源方法
@protocol DSWaterFlowViewDataSource <NSObject>
@required
/** * cell的总数 */
- (NSInteger)numberOfcellsInWaterFlowView:(DSWaterFlowView *)waterFlowView;
/** * index所对应的cell */
- (DSWaterFlowViewCell *)waterFlowView:(DSWaterFlowView *)waterFlowView cellAtindex:(NSInteger)index;
@optional
/** * 有多少列 */
- (NSInteger)numberOfColumnsInWaterFlowView:(DSWaterFlowView *)waterFlowView;
@end
#pragma mark - 代理方法
@protocol DSWaterFlowViewDelegate <UIScrollViewDelegate>
@optional
/** * index所对应cell的行高 */
- (CGFloat)waterFlowView:(DSWaterFlowView *)waterFlowView heigthAtIndex:(NSInteger)index;
/** * 选中index位置的cell */
- (void)waterFlowView:(DSWaterFlowView *)waterFlowView didSelectAtIndex:(NSInteger)index;
/** * 间距 */
- (CGFloat)waterFlowView:(DSWaterFlowView *)waterFlowView marginType:(DSWaterFlowViewMarginType)type;
@end
在@interface里面写下delegate和datasource
/** * 数据源 */
@property (nonatomic,weak) id<DSWaterFlowViewDataSource> datasource;
/** * 代理 */
@property (nonatomic,weak) id<DSWaterFlowViewDelegate> delegate;
注意:
1>间距可以用枚举DSWaterFlowViewMarginType
typedef enum {
DSWaterFlowViewMarginTypeTop,
DSWaterFlowViewMarginTypeLeft,
DSWaterFlowViewMarginTypeBottom,
DSWaterFlowViewMarginTypeRight,
DSWaterFlowViewMarginTypeColumn, // 列间距
DSWaterFlowViewMarginTypeRow, // 行间距
} DSWaterFlowViewMarginType;
2>代理协议应该继承自UIScrollViewDelegate(不然会出现警告),因为scrollView里面也有一个代理,所以瀑布流的delegate也应该同时遵守DSWaterFlowViewDelegate和UIScrollViewDelegate
根据tableView的原理,cell的显示是通过reloadData方法去通知代理和数据源,所以我们可以在.h文件也写出reloadData方法来刷新数据
/** * 刷新数据 */
- (void)reloadData;
注意:当控制器实现了数据源和代理方法后,第一次就应该调用一次reloadData方法,可以在DSWaterFlowView的.m文件中实现willMoveToSuperview方法
/** * 当DSWaterFlowView进入即将进入父控件的时候就刷新数据(第一次) */
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[self reloadData];
}
接下来就应该实现reloadData方法,计算每个cell的位置
/** * 刷新数据 * 计算每一个cell的位置 */
- (void)reloadData
{
/** cell的总数 */
NSInteger numberOfCells = [self.datasource numberOfcellsInWaterFlowView:self];
/** 总列数 */
NSInteger numberOfColumns = [self numberOfColumns];
/** 间距 */
CGFloat topM = [self marginForType:(DSWaterFlowViewMarginTypeTop)];
CGFloat bottomM = [self marginForType:(DSWaterFlowViewMarginTypeBottom)];
CGFloat leftM = [self marginForType:(DSWaterFlowViewMarginTypeLeft)];
CGFloat rightM = [self marginForType:(DSWaterFlowViewMarginTypeRight)];
CGFloat columnM = [self marginForType:(DSWaterFlowViewMarginTypeColumn)];
CGFloat rowM = [self marginForType:(DSWaterFlowViewMarginTypeRow)];
/** cell的宽度 */
CGFloat cellW = (self.width - leftM - rightM - (numberOfColumns - 1) * columnM) / numberOfColumns;
/** 所有列最大Y值的数组 */
CGFloat maxOfColumns[numberOfColumns];
for (int i = 0; i < numberOfColumns; i++) {
maxOfColumns[i] = 0.0;
}
// 计算所有cell的frame
for (int i = 0; i < numberOfCells; i++) {
/** 最小Y值cell的列数(最短的一列) */
NSInteger cellColumn = 0;
/** cell所处那列的最大Y值(最短那一列的最大Y值) */
CGFloat maxOfCellColumn = maxOfColumns[cellColumn];
// 求出最短的一列
for (int j = 1; j < numberOfColumns; j++) {
if (maxOfColumns[j] < maxOfCellColumn) {
cellColumn = j;
maxOfCellColumn = maxOfColumns[j];
}
}
// 询问代理位置的高度
CGFloat cellH = [self heightAtidnex:i];
// cell的位置
CGFloat cellX = leftM + cellColumn * (cellW + columnM);
CGFloat cellY = 0;
if (maxOfCellColumn == 0.0) { // 首行
cellY = topM;
}else {
cellY = maxOfCellColumn + rowM;
}
// 添加frame到数组中
CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH);
[self.cellFrames addObject:[NSValue valueWithCGRect:cellFrame]];
// 更新最短那一列的Y值
maxOfColumns[cellColumn] = CGRectGetMaxY(cellFrame);
// 整个瀑布流的高度
CGFloat contentH = maxOfColumns[0];
for (int i = 1; i < numberOfColumns; i++) {
if (maxOfColumns[i] > contentH) {
contentH = maxOfColumns[i];
}
}
contentH += bottomM;
// 设置scrollView可滚动的范围
self.contentSize = CGSizeMake(0, contentH);
}
}
#pragma mark - 私有方法
/** * 获取间距 */
- (CGFloat)marginForType:(DSWaterFlowViewMarginType)type
{
if ([self.delegate respondsToSelector:@selector(waterFlowView:marginType:)]) {
return [self.delegate waterFlowView:self marginType:type];
}else {
return DSWaterFlowViewDefaultMargin;
}
}
/** * 返回多少列 */
- (NSInteger)numberOfColumns
{
if ([self.datasource respondsToSelector:@selector(numberOfColumnsInWaterFlowView:)]) {
return [self.datasource numberOfColumnsInWaterFlowView:self];
}else {
return DSWaterFlowViewDefaultNumberOfColumns;
}
}
/** * 返回index对应的cell的高度 */
- (CGFloat)heightAtidnex:(NSInteger)index
{
if ([self.delegate respondsToSelector:@selector(waterFlowView:heigthAtIndex:)]) {
return [self.delegate waterFlowView:self heigthAtIndex:index];
}else {
return DSWaterFlowViewDefaultCellH;
}
}
在私有扩展里面添加cellFrames属性
/** * 所有cell的frame */
@property (nonatomic, strong) NSMutableArray *cellFrames;
并且设置默认的参数(列数,间距,高度等)
/** 默认cell的高度 */
#define DSWaterFlowViewDefaultCellH 70
/** 默认间距 */
#define DSWaterFlowViewDefaultMargin 10
/** 默认的列数 */
#define DSWaterFlowViewDefaultNumberOfColumns 3
这个时候瀑布流的基本框架差不多搭建完成了,但是有两点瑕疵:
1>不具备像tableview那样的重用机制
2>不应该在reloadData方法向数据源索要cell,而是应该像tableview一样,在滚动的时候来索要cell
/** * 当UIScrollView滚动的时候也会调用这个方法 */
- (void)layoutSubviews
{
[super layoutSubviews];
// 向数据源索要对应位置的cell
NSUInteger numberOfCells = self.cellFrames.count;
for (int i = 0; i < numberOfCells; i++) {
// 取出i位置的cell
CGRect cellFrame = [self.cellFrames[i] CGRectValue];
// 优先从字典中取
DSWaterFlowViewCell *cell = self.displayingCells[@(i)];
// 判断i位置对应的frame在不在屏幕上(能否看见)
if ([self isInScreen:cellFrame]) { // 在屏幕上
if (cell == nil) {
// 向数据源索要cell,并添加进来
cell = [self.datasource waterFlowView:self cellAtindex:i];
cell.frame = cellFrame;
[self addSubview:cell];
// 存放到字典中
self.displayingCells[@(i)] = cell;
}
}else { // 不在屏幕上
if (cell) {
// 从瀑布流和字典中删除掉
[cell removeFromSuperview];
[self.displayingCells removeObjectForKey:@(i)];
// 存进缓存池
[self.reusableCells addObject:cell];
}
}
}
}
/** * 根据缓存池标识从缓存池中取出可循环利用的cell */
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
// 从缓存池中取出可重用cell
__block DSWaterFlowViewCell *reusbaleCell = [self.reusableCells anyObject];
// 遍历缓存池
[self.reusableCells enumerateObjectsUsingBlock:^(DSWaterFlowViewCell *cell, BOOL * _Nonnull stop) {
if ([reusbaleCell.identifier isEqualToString:cell.identifier]) {
reusbaleCell = cell;
*stop = YES;
}
}];
if (reusbaleCell) { // 从缓存池中移除cell
[self.reusableCells removeObject:reusbaleCell];
}
// 返回可重用cell
return reusbaleCell;
}
/** * 判断一个frame有没有在屏幕上 */
- (BOOL)isInScreen:(CGRect)frame
{
return (CGRectGetMaxY(frame) > self.contentOffset.y) && (CGRectGetMinY(frame) < self.contentOffset.y + self.height);
}
私有扩展添加如下属性:
/** * 正在展示的cell */
@property (nonatomic, strong) NSMutableDictionary *displayingCells;
/** * 缓存池是随机的,随机从缓存池去数据(用set,存放离开屏幕的cell) */
@property (nonatomic, strong) NSMutableSet *reusableCells;
注意点:
1>当UIScrollView滚动的时候就会调用layoutSubviews方法,因此可以在这个方法中向数据源索要cell
2>displayingCells属性用来保存目前显示在屏幕上的cell(用字典是因为一个位置可以对应一个cell),判断当前位置上的cell在不在屏幕,可以防止出现在原地小范围滚动也会不停的向数据源索要cell并且重复添加进DSWaterFlowView
3>reusableCells属性就是缓存池(用NSSet是因为从缓存池中取cell是随机的),用来装离开屏幕的cell
4>提供dequeueReusableCellWithIdentifier给外面,让其从缓存池中取cell,所以应该在.h文件提供该方法
/** * 根据缓存池标识从缓存池中取出可循环利用的cell */
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;
同时为DSWaterFlowViewCell提供identifier属性
@property (nonatomic, copy) NSString *identifier;
对于如何用这个控件,tableView怎么用,这个就怎么用!
1>将该控件添加到控制器View上,设置代理和数据源,通知遵守协议
// 创建瀑布流控件
DSWaterFlowView *waterFlowView = [[DSWaterFlowView alloc] init];
waterFlowView.frame = self.view.bounds;
waterFlowView.delegate = self;
waterFlowView.datasource = self;
[self.view addSubview:waterFlowView];
2>然后实现对应的方法就OK了
好了,文章到这里,整个瀑布流控件就完成了!