瀑布流的原理与实现

-.什么是瀑布流?

瀑布流视图与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(@"点击了第%lucell",(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];

        }

    }

}

演示图片如下:

源码下载地址:

https://github.com/GudTeach/WaterfallsView

posted on 2016-05-08 12:43  yesss  阅读(31638)  评论(0编辑  收藏  举报

导航