瀑布流的原理與實現


-.什么是瀑布流?

瀑布流視圖與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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM