美文网首页
瀑布流Demo

瀑布流Demo

作者: _table | 来源:发表于2015-10-21 21:04 被阅读155次
    闲言碎语不要讲,直接看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

    相关文章

      网友评论

          本文标题:瀑布流Demo

          本文链接:https://www.haomeiwen.com/subject/btqthttx.html