瀑布流Demo

2015-10-21  本文已影响155人  _table
闲言碎语不要讲,直接看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

上一篇下一篇

猜你喜欢

热点阅读