一:瀑布流框架封装的实现思路:此瀑布流框架的封装仿照tableView的底层实现,1:每个cell的frame的设置都是找出每列的最大y值,比较每列的最大y值,将下一个cell放在最大y值最小的那一列,并更新最大y值,继续比较设置frame。2:还涉及了类似于tableView缓存池的处理 瀑布流效果如图:
二:封装代码:
1:瀑布流控件的view封装:
1 // 使用瀑布流形式展示内容的控件 2 3 #import <UIKit/UIKit.h> 4 5 typedef enum { 6 HMWaterflowViewMarginTypeTop, 7 HMWaterflowViewMarginTypeBottom, 8 HMWaterflowViewMarginTypeLeft, 9 HMWaterflowViewMarginTypeRight, 10 HMWaterflowViewMarginTypeColumn, // 每一列 11 HMWaterflowViewMarginTypeRow, // 每一行 12 } HMWaterflowViewMarginType; 13 14 @class HMWaterflowView, HMWaterflowViewCell; 15 16 /** 17 * 数据源方法 18 */ 19 @protocol HMWaterflowViewDataSource <NSObject> 20 @required 21 /** 22 * 一共有多少个数据 23 */ 24 - (NSUInteger)numberOfCellsInWaterflowView:(HMWaterflowView *)waterflowView; 25 /** 26 * 返回index位置对应的cell 27 */ 28 - (HMWaterflowViewCell *)waterflowView:(HMWaterflowView *)waterflowView cellAtIndex:(NSUInteger)index; 29 30 @optional 31 /** 32 * 一共有多少列 33 */ 34 - (NSUInteger)numberOfColumnsInWaterflowView:(HMWaterflowView *)waterflowView; 35 @end 36 37 /** 38 * 代理方法 39 */ 40 @protocol HMWaterflowViewDelegate <UIScrollViewDelegate> 41 @optional 42 /** 43 * 第index位置cell对应的高度 44 */ 45 - (CGFloat)waterflowView:(HMWaterflowView *)waterflowView heightAtIndex:(NSUInteger)index; 46 /** 47 * 选中第index位置的cell 48 */ 49 - (void)waterflowView:(HMWaterflowView *)waterflowView didSelectAtIndex:(NSUInteger)index; 50 /** 51 * 返回间距 52 */ 53 - (CGFloat)waterflowView:(HMWaterflowView *)waterflowView marginForType:(HMWaterflowViewMarginType)type; 54 @end 55 56 /** 57 * 瀑布流控件 58 */ 59 @interface HMWaterflowView : UIScrollView 60 /** 61 * 数据源 62 */ 63 @property (nonatomic, weak) id<HMWaterflowViewDataSource> dataSource; 64 /** 65 * 代理 66 */ 67 @property (nonatomic, weak) id<HMWaterflowViewDelegate> delegate; 68 69 /** 70 * 刷新数据(只要调用这个方法,会重新向数据源和代理发送请求,请求数据) 71 */ 72 - (void)reloadData; 73 74 /** 75 * cell的宽度 76 */ 77 - (CGFloat)cellWidth; 78 79 /** 80 * 根据标识去缓存池查找可循环利用的cell 81 */ 82 - (id)dequeueReusableCellWithIdentifier:(NSString *)identifier; 83 @end
思路分析:1:仿照tableView来设置dataSource 和 delegate 两个代理:waterFlow的dataSource代理方法中仿照tableView的dataSource代理方法,@required 必须实现的是,1:一共有多少个cell,- (NSUInteger)numberOfCellsInWaterflowView:(HMWaterflowView *)waterflowView ,2:根据cell的index返回每一个cell,- (HMWaterflowViewCell *)waterflowView:(HMWaterflowView *)waterflowView cellAtIndex:(NSUInteger)index;其中HMWaterflowViewCell就为每一个单元的cell,自定义HMWaterflowViewCell继承于UIView 3:@optional 方法为,一共返回多少列,如果没有实现此方法则返回默认的列数 2:在waterFlow的delegate方法中,都为@optional,并仿照tableView提供,1:根据index返回每个cell的行高 - (CGFloat)waterflowView:(HMWaterflowView *)waterflowView heightAtIndex:(NSUInteger)index;2:点击index处的cell的方法:- (void)waterflowView:(HMWaterflowView *)waterflowView didSelectAtIndex:(NSUInteger)index; 3:返回整个瀑布流的间距值,若不返回,则会有默认值,上下左右,行间距,列间距,根据上下左右行列不同的类型返回不同的高度,将这几个不同的类型定义成枚举值,将枚举传过去,外界可利用switch根据不同类型,返回不同的间距(类似于按钮回调的枚举tag)
2:以属性设置两个代理:设置delegate时,会有警告,因为瀑布流控件继承的是UIScroll,则继承了UIScroll在.h声明的属性和方法,此delegate会覆盖掉scrollView的滚动视图的代理,所以会有警告,解决办法是,让瀑布流控件的代理协议再遵守UISCrollViewDelegate,则在外部只要遵守了瀑布流的代理,也就遵守了UIScroll的滚动代理,所以以属性设置瀑布流代理后,外部遵守协议,设置代理,则其就可以调用UIscroll的滚动代理方法了
3:仿照tableView并提供刷新表格,和缓存池根据重用标识取cell的方法,- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;其中id 和 instanceType的区别:1:id 修饰非关联返回类型 ,可做参数,也可以做返回值 而instanceType为关联返回类型,返回该方法所在类的类型,并且只能做返回值,一般以alloc。autorelease,retain开头的方法用id 2:用instanceType相对id来说,能帮助编译器快速确定返回值类型。
2:瀑布流的内部核心封装代码:
1 #import "HMWaterflowView.h" 2 #import "HMWaterflowViewCell.h" 3 4 #define HMWaterflowViewDefaultCellH 70 5 #define HMWaterflowViewDefaultMargin 8 6 #define HMWaterflowViewDefaultNumberOfColumns 3 7 8 @interface HMWaterflowView() 9 /** 10 * 所有cell的frame数据 11 */ 12 @property (nonatomic, strong) NSMutableArray *cellFrames; 13 /** 14 * 正在展示的cell 15 */ 16 @property (nonatomic, strong) NSMutableDictionary *displayingCells; 17 /** 18 * 缓存池(用Set,存放离开屏幕的cell) 19 */ 20 @property (nonatomic, strong) NSMutableSet *reusableCells; 21 @end 22 23 @implementation HMWaterflowView 24 25 #pragma mark - 初始化 26 - (NSMutableArray *)cellFrames 27 { 28 if (_cellFrames == nil) { 29 self.cellFrames = [NSMutableArray array]; 30 } 31 return _cellFrames; 32 } 33 34 - (NSMutableDictionary *)displayingCells 35 { 36 if (_displayingCells == nil) { 37 self.displayingCells = [NSMutableDictionary dictionary]; 38 } 39 return _displayingCells; 40 } 41 42 - (NSMutableSet *)reusableCells 43 { 44 if (_reusableCells == nil) { 45 self.reusableCells = [NSMutableSet set]; 46 } 47 return _reusableCells; 48 } 49 50 - (id)initWithFrame:(CGRect)frame 51 { 52 self = [super initWithFrame:frame]; 53 if (self) { 54 55 } 56 return self; 57 } 58 59 - (void)willMoveToSuperview:(UIView *)newSuperview 60 { 61 [self reloadData]; 62 } 63 64 #pragma mark - 公共接口 65 /** 66 * cell的宽度 67 */ 68 - (CGFloat)cellWidth 69 { 70 // 总列数 71 int numberOfColumns = [self numberOfColumns]; 72 CGFloat leftM = [self marginForType:HMWaterflowViewMarginTypeLeft]; 73 CGFloat rightM = [self marginForType:HMWaterflowViewMarginTypeRight]; 74 CGFloat columnM = [self marginForType:HMWaterflowViewMarginTypeColumn]; 75 return (self.bounds.size.width - leftM - rightM - (numberOfColumns - 1) * columnM) / numberOfColumns; 76 } 77 78 /** 79 * 刷新数据 80 */ 81 - (void)reloadData 82 { 83 // 清空之前的所有数据 84 // 移除正在正在显示cell 85 [self.displayingCells.allValues makeObjectsPerformSelector:@selector(removeFromSuperview)]; 86 [self.displayingCells removeAllObjects]; 87 [self.cellFrames removeAllObjects]; 88 [self.reusableCells removeAllObjects]; 89 90 // cell的总数 91 int numberOfCells = [self.dataSource numberOfCellsInWaterflowView:self]; 92 93 // 总列数 94 int numberOfColumns = [self numberOfColumns]; 95 96 // 间距 97 CGFloat topM = [self marginForType:HMWaterflowViewMarginTypeTop]; 98 CGFloat bottomM = [self marginForType:HMWaterflowViewMarginTypeBottom]; 99 CGFloat leftM = [self marginForType:HMWaterflowViewMarginTypeLeft]; 100 CGFloat columnM = [self marginForType:HMWaterflowViewMarginTypeColumn]; 101 CGFloat rowM = [self marginForType:HMWaterflowViewMarginTypeRow]; 102 103 // cell的宽度 104 CGFloat cellW = [self cellWidth]; 105 106 // 用一个C语言数组存放所有列的最大Y值 107 CGFloat maxYOfColumns[numberOfColumns]; 108 for (int i = 0; i<numberOfColumns; i++) { 109 maxYOfColumns[i] = 0.0; 110 } 111 112 // 计算所有cell的frame 113 for (int i = 0; i<numberOfCells; i++) { 114 // cell处在第几列(最短的一列) 115 NSUInteger cellColumn = 0; 116 // cell所处那列的最大Y值(最短那一列的最大Y值) 117 CGFloat maxYOfCellColumn = maxYOfColumns[cellColumn]; 118 // 求出最短的一列 119 for (int j = 1; j<numberOfColumns; j++) { 120 if (maxYOfColumns[j] < maxYOfCellColumn) { 121 cellColumn = j; 122 maxYOfCellColumn = maxYOfColumns[j]; 123 } 124 } 125 126 // 询问代理i位置的高度 127 CGFloat cellH = [self heightAtIndex:i]; 128 129 // cell的位置 130 CGFloat cellX = leftM + cellColumn * (cellW + columnM); 131 CGFloat cellY = 0; 132 if (maxYOfCellColumn == 0.0) { // 首行 133 cellY = topM; 134 } else { 135 cellY = maxYOfCellColumn + rowM; 136 } 137 138 // 添加frame到数组中 139 CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH); 140 [self.cellFrames addObject:[NSValue valueWithCGRect:cellFrame]]; 141 142 // 更新最短那一列的最大Y值 143 maxYOfColumns[cellColumn] = CGRectGetMaxY(cellFrame); 144 } 145 146 // 设置contentSize 147 CGFloat contentH = maxYOfColumns[0]; 148 for (int j = 1; j<numberOfColumns; j++) { 149 if (maxYOfColumns[j] > contentH) { 150 contentH = maxYOfColumns[j]; 151 } 152 } 153 contentH += bottomM; 154 self.contentSize = CGSizeMake(0, contentH); 155 } 156 157 /** 158 * 当UIScrollView滚动的时候也会调用这个方法 159 */ 160 - (void)layoutSubviews 161 { 162 [super layoutSubviews]; 163 164 // 向数据源索要对应位置的cell 165 NSUInteger numberOfCells = self.cellFrames.count; 166 for (int i = 0; i<numberOfCells; i++) { 167 // 取出i位置的frame 168 CGRect cellFrame = [self.cellFrames[i] CGRectValue]; 169 170 // 优先从字典中取出i位置的cell 171 HMWaterflowViewCell *cell = self.displayingCells[@(i)]; 172 173 // 判断i位置对应的frame在不在屏幕上(能否看见) 174 if ([self isInScreen:cellFrame]) {// 在屏幕上 175 if (cell == nil) { 176 cell = [self.dataSource waterflowView:self cellAtIndex:i]; 177 cell.frame = cellFrame; 178 [self addSubview:cell]; 179 180 // 存放到字典中 181 self.displayingCells[@(i)] = cell; 182 } 183 } else { // 不在屏幕上 184 if (cell) { 185 // 从scrollView和字典中移除 186 [cell removeFromSuperview]; 187 [self.displayingCells removeObjectForKey:@(i)]; 188 189 // 存放进缓存池 190 [self.reusableCells addObject:cell]; 191 } 192 } 193 } 194 } 195 196 - (id)dequeueReusableCellWithIdentifier:(NSString *)identifier 197 { 198 __block HMWaterflowViewCell *reusableCell = nil; 199 200 [self.reusableCells enumerateObjectsUsingBlock:^(HMWaterflowViewCell *cell, BOOL *stop) { 201 if ([cell.identifier isEqualToString:identifier]) { 202 reusableCell = cell; 203 *stop = YES; 204 } 205 }]; 206 207 if (reusableCell) { // 从缓存池中移除 208 [self.reusableCells removeObject:reusableCell]; 209 } 210 return reusableCell; 211 } 212 213 #pragma mark - 私有方法 214 /** 215 * 判断一个frame有无显示在屏幕上 216 */ 217 - (BOOL)isInScreen:(CGRect)frame 218 { 219 return (CGRectGetMaxY(frame) > self.contentOffset.y) && 220 (CGRectGetMinY(frame) < self.contentOffset.y + self.bounds.size.height); 221 } 222 223 /** 224 * 间距 225 */ 226 - (CGFloat)marginForType:(HMWaterflowViewMarginType)type 227 { 228 if ([self.delegate respondsToSelector:@selector(waterflowView:marginForType:)]) { 229 return [self.delegate waterflowView:self marginForType:type]; 230 } else { 231 return HMWaterflowViewDefaultMargin; 232 } 233 } 234 /** 235 * 总列数 236 */ 237 - (NSUInteger)numberOfColumns 238 { 239 if ([self.dataSource respondsToSelector:@selector(numberOfColumnsInWaterflowView:)]) { 240 return [self.dataSource numberOfColumnsInWaterflowView:self]; 241 } else { 242 return HMWaterflowViewDefaultNumberOfColumns; 243 } 244 } 245 /** 246 * index位置对应的高度 247 */ 248 - (CGFloat)heightAtIndex:(NSUInteger)index 249 { 250 if ([self.delegate respondsToSelector:@selector(waterflowView:heightAtIndex:)]) { 251 return [self.delegate waterflowView:self heightAtIndex:index]; 252 } else { 253 return HMWaterflowViewDefaultCellH; 254 } 255 } 256 257 #pragma mark - 事件处理 258 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 259 { 260 if (![self.delegate respondsToSelector:@selector(waterflowView:didSelectAtIndex:)]) return; 261 262 // 获得触摸点 263 UITouch *touch = [touches anyObject]; 264 // CGPoint point = [touch locationInView:touch.view]; 265 CGPoint point = [touch locationInView:self]; 266 267 __block NSNumber *selectIndex = nil; 268 [self.displayingCells enumerateKeysAndObjectsUsingBlock:^(id key, HMWaterflowViewCell *cell, BOOL *stop) { 269 if (CGRectContainsPoint(cell.frame, point)) { 270 selectIndex = key; 271 *stop = YES; 272 } 273 }]; 274 275 if (selectIndex) { 276 [self.delegate waterflowView:self didSelectAtIndex:selectIndex.unsignedIntegerValue]; 277 } 278 } 279 280 @end
思路分析:1:瀑布流的核心代码的封装也是仿照tableView的底层实现:在tableView第一次加载的时候会首先调用一次刷新数据的方法,在下列方法中实现:
- (void)willMoveToSuperview:(UIView *)newSuperview{[self reloadData];},此方法willMoveToSuperview,是子view即将添加到superView时会调用一次。调用[self reloadData],模拟tableView第一次刷新表格 2:基本实现思路是,在reloadData方法中设置每一个cell的frame,存放到大数组中,再在layoutSubView里设置每一个view的frame。
2:在reloadData方法中,1:每执行一次reloadData方法,都要将原来的数据清空,来展示新的cell。其中self.displayingCells为正在显示的cell的缓存字典,key值为每一个index,value为每一个index对应的正在显示的cell(dic.allKeys,dic.allValues,分别获得字典的所有的key值和value值得到一个数组,enum遍历字典的时候,分别可得到字典的key值和value值)。2:在清空数据时,缓存字典,让字典中每一个正在显示的cell从父视图上移除,并清空缓存字典,缓存池,还有装有frame的数组,其中数组里有一个方法是遍历每个元素让每个元素都去执行某个方法[self.displayingCells.allValues makeObjectsPerformSelector:@selector(removeFromSuperview)]; 2:在reloadData中最主要的就是要求出每个cell的xy宽高,其中高度需要询问代理,若实现了代理方法,则根据服务器返回图片的宽高,按比例计算出相应的高度并返回。要想设置每个cell的frame,调用数据源方法,返回总的cell的个数,for循环遍历每个cell,因为设置frame,要从最大y值最小的那列开始设置,所以在for循环遍历每一个cell的时候,首先应找出最小y值的那一列,和最小y值。定义数组存放每一列的最大的y值,因为其为基本数据类型,所以用C语言数组存放,OC数组只能存放对象。先调用代理方法返回列数,若没有实现代理方法,则默认返回的而是3列,定义C语言的数组,定义数组元素个数为numberOfColumns列数的数组,且数组中元素的基本类型都为CGFloat类型。并对数组中的元素初始化,遍历数组,取出每个元素,赋值为0.0,float类型
CGFloat maxYOfColumns[numberOfColumns];
for (int i = 0; i<numberOfColumns; i++) {
maxYOfColumns[i] = 0.0;
}
3:找出最小y值所在的列号,并记录此列下最大的y值。先假设最小的y值的列号是第0列,则最小y值列号下的最大的y值就为从C语言数组中根据列号取出的该列的最大y值,再for循环遍历C语言数组,从列号为1开始遍历,做判断,从列号为1开始从数组中取出的最大y值还比定义的最大y值还小,则此时记录下最小的y值的列号,和最小y值列号下的最大y值。直到找到最小y值的列号,此时,设置frame就从最小y值的列号处开始设置。
4:找到最小y值所在的列号后,开始设置cell的frame,宽度 = 屏幕宽度 - 左右间距 - (总列数 -1)*列间距,都可以求出,屏幕的高度,调用代理根据index返回该index处的cell的高度,(瀑布流图片的宽高都为后台服务器返回),若实现了代理高度的方法,则返回高度时,应该根据服务器返回图片高度的比例计算出。若没实现代理方法则返回默认的高度。 询问代理i位置的高度 CGFloat cellH = [self heightAtIndex:i];X值的计算:计算x值和cell所在的列号有关, CGFloat cellX = leftM + cellColumn * (cellW + columnM);Y值计算:和是否是首行有关,若是在首行,则y值为顶部间距,若不在首行,则y值为该列的最大y值加上一个行间距的值,是否是首行的判断,则判断C语言数组中每一列对应的最大y值,若为0,则在首行,若不为0,则不在首行,此时可以设置cell的frame
5:将设置好的cell的frame(结构体类型,非对象)封装成OC对象,存放到数组中。 CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH); [self.cellFrames addObject:[NSValue valueWithCGRect:cellFrame]];
6:还要更新C语言数组中设置frame的所在列的最大y值,以便下次循环计算:maxYOfColumns[cellColumn] = CGRectGetMaxY(cellFrame);
7:在设置完所有的frame后,要根据C语言数组中最大的y值,计算scrollView的contentSize,否则scrollView将不会滚动。如何找出最大的y值?先定义第0列为最大y值,将最大的y值从数组中取出,在for循环遍历C语言数组,j从1开始,将数组中的每一列最大y值取出比较,若比这个最大的还大,则记录下来,最后计算contentSize为最大y值加一个底部间距
CGFloat contentH = maxYOfColumns[0];
for (int j = 1; j<numberOfColumns; j++) {
if (maxYOfColumns[j] > contentH) {
contentH = maxYOfColumns[j];
}
}
contentH += bottomM;
self.contentSize = CGSizeMake(0, contentH);
3:layoutSubView中设置每一个cell的frame:1:遍历存放cell所有frame的数组,取出每一个frame,CGRect cellFrame = [self.cellFrames[i] CGRectValue];首先先判断是否在屏幕上,如果不在屏幕上,则将其cell从父视图上移除,并且从缓存字典中移除,并加入到缓存池,NSMutableSet 所定义的集合中,同数组的用法是相同,存放对象,但数组是有序的,NSMutableSet是无序的。如果在屏幕上显示,则证明已经计算好frame,则优先从缓存字典中根据索引取出cell。判断取出的cell是否存在,不存在,则调用数据源方法,从缓存池中取或是自行创建,返回一个cell,设置frame,添加到scrollView上。并将新创建的cell存入缓存字典中。2:如何判断是否在屏幕上,传递frame,根据frame判断是否在屏幕上
- (BOOL)isInScreen:(CGRect)frame
{
return (CGRectGetMaxY(frame) > self.contentOffset.y) &&
(CGRectGetMinY(frame) < self.contentOffset.y + self.bounds.size.height);
}
4:根据重用标识从缓存池中取数据:遍历集合,得到每一个cell,做条件过滤,判断如果cell的重用标识符与传入的标识符相等,则赋值cell,并停止遍历,在block内部若是想赋值外部变量,则用__block修饰外部的变量,最后再判断cell如果存在从缓存池中删除,若是不删除重用的cell,则其不到重用的作用,内存会被撑爆.
5:cell的点击事件:1:若是想实现点击事件:1:继承UIControl的addTarget ,touch或是valueChanged 2;添加手势监听器,要开启用户交互权限(如lable,iamgeView)3:实现touchBeagn,touchEnd,touchCancle方法,其中的touchBeagn刚开始点击的时候调用,touchEnd结束点击的时候调用。2:cell的点击采取实现touch方法,实现的是touchEnd,点击结束时的方法。并实现UITouch的方法。3:首先做条件过滤,若没有实现该代理方法,直接返回,若实现了代理方法,先获得触摸点, UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self];得到触摸点,若是第二个参数为cell则触摸点的位置是相对cell来计算的,若是为self,则是针对整个瀑布流控件的frame来计算的。其中touch.view获得的是触摸的view,也就是cell 。目的是求出index,调用代理将index传过去,所以遍历缓存字典,得到key,value值,key为index,value为cell,再判断所得触摸点在不在cell上,调用CGRectContainsPoint(cell.frame, point),如果在,则赋值index,停止遍历,再外部判断index值是否存在,存在则调用代理执行代理的点击方法。@5或是@(3)两个均为对象的表示形式,都为NSNumber类型。
7:自定义cell的封装:
1 #import <UIKit/UIKit.h> 2 3 @interface HMWaterflowViewCell : UIView 4 @property (nonatomic, copy) NSString *identifier; 5 @end
1 #import "HMWaterflowViewCell.h" 2 3 @implementation HMWaterflowViewCell 4 5 - (id)initWithFrame:(CGRect)frame 6 { 7 self = [super initWithFrame:frame]; 8 if (self) { 9 // Initialization code 10 } 11 return self; 12 } 13 14 /* 15 // Only override drawRect: if you perform custom drawing. 16 // An empty implementation adversely affects performance during animation. 17 - (void)drawRect:(CGRect)rect 18 { 19 // Drawing code 20 } 21 */ 22 23 @end