1.瀑布流
瀑布流,又稱瀑布流式佈局。是比較流行的一種網站頁面佈局,視覺表現為參差不齊的多欄佈局,隨著頁面滾動條向下滾動,這種佈局還會不斷載入資料塊並附加至當前尾部。
QQ20150916-4.png
2.瀑布流中需要注意的點
- cell的新增順序:瀑布流佈局中,每一行的cell擺放的位置並不是按順序擺放,而是優先新增在上一行中高度最短的列中。如果不是按照最短高度所在的列優先新增,就會導致列遇列之間高度相差越來越大,接下來我們用圖片來總結理解為什麼要這樣做。
QQ20150916-2.png
QQ20150916-3.png
選擇了按最短列優先新增需要我們做的是,在每次新增後,需要找到總列數中,高度最短的一列,需要我們寫一個比較列數高度最小的比較演算法,用一個陣列裝入不同列的高度,選出高度最短的列號作為下次新增的列號。
1 2 3 4 5 6 7 8 9 10 |
// cell 處在最短的一列 NSUInteger cellColumn = 0; // cell 所處那列的最大Y值(最短那一列的最大Y值) CGFloat maxYOfCellColum = maxYOfColums[cellColumn]; for (int j = 1; j < numberOfColumns; j++) { if (maxYOfColums[j] < maxYOfCellColum) { cellColumn = j; maxYOfCellColum = maxYOfColums[j]; } } |
- cell的建立規則:在瀑布流中,由於資料比較多,不能一次性建立所有的cell,這樣會造成記憶體的浪費。我們可以發現,當cell出現在螢幕上(肉眼可以觀察到的位置)時,我們才有必要把這個cell顯示出來,就如UITableViewCell一樣,需要的時候才建立,不需要的時候放在快取池裡,建立cell的時候優先在快取池中查詢有無可迴圈利用的cell。
總結:在建立cell的時候,我們的規則是,優先從快取池中尋找可迴圈利用的cell,有的話直接使用cell,沒有可迴圈利用的話再建立。
123456789101112131415- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier{__block RFWaterFallViewCell *reusableCell = nil;[self.reusableCells enumerateObjectsUsingBlock:^(RFWaterFallViewCell *cell, BOOL *stop) {if ([cell.identifier isEqualToString:identifier]) {reusableCell = cell;*stop = YES;}}];if (reusableCell) { // 從快取池中移除[self.reusableCells removeObject:reusableCell];}return reusableCell;}
3.結構設計
要顯示一個瀑布流佈局,首先我們要有資料,於是我們可以建立資料來源的protocol,我們需要知道每個cell的高度和點選cell的事件處理等,我們可以建立瀑布流控制元件的代理delegate用來監聽控制器點選事件。怎麼樣,是不是感覺這個結構很熟悉呢,這不就是我們天天在用的UITableView本來的結構嗎?
我們先來看一下UITableView是怎麼展示資料的:
- 呼叫資料來源的下面方法得知一共有多少組資料
1 |
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView; |
- 呼叫資料來源的下面方法得知每一組有多少行資料
1 |
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; |
- 呼叫資料來源的下面方法得知每一行顯示什麼內容
1 |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; |
好了,這樣的結構我們再熟悉不過了,我們接著來模範UITableView來建立屬於我們自己的瀑布流控制器
QQ20150915-1.png
- 首先我們需要知道我們資料來源中需要顯示多少個cell、每個index索引顯示對應的是哪個cell、每一行需要顯示多少列。
1234567891011121314#pragma mark - RFWaterFallDataSource 資料來源方法@protocol RFWaterFallDataSource@required/** 顯示多少個cell */- (NSUInteger)numberOfCellInWaterFallView:(RFWaterFallView *)waterFallView;/** 顯示每個index對應的cell */- (RFWaterFallViewCell *)waterFallView:(RFWaterFallView *)waterFallView cellForRowAtIndexPath:(NSUInteger)index;@optional/** 顯示每一行多少列*/- (NSUInteger)numberOfColumnsInWaterFallView:(RFWaterFallView *)waterFallView;@end - 在代理中,我們需要知道使用者點選了是哪個cell,每一個index所對應的cell的高度是多少。
12345678#pragma mark - RFWaterFallViewDelegate 代理方法@protocol RFWaterFallViewDelegate/** index位置的cell的高度 */- (CGFloat)waterFallView:(RFWaterFallView *)waterFallView heightAtIndex:(NSUInteger)indexPath;/** 選中第index個cell*/- (void)waterFallView:(RFWaterFallView *)waterFallView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;@end - 在瀑布流控制元件中,我們僅僅需要像UITableView一樣暴露dataSource、delegate屬性,和一些常用的公共方法
123456789101112131415#pragma mark - RFWaterFallView 瀑布流控制元件@interface RFWaterFallView : UIScrollView/** 資料來源 */@property (nonatomic, weak) id dataSource;/** 代理 */@property (nonatomic, weak) id delegate;/** 重新整理資料*/- (void)reloadData;/** 在快取池中取得可迴圈利用的cell*/- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;@end
4.實現效果
在需要實現瀑布流的程式中,可以直接把我們生成的類作為控制元件來使用,就能輕鬆達到瀑布流的效果,如下:
5.完整的實現檔案程式碼
以後需要用到瀑布流的時候,直接拖入檔案像UITableView那樣呼叫就可以使用了~怎麼樣,是不是很方便呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
@interface RFWaterFallView() /** 所有cell的frame */ @property (nonatomic, strong) NSMutableArray *cellFrames; /** 正在展示的cell */ @property (nonatomic, strong) NSMutableDictionary *displayingCells; /** 快取池 (用Set,存放離開螢幕的cell) */ @property (nonatomic, strong) NSMutableSet *reusableCells; @end @implementation RFWaterFallView #pragma mark - 懶載入 - (NSMutableArray *)cellFrames { if (_cellFrames == nil) { self.cellFrames = [NSMutableArray array]; } return _cellFrames; } - (NSMutableDictionary *)displayingCells { if (_displayingCells == nil) { self.displayingCells = [NSMutableDictionary dictionary]; } return _displayingCells; } - (NSMutableSet *)reusableCells { if (_reusableCells == nil) { self.reusableCells = [NSMutableSet set]; } return _reusableCells; } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { } return self; } #pragma mark - 公共方法 /** 重新整理資料 計算每一個cell的frame */ - (void)reloadData{ // cell的總數 int numberOfcells = [self.dataSource numberOfCellInWaterFallView:self]; // 每一行顯示的列數 int numberOfColumns = [self numberOfColumns]; // 間距 CGFloat topM = [self marginForType:RFWaterfallViewMarginTypeTop]; CGFloat bottomM = [self marginForType:RFWaterfallViewMarginTypeBottom]; CGFloat leftM = [self marginForType:RFWaterfallViewMarginTypeLeft]; CGFloat rightM = [self marginForType:RFWaterfallViewMarginTypeRight]; CGFloat columnM = [self marginForType:RFWaterfallViewMarginTypeColumn]; CGFloat rowM = [self marginForType:RFWaterfallViewMarginTypeRow]; // cell的寬度 CGFloat cellW = (self.width - leftM - rightM - (numberOfColumns - 1) * columnM) / numberOfColumns; // 用一個C語言陣列存放所有列的最大Y值 CGFloat maxYOfColums[numberOfColumns]; for (int i = 0; i < numberOfColumns; i++) { // 初始化陣列 maxYOfColums[i] = 0.0; } // 計算所有cell的frame for (int i = 0; i<numberOfcells; i++) { // cell 處在最短的一列 NSUInteger cellColumn = 0; // cell 所處那列的最大Y值(最短那一列的最大Y值) CGFloat maxYOfCellColum = maxYOfColums[cellColumn]; for (int j = 1; j < numberOfColumns; j++) { if (maxYOfColums[j] < maxYOfCellColum) { cellColumn = j; maxYOfCellColum = maxYOfColums[j]; } } //詢問代理i位置的高度 CGFloat cellH = [self heightAtIndex:i]; // cell的位置 CGFloat cellX = leftM + cellColumn * (cellW + columnM); CGFloat cellY = 0; if (maxYOfCellColum == 0.0) { //首行 cellY = topM; }else { cellY = maxYOfCellColum + rowM; } // 新增frame到陣列中 CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH); [self.cellFrames addObject:[NSValue valueWithCGRect:cellFrame]]; // 更新最短那一列的最大Y值 maxYOfColums[cellColumn] = CGRectGetMaxY(cellFrame); } // 設定contentSize CGFloat contentH = maxYOfColums[0]; for (int j = 1; j < numberOfColumns ; j++) { if (maxYOfColums[j] > contentH) { contentH = maxYOfColums[j]; } } contentH += bottomM; self.contentSize = CGSizeMake(0, contentH); } /** 當UIScrollView滾動的時候會呼叫這個方法 */ - (void)layoutSubviews{ [super layoutSubviews]; // 向資料來源索要對應位置的cell NSUInteger numberOfCells = self.cellFrames.count; for (int i = 0; i < numberOfCells; i++) { // 取出i位置的frame CGRect cellFrame = [self.cellFrames[i] CGRectValue]; // 優先從字典中取出i位置的cell RFWaterFallViewCell *cell = self.displayingCells[@(i)]; // 判斷i位置對應的frame在不在螢幕上 if ([self isInScreen:cellFrame]) { // 在螢幕上 if (cell == nil) { cell = [self.dataSource waterFallView:self cellForRowAtIndexPath:i]; cell.frame = cellFrame; [self addSubview:cell]; // 存放到字典中 self.displayingCells[@(i)] = cell; } }else{ // 不在螢幕上 if(cell){ // 從scrollview和字典中移除 [cell removeFromSuperview]; [self.displayingCells removeObjectForKey:@(i)]; // 存放到快取池中 [self.reusableCells addObject:cell]; } } cell = [self.dataSource waterFallView:self cellForRowAtIndexPath:i]; } } - (id)dequeueReusableCellWithIdentifier:(NSString *)identifier{ __block RFWaterFallViewCell *reusableCell = nil; [self.reusableCells enumerateObjectsUsingBlock:^(RFWaterFallViewCell *cell, BOOL *stop) { if ([cell.identifier isEqualToString:identifier]) { reusableCell = cell; *stop = YES; } }]; if (reusableCell) { // 從快取池中移除 [self.reusableCells removeObject:reusableCell]; } return reusableCell; } #pragma mark - 私有方法 - (CGFloat)marginForType:(RFWaterfallViewMarginType)type { if ([self.delegate respondsToSelector:@selector(waterfallView:marginForType:)]) { return [self.delegate waterfallView:self marginForType:type]; } else { return RFWaterfallViewDefaultMargin; } } - (NSUInteger)numberOfColumns{ if ([self.dataSource respondsToSelector:@selector(numberOfColumnsInWaterFallView:)]) { return [self.dataSource numberOfColumnsInWaterFallView:self]; } return RFWaterfallViewDefaultNumberOfColumns; } /** 返回每個cell對應的高度 */ - (CGFloat)heightAtIndex:(NSUInteger)index{ if ([self.delegate respondsToSelector:@selector(waterFallView:heightAtIndex:)]) { return [self.delegate waterFallView:self heightAtIndex:index]; } return RFWaterfallViewDefaultCellH; } /** 判斷一個cell有沒有在螢幕上顯示出來*/ - (BOOL)isInScreen:(CGRect)frame{ return (CGRectGetMaxY(frame) > self.contentOffset.y) && (CGRectGetMinY(frame) < self.contentOffset.y + self.height); } #pragma mark - 事件處理 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ if (![self.delegate respondsToSelector:@selector(waterFallView:didSelectRowAtIndexPath:)]) { return; } // 獲得觸控點 UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self]; __block NSNumber *selectIndex = nil; [self.displayingCells enumerateKeysAndObjectsUsingBlock:^(id key, RFWaterFallViewCell *cell, BOOL *stop) { if (CGRectContainsPoint(cell.frame, point)) { selectIndex = key; *stop = YES; } }]; if (selectIndex) { [self.delegate waterFallView:self didSelectRowAtIndexPath:selectIndex]; } } |
6.後記:
需要demo的我再fa最近開始學習swift,由OC轉化為swift希望學起來比較快,希望學習的步伐能越來越快,還請各位前輩們多多指導