瀑布流的原理與實現

-.什麼是瀑布流?git

瀑布流視圖與UITableView相似,可是相對複雜一點.UITableView只有一列,能夠有多個小節(section),每個小節(section)能夠有多行(row).github

瀑布流呢,能夠有多列,每個item(單元格)的高度能夠不相同,可是寬度必須同樣.排列的方式是,從左往右排列,哪一列如今的總高度最小,就優先排序把item(單元格)放在這一列.這樣排完全部的單元格後,能夠保證每一列的總高度都相差不大,不至於,有的列很矮,有的列很高.這樣就很難看了.數組

上面的數字,就是每一個單元格的序號,能夠看到item的排列順序是個什麼狀況.緩存

 

二.怎麼實現一個瀑布流呢?dom

仿照UITableView的設計,咱們要知道有多少個單元格,咱們得問咱們的數據源.有幾列,問數據源.在某一個序號上是怎樣的cell,問數據源.ide

某一個序號單元格的高度,問代理.單元格的列邊距,行邊距,總體的瀑布流視圖的上下左右距離瀑布流視圖所在的父視圖的邊距.這些,都問代理.性能

同時,咱們也要在接口處對外提供方法,reloadData,當瀑布流視圖要更新的時候能夠調用.咱們還要對外提供方法cellWidth,讓外界能夠直接知道每一個單元格的高度是怎樣的.同時,咱們也要提供一個相似於UITableView的用來從緩存池取cell的方法.否則的話,屏幕每滑動到新的單元格地方,就要從新新建一個cell,這樣當瀑布流總單元格多了以後,有多少單元格須要顯示就建立多少次,這樣是至關消耗性能的.因此要有緩存池,讓外界在取cell時優先從緩存池取,緩存池取不到了,再來新建一個cell也不遲.UITableView就是這麼作的.咱們也要這麼作.因此對外提供一個方法 -(WaterfallsViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;ui

仿照UITableView每一個單元格是一種UITableViewCell,咱們也能夠定義一個瀑布流cell,繼承於UIView便可,後期調用時能夠隨意定製cell的內容.atom

由於要實現的瀑布流,須要上下滾動,其實是一個UIScrollView.因此,直接繼承於UIScrollView.spa

 

因此有以下的接口定義

 

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

相關文章
相關標籤/搜索