闲言碎语不要讲,直接看demo效果
瀑布流效果
再来看看整个项目截图吧,很简单
项目结构
首先我们来了解一下瀑布流,这种形式大多用于电商的app,像Pinterest
,蘑菇街之类的,展示一些高度不同的图片,这种布局适合于小数据块,每个数据块内容相近且没有侧重。通常,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。所以,我决定基于scrollView
来写这个demo,同时当说到上下滚动并且展示内容,我们第一时间想到了UITableView
,那么让我们来想想怎么使用tableView
,下面列出它的数据源和代理中一些常用的方法。
#pragma mark UITableViewDataSource method
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
@optional
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
#pragma mark - UITableViewDelegate method
-(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
我们通过调用这些代理方法基本就能布局好一个tableView
,当然,作为一个iOS开发工程师,我们当然要以apple的标准来,所以我决定模仿tableView的代理API方式,写一个自己的waterFallView
,下面上代码,是waterFallView
的代理方法。
@class YLWaterFallCell, YLWaterFallView;
/**
* 数据源
*/
@protocol YLWaterFallViewDataSource <NSObject>
/**
* 返回index所在位置的cell
*/
-(YLWaterFallCell *)waterFallView:(YLWaterFallView *)waterFallView cellForIndex:(NSUInteger)index;
/**
* 返回一共有多少个cell
*/
-(NSUInteger)numbersOfCellsInWaterFallView:(YLWaterFallView *)waterFallView;
@optional
/**
* 返回有多少列
*/
-(NSUInteger)numbersOfColumnsInWaterFallView:(YLWaterFallView *)waterFallView;
@end
/**
* 代理
*/
@protocol YLWaterFallViewDelegate <NSObject>
@optional
/**
* 返回index位置cell的高度
*/
-(CGFloat)waterFallView:(YLWaterFallView *)waterFallView heightForCellAtIndex:(NSUInteger)index;
/**
* 返回间距
*/
-(CGFloat)waterFallView:(YLWaterFallView *)waterFallView marginForType:(YLWaterFallViewMarginType)type;
/**
* 处理选中事件
*/
-(void)waterFallView:(YLWaterFallView *)waterFallView didSelectedAtIndex:(NSUInteger)index;
整个view的两个代理就写好了,大家一定跃跃欲试想赶紧到controller里面去调用,布局整个界面了吧,不过先不着急,再想想,我们用tableView布局的时候,用什么显示数据?没错是一个cell,所以我们先定义一个cell,继承UIView,就这么简单,其他什么事情也不做这里一个cell就是一个小块,放置一块内容,但是既然有了cell我们就要调用,我们再想想table怎么创建cell呢/相信大家都很熟悉
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (!cell) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
}
cell.textLabel.text = [NSString stringWithFormat:@"This is %ld", (long)indexPath.row];
return cell;
没错,就是dequeueReusableCellWithIdentifier:
方法,那我们不妨也先定义在头文件里,用到时候再说嘛。然后呢,调完代理之后还要显示数据,于是我们想到了reloadData
方法,OK,现在让我们来看看头文件吧
YLWaterFallView.h
@interface YLWaterFallView : UIScrollView
@property (nonatomic, weak) id<YLWaterFallViewDataSource> datasource;
@property (nonatomic, weak) id<YLWaterFallViewDelegate> delegate;
/**
* 刷新数据
*/
-(void)reloadData;
/**
* 得到缓存池的cell
*/
-(id)dequeueReusableCellWithIdentifier:(NSString *)identifier;
@end
下面我们先不着急实现方法,现在controller里面调用,就和tableView一样,相信大家都比我熟练,什么创建view,遵守协议,设置代理就不再多言,直接进入调用部分
YLWaterFallViewController.m
#pragma mark - YLWaterFallViewDelegate method
-(CGFloat)waterFallView:(YLWaterFallView *)waterFallView heightForCellAtIndex:(NSUInteger)index
{
switch (index % 3) {
case 0:
return 150;
case 1:
return 110;
case 2:
return 200;
default:
return 100;
}
}
-(CGFloat)waterFallView:(YLWaterFallView *)waterFallView marginForType:(YLWaterFallViewMarginType)type
{
switch (type) {
case YLWaterFallViewMarginTypeBottom:
case YLWaterFallViewMarginTypeLeft:
case YLWaterFallViewMarginTypeRight:
case YLWaterFallViewMarginTypeTop:
return 10;
break;
case YLWaterFallViewMarginTypeColumns:
return 12;
break;
case YLWaterFallViewMarginTypeRows:
return 15;
break;
default:
return 11;
break;
}
}
-(void)waterFallView:(YLWaterFallView *)waterFallView didSelectedAtIndex:(NSUInteger)index
{
NSLog(@"点击了第%ld个", index);
}
#pragma mark - YLWaterFallViewDataSource method
-(YLWaterFallCell *)waterFallView:(YLWaterFallView *)waterFallView cellForIndex:(NSUInteger)index
{
YLWaterFallCell *cell = [waterFallView dequeueReusableCellWithIdentifier:@"cell"];
if (!cell) {
cell = [[YLWaterFallCell alloc] init];
cell.identifier = @"cell";
}
cell.backgroundColor = YLRandomColor;
return cell;
}
-(NSUInteger)numbersOfCellsInWaterFallView:(YLWaterFallView *)waterFallView
{
return 100;
}
-(NSUInteger)numbersOfColumnsInWaterFallView:(YLWaterFallView *)waterFallView
{
return 4;
}
关于这里的这些数据都是我随意设置的,看官可随心意更改,就把它当作tableView的代理一样。
当然,如果真的是tableView的话,此时就大功告成啦,其实这才是刚刚开始,下面我们回到waterFallView.m文件中完成那些方法吧。
先说说我的想法吧,和tableView一样,这个控件最大的核心在于cell的重用,也是最难的地方,我所做的就是先根据代理方法的返回值计算出每一个cell的frame,然后密切关注每个cell,如果它一直在屏幕上,就不去管它,让它随心所欲的滚,只要它滚出屏幕看不见了,就将其放进缓存池,一旦有新的cell进入屏幕,优先从缓存池中去找是否有闲置的cell,如果没有,就用代理方法创建一个,直到cell完全够用,循环利用,子子孙孙,无穷匮也。于是我用了三个属性,诸君一看便知
YLWaterFallView.m
/**
* 存放frame的数组
*/
@property (nonatomic, strong) NSMutableArray *frameArray;
/**
* 存放显示在屏幕上的cell,用字典
*/
@property (nonatomic, strong) NSMutableDictionary *cellsOnScreen;
/**
* 缓存池
*/
@property (nonatomic, strong) NSMutableSet *reusableCells;
这里字典里存储的key/value
对应cell的index/该cell
,set里存储的就是cell,因为set是无序的,符合缓存池的特性。
关于计算cell的frame问题,大家不要疑惑,我们是先根据代理方法返回的那些值来计算frame,并不是传统意义上的根据上一个显示的cell来计算下一个,这里是先讲cell的frame都计算完毕,再等着屏幕滚动,我来判断谁滚蛋了,谁还在。另外,这里cell的排列原则是,取y值最小的一列,将最新的cell摆放上去,这样才能造成层次不齐的效果,比如有三列cell,我们遍历每一列的y值,取出最小的,加上间距就是最新cell的y值,很简单的一个取最小值的算法,我们其中一段核心代码。在reloadData
方法里。
//5.计算cell的frame
//先用一个c类型数组存起每一列的最大值
CGFloat maxYOfColumns[numberOfColumns];
for (NSUInteger i = 0; i < numberOfColumns; i++) {
maxYOfColumns[i] = 0.0;
}
//计算每一个cell所在的位置,这里的原则是依次遍历每一列的y值,取最小的一列放置最新的cell,这样才能达到瀑布流的效果
for (NSUInteger i = 0; i < numberOfCells; i++) {
//从第0列开始一个一个对比,有比它的y值小的就取出来,直到所有列数遍历完剩下的就是最小值,一个很基础的算法
NSUInteger theColumn = 0;
CGFloat yOfTheColumn = maxYOfColumns[theColumn];
for (NSUInteger j = 0; j < numberOfColumns; j++) {
if (maxYOfColumns[j] < yOfTheColumn) {
theColumn = j;
yOfTheColumn = maxYOfColumns[j];
}
}
//取出该cell的高度
CGFloat cellH = [self heightAtIndex:i];
//x值
CGFloat cellX = left + theColumn * (cellW + column);
//y值
CGFloat cellY;
if (yOfTheColumn == 0) {
cellY = top;
}else{
cellY = yOfTheColumn + row;
}
CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH);
//添加到frame数组中
[self.frameArray addObject:[NSValue valueWithCGRect:cellFrame]];
//更新这一列的y值
maxYOfColumns[theColumn] = CGRectGetMaxY(cellFrame);
}
我觉得注释也写得挺清楚的,下面再看重用的问题,这个问题重在监控view的滚动,然后根据cell是否在视图上,如果不在是否能从缓存池里取得以及移除view的cell及时扔进缓存池,大家可能会想到用scrollView的代理方法监听滚动,但是这里有更好的,因为涉及frame,使用layoutSubviews
会更合适,因为view一滚动,这个也会调用,实在是监听滚动,设置frame,居家旅行,杀人越货的神器。看代码。
-(void)layoutSubviews
{
for (NSUInteger i = 0; i < self.frameArray.count; i++) {
//取出frame
CGRect cellFrame = [self.frameArray[i] CGRectValue];
//先从屏幕显示cell的数组中取出
YLWaterFallCell *cell = self.cellsOnScreen[@(i)];
if ([self cellIsOnScreen:cellFrame]) {
if (cell == nil) {//缓存池里没有可重用的cell
cell = [self.datasource waterFallView:self cellForIndex:i];//找代理要
cell.frame = cellFrame;
self.cellsOnScreen[@(i)] = cell;
[self addSubview:cell];
}
}else{
if (cell) {
[cell removeFromSuperview];
[self.cellsOnScreen removeObjectForKey:@(i)];
//放入缓存池
[self.reusableCells addObject:cell];
}
}
}
}
好了,如果大家想要用这个控件的话,直接导入
这四个文件,然后像用tableView的一样用法就好,希望大家能喜欢。
网友评论