-.什么是瀑布流?
瀑布流視圖與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(@"點擊了第%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];
}
}
}
演示圖片如下:
源碼下載地址: