-.什麼是瀑布流?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(@"點擊了第%lu個cell",(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];
}
}
}
演示圖片以下:
源碼下載地址: