瀑布流的原理与实现
-.什么是瀑布流?
瀑布流视图与UITableView类似,但是相对复杂一点.UITableView只有一列,可以有多个小节(section),每一个小节(section)可以有多行(row).
瀑布流呢,可以有多列,每一个item(单元格)的高度可以不相同,但是宽度必须一样.排列的方式是,从左往右排列,哪一列现在的总高度最小,就优先排序把item(单元格)放在这一列.这样排完所有的单元格后,可以保证每一列的总高度都相差不大,不至于,有的列很矮,有的列很高.这样就很难看了.
上面的数字,就是每个单元格的序号,可以看到item的排列顺序是个什么情况.
二.怎么实现一个瀑布流呢?
仿照UITableView的设计,我们要知道有多少个单元格,我们得问我们的数据源.有几列,问数据源.在某一个序号上是怎样的cell,问数据源.
某一个序号单元格的高度,问代理.单元格的列边距,行边距,整体的瀑布流视图的上下左右距离瀑布流视图所在的父视图的边距.这些,都问代理.
同时,我们也要在接口处对外提供方法,reloadData,当瀑布流视图要更新的时候可以调用.我们还要对外提供方法cellWidth,让外界可以直接知道每个单元格的高度是怎样的.同时,我们也要提供一个类似于UITableView的用来从缓存池取cell的方法.不然的话,屏幕每滑动到新的单元格地方,就要重新新建一个cell,这样当瀑布流总单元格多了之后,有多少单元格需要显示就创建多少次,这样是相当消耗性能的.所以要有缓存池,让外界在取cell时优先从缓存池取,缓存池取不到了,再来新建一个cell也不迟.UITableView就是这么做的.我们也要这么做.所以对外提供一个方法 -(WaterfallsViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;
仿照UITableView每个单元格是一种UITableViewCell,我们也可以定义一个瀑布流cell,继承于UIView即可,后期调用时可以随意定制cell的内容.
因为要实现的瀑布流,需要上下滚动,实际上是一个UIScrollView.所以,直接继承于UIScrollView.
所以有如下的接口定义
1.这个是瀑布流视图 WaterfallsView
// Copyright © 2015年 penglang. All rights reserved.
//
#import <UIKit/UIKit.h>
@class WaterfallsView,WaterfallsViewCell;
typedef enum {
WaterfallsViewMarginTypeTop,
WaterfallsViewMarginTypeLeft,
WaterfallsViewMarginTypeBottom,
WaterfallsViewMarginTypeRight,
WaterfallsViewMarginTypeColumn,
WaterfallsViewMarginTypeRow
}WaterfallsViewMarginType;
@protocol WaterfallsViewDataSource <NSObject>
@required
//- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
/**
* 有多少个cell
*/
-(NSUInteger)numberOfCells;
/**
* 在某一个序号的cell
*/
-(WaterfallsViewCell *)waterfallsView:(WaterfallsView *)waterfallsView cellAtIndex:(NSUInteger)index;
@optional
/**
* 有多少列
*/
-(NSUInteger)numberOfColumns;
@end
@protocol WaterfallsViewDelegate <UIScrollViewDelegate>
@optional
/**
* 某一个序号的单元格的高度
*/
-(CGFloat)waterfallsView:(WaterfallsView *)waterfallsView heightAtIndex:(NSUInteger)index;
/**
* 单元格与瀑布流视图的边界
*/
-(CGFloat)waterfallsView:(WaterfallsView *)waterfallsView margins:(WaterfallsViewMarginType)marginType;
/**
* 点击了某一个序号的单元格,怎么处理
*/
-(void)waterfallsView:(WaterfallsView *)waterfallsView didSelectCellAtIndex:(NSUInteger)index;
@end
@interface WaterfallsView : UIScrollView
@property (nonatomic, assign) id<WaterfallsViewDataSource> dataSource;
@property (nonatomic, assign) id<WaterfallsViewDelegate> delegate;
-(void)reloadData;
-(WaterfallsViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;
-(CGFloat)cellWidth;
@end
//这个是瀑布流视图的单元格视图 WaterfallsViewCell
// Copyright © 2015年 penglang. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface WaterfallsViewCell : UIView
@property (nonatomic,copy, readonly) NSString *reuseIdentifier;
-(instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier;
@end
//cell的定义与实现
// WaterfallsViewCell.h
// Copyright © 2015年 penglang. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface WaterfallsViewCell : UIView
@property (nonatomic,copy, readonly) NSString *reuseIdentifier;
-(instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier;
@end
// Copyright © 2015年 penglang. All rights reserved.
//
#import "WaterfallsViewCell.h"
//static NSUInteger count = 0;
@interface WaterfallsViewCell ()
@property (nonatomic,copy, readwrite) NSString *reuseIdentifier;
@end
// WaterfallsViewCell.m
@implementation WaterfallsViewCell
-(instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier{
if (self = [super init]) {
self.reuseIdentifier = reuseIdentifier;
}
//NSLog(@"创建cell%lu",(unsigned long)++count);
return self;
}
@end
然后,我们在控制器中就可以看该怎么使用定义的数据源方法
#import "ViewController.h"
#import "WaterfallsView.h"
#import "WaterfallsViewCell.h"
#define MyColor(r,g,b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1.0]
#define MyColorA(r,g,b,a) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:a]
@interface ViewController ()<WaterfallsViewDataSource,WaterfallsViewDelegate>
@property (nonatomic, weak) WaterfallsView *waterfallsView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//初始化瀑布流
WaterfallsView *waterfallsView = [[WaterfallsView alloc] initWithFrame:self.view.bounds];
waterfallsView.dataSource = self;
waterfallsView.delegate = self;
[self.view addSubview:waterfallsView];
_waterfallsView = waterfallsView;
}
#pragma mark - WaterfallsViewDataSource 数据源方法实现
-(NSUInteger)numberOfCells{
return 16;
}
-(NSUInteger)numberOfColumns{
return 3;
}
-(WaterfallsViewCell *)waterfallsView:(WaterfallsView *)waterfallsView cellAtIndex:(NSUInteger)index{
static NSString *ID = @"waterfallsCell";
WaterfallsViewCell *cell = [waterfallsView dequeueReusableCellWithIdentifier:ID];
if (cell == nil) {
cell = [[WaterfallsViewCell alloc] initWithReuseIdentifier:ID];
cell.backgroundColor = MyColor(arc4random_uniform(256), arc4random_uniform(256), arc4random_uniform(256));
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10, 10, 100, 20)];
label.textColor = [UIColor whiteColor];
label.tag = 10;
[cell addSubview:label];
}
UILabel *label = (UILabel *)[cell viewWithTag:10];
label.text = [NSString stringWithFormat:@"%lu",(unsigned long)index];
return cell;
}
#pragma mark - WaterfallsViewDelegate 代理方法实现
-(CGFloat)waterfallsView:(WaterfallsView *)waterfallsView heightAtIndex:(NSUInteger)index{
switch (index%3) {
case 0:
return 70.0;
break;
case 1:
return 90.0;
break;
case 2:
return 120.0;
break;
default:
return 150.0;
break;
}
}
-(CGFloat)waterfallsView:(WaterfallsView *)waterfallsView margins:(WaterfallsViewMarginType)marginType{
switch (marginType) {
case WaterfallsViewMarginTypeTop:
return 30;
case WaterfallsViewMarginTypeLeft:
case WaterfallsViewMarginTypeBottom:
case WaterfallsViewMarginTypeRight:
return 10;
break;
default:
return 5;
break;
}
}
-(void)waterfallsView:(WaterfallsView *)waterfallsView didSelectCellAtIndex:(NSUInteger)index{
NSLog(@"点击了第%lu个cell",(unsigned long)index);
}
@end
在控制器中这样用当然写代码很舒服了.可是我们只是对外提供了这些方法,很好用,而我们并没有实现它.
现在就来实现它的方法.
核心需要实现的方法其实就是
-(void)reloadData
在调用reloadData之时,需要重新将cell在瀑布流上显示出来,我们要确定每个单元格的位置,在相应的位置显示相应的我们设置好的单元格.
所以,我们要知道瀑布流视图的上\左\下\右边距各是多少,这些可以问数据源方法,我们可以在瀑布流视图实现里面给一个默认的间距.如果控制器没有实现边距数据源方法,就用我们默认设置的边距.
所以写一个辅助的方法,供内部调用
-(CGFloat)marginForType:(WaterfallsViewMarginType)type{
CGFloat margin = 0;
if ([self.delegate respondsToSelector:@selector(waterfallsView:margins:)]) {
margin = [self.delegate waterfallsView:self margins:type];
}else{
margin = WaterfallsViewDefaultMargin;
}
return margin;
}
这样就可以知道各种间距了.
同时,要知道总共有多少个cell,问数据源方法的实现者
有几列,也可以问数据源实现者,如果外界没实现,可以预先设置一个默认的列数.
所以有如下一些方法供内部调用,还有各种高度,等等.
-(CGFloat)marginForType:(WaterfallsViewMarginType)type{
CGFloat margin = 0;
if ([self.delegate respondsToSelector:@selector(waterfallsView:margins:)]) {
margin = [self.delegate waterfallsView:self margins:type];
}else{
margin = WaterfallsViewDefaultMargin;
}
return margin;
}
-(NSUInteger)columnsCount{
if ([self.dataSource respondsToSelector:@selector(numberOfColumns)]) return [self.dataSource numberOfColumns];
return WaterfallsViewDefaultColumnCount;
}
-(CGFloat)cellHeightAtIndex:(NSUInteger)index{
if ([self.delegate respondsToSelector:@selector(waterfallsView:heightAtIndex:)]) return [self.delegate waterfallsView:self heightAtIndex:index];
return WaterfallsViewDefaultCellHeight;
}
reloadData方法我就直接放出来了
-(void)reloadData{
[self.cellFrames removeAllObjects];
[self.displayingCells removeAllObjects];
CGFloat topM = [self marginForType:WaterfallsViewMarginTypeTop];
CGFloat leftM = [self marginForType:WaterfallsViewMarginTypeLeft];
CGFloat bottomM = [self marginForType:WaterfallsViewMarginTypeBottom];
CGFloat rowM = [self marginForType:WaterfallsViewMarginTypeRow];
CGFloat columnM = [self marginForType:WaterfallsViewMarginTypeColumn];
NSUInteger totalCellCount = [self.dataSource numberOfCells];
NSUInteger totalColumnCount = [self columnsCount];
CGFloat cellW = [self cellWidth];
//这个数组用来存放每一列的最大的高度
CGFloat maxYOfColumn[totalColumnCount];
for (int i = 0; i < totalColumnCount; i++) {
maxYOfColumn[i] = 0;
}
int cellColumn;
for (int i = 0; i < totalCellCount; i++) {
CGFloat cellH = [self cellHeightAtIndex:i];
cellColumn = 0;
for (int j = 1; j < totalColumnCount; j++) {
if (maxYOfColumn[j] < maxYOfColumn[cellColumn]) {
cellColumn = j;
}
}
CGFloat cellX = leftM + (cellW + columnM) * cellColumn;
CGFloat cellY;
if (maxYOfColumn[cellColumn] == 0) {
cellY = topM;
}else{
cellY = maxYOfColumn[cellColumn] + rowM;
}
CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH);
[self.cellFrames addObject:[NSValue valueWithCGRect:cellFrame]];
maxYOfColumn[cellColumn] = CGRectGetMaxY(cellFrame);
}
CGFloat maxYOfWaterfallsView = 0;
for (int i = 0; i < totalColumnCount; i++) {
if (maxYOfColumn[i] > maxYOfWaterfallsView) maxYOfWaterfallsView = maxYOfColumn[i];
}
maxYOfWaterfallsView += bottomM;
self.contentSize = CGSizeMake(0, maxYOfWaterfallsView);
}
以上只是把所有cell的frame求出来了,用数组保存,这样就可以知道每一个cell放在瀑布流上的哪个地方.最终得到最大的cell的高度后,就可以设置瀑布流视图的contentSize.不然无法滚动.但是,这还不够的,
我们要将cell在瀑布流视图上显示出来.
这个操作,是在layoutSubviews方法中实现
-(void)layoutSubviews{
[super layoutSubviews];
//回滚,从后往前遍历cell
if (self.scrollDirection == WaterfallsViewScrollDirectionRollback) {
for (int i = (int)[self.dataSource numberOfCells] - 1; i >=0 ; i--) {
[self handleCellWithIndex:i];
}
}else{ //往前滑动更多的cell,一般情况,从前往后遍历cell
for (int i = 0; i < [self.dataSource numberOfCells]; i++) {
[self handleCellWithIndex:i];
}
}
lastContentOffsetY = self.contentOffset.y;
NSLog(@"displaying Cells Count :%lu",(unsigned long)self.displayingCells.count);
}
//因为向前滚,序号小的cell要先消失,在后面的序号大的cell要新显示出来.所以,从序号小的遍历起.不在屏幕上的cell可以先回收到缓存池中,后面要显示的的cell就可以从缓存池中去拿了.
//同理,回滚的话,序号大的cell要先消失,在前面的序号小的cell要新显示出来,所以,从序号大的遍历起,不在屏幕上的cell先回收,前面新显示的cell就可以从缓存池中去拿了.
滚动方向的枚举定义,以及怎么获取滚动方向,如下
//滚动方向的枚举定义
typedef enum{
WaterfallsViewScrollDirectionForward,
WaterfallsViewScrollDirectionRollback
}WaterfallsViewScrollDirection;
//获得当前的滚动方向
-(WaterfallsViewScrollDirection)scrollDirection{
if (self.contentOffset.y < lastContentOffsetY) return WaterfallsViewScrollDirectionRollback;
return WaterfallsViewScrollDirectionForward;
}
//处理某一个序号的cell,从保存的cellFrames数组中获得这个序号的cell的frame,先尝试看当前cell有没有在显示在屏幕上,在displayingCells字典中能不能拿到
//如果当前的cell有在屏幕上显示,如果cell在displayingCells字典中没有拿到,问数据源方法要.然后给cell设置我们事先就算好的frame,把它加入到displayingCells字典中,
//同时加入到瀑布流视图上.
//如若不在屏幕上,并且displayingCells字典中能够取到,说明刚刚它在屏幕上显示着呢,现在要从屏幕上离开了,那就要把它从displayingCells字典中移除,同时从瀑布流视图移除,
//可以加入缓存池中
-(void)handleCellWithIndex:(NSUInteger)index{
CGRect cellFrame = [self.cellFrames[index] CGRectValue];
WaterfallsViewCell *cell = self.displayingCells[@(index)];
if ([self isOnScreen:cellFrame] == YES) {
if (cell == nil) {
cell = [self.dataSource waterfallsView:self cellAtIndex:index];
cell.frame = cellFrame;
self.displayingCells[@(index)] = cell;
[self addSubview:cell];
}
}else{
if (cell != nil) {
[self.displayingCells removeObjectForKey:@(index)];
[cell removeFromSuperview];
[self.reusableCells addObject:cell];
}
}
}
//是否在屏幕上,如果cell的y值最大处,比瀑布流视图的contentOffset.y小,说明在显示区域的上部分.如果cell的y值最小处,比瀑布流视图显示的最大y值的地方还大,说明说明在显示区域的下部分.
//这两种都不在屏幕上呢.其他情况,都是在屏幕上的
-(BOOL)isOnScreen:(CGRect)cellFrame{
if (CGRectGetMaxY(cellFrame) <= self.contentOffset.y) return NO;
if (cellFrame.origin.y >= self.contentOffset.y + self.frame.size.height) return NO;
return YES;
}
/**
* 供外界调用取可重复利用cell
*/
//外界调用,当控制器中实现数据源方法
-(WaterfallsViewCell *)waterfallsView:(WaterfallsView *)waterfallsView cellAtIndex:(NSUInteger)index;
时,先调用这个方法,从缓存池中取cell,不同结构的cell可以用不同的identifier以示区别.从缓存池中取cell,也是根据cell的identifier来取.
当缓存池中取到cell了之后,要将cell从缓存池中移除,表示这个cell已经被利用了,取不到cell,说明这个identifier标示的cell已经被用完了.外面需要自己新建cell.这个就是cell的根据标识重复利用cell的原理.
/**
* 在某一个序号的cell
*/
-(WaterfallsViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier{
__block WaterfallsViewCell *cell = nil;
[self.reusableCells enumerateObjectsUsingBlock:^(WaterfallsViewCell *reusableCell, BOOL *stop) {
if ([reusableCell.reuseIdentifier isEqualToString:identifier]) {
cell = reusableCell;
*stop = YES;
}
}];
if (cell != nil) {
[self.reusableCells removeObject:cell];
}
// NSLog(@"缓存池剩余的cell个数:%ld",self.reusableCells.count);
return cell;
}
//当点击了某个cell之后,需要有所响应.代理方法中拿到了点击的cell之后,即可做相应的处理.
所以这里实现了touchesBegan: withEvent:方法
//通过遍历displayingCells中的所有cell,如果触摸发生的地方正好在其中的某一个cell中,把cell的序号通过代理方法传出去,外界就知道了某个cell被点击了,自己实现相应的方法,即可做出相应的反应.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint pointInView = [touch locationInView:self];
__block NSInteger selectedIndex = -1;
[self.displayingCells enumerateKeysAndObjectsUsingBlock:^(NSNumber *index, WaterfallsViewCell *cell, BOOL * stop) {
if (CGRectContainsPoint(cell.frame, pointInView) == YES) {
selectedIndex = [index unsignedIntegerValue];
*stop = YES;
}
}];
if (selectedIndex >= 0) {
if ([self.delegate respondsToSelector:@selector(waterfallsView:didSelectCellAtIndex:)]) {
[self.delegate waterfallsView:self didSelectCellAtIndex:selectedIndex];
}
}
}
演示图片如下:
源码下载地址: