瀑布流Demo

闲言碎语不要讲,直接看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的一样用法就好,希望大家能喜欢。

github地址https://github.com/shidayangli/WaterFallDemo.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容

  • 概述在iOS开发中UITableView可以说是使用最广泛的控件,我们平时使用的软件中到处都可以看到它的影子,类似...
    liudhkk阅读 8,834评论 3 38
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,616评论 4 59
  • 我们在上一篇《通过代码自定义不等高cell》中学习了tableView的相关知识,本文将在上文的基础上,利用sto...
    啊世ka阅读 1,435评论 2 7
  • 年过27,人生蹚了小半程,没到阅尽繁华写满沧桑,但也不少坎坷涟漪,酒肉不少穿肠过,姑娘没少身边睡。睡过的姑娘,有温...
    覃小黑阅读 275评论 0 0
  • 犹记那日,我与他,在风雪中相遇。他,犹如盛放的火焰,闯入了我的世界。
    千羽樱华阅读 88评论 0 1