瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布局的网站是Pinterest,逐渐在国内流行开来。国内大多数清新站基本为这类风格。
app上也有采用瀑布流显示表格的。它是指表格宽度固定(不同屏幕的手机适配时,采用屏幕宽度减去两边的边距和横向表格间距除以一行最多表格数),通常一行显示两个表格,高度随着图片高度和文本高度而形成左右两边的表格高度不同。一般都是采用UICollectionView的流式布局实现。
主要有两种实现方法:
方法一:
直接计算表格的高度和修改表格的frame。
这种方法的优点是实现简单,对普通表格的简单处理就能实现瀑布流。
缺点:当左右两侧的表格高度总和差别很大,会出现一边表格没有,另一边的表格还有很多,造成左右不基本对称。除非你能保证左右两边的高度基本相当,才能解决这个硬伤。
方法二:
自定义一个基于UICollectionViewFlowLayout的子类实现部分方法,并且计算出每个表格的高度。
当然也可以自定义一个基于UICollectionViewLayout的子类。只是他们对这个子类的使用和子类的方法稍有不通。经过测试都能实现瀑布流。至于网上说的只能使用基于UICollectionViewLayout的子类,纯属是他的一家之言。我就是用基于UICollectionViewLayout的子类来实现了,使用习惯了,本文就这个父类来实现瀑布流页面。
简单的方法只能实现简单的功能,有些硬伤是无法完美解决的。在技术这条路上没有任何捷径可走,完美的方案通常采用的技术较复杂些。注意:这个子类估计能实现表头的方法,表尾的高度很难计算,我没有实现表尾的代理方法。
方法二的实现代码:
FHWaterFallFlowLayout.h文件:
#import <UIKit/UIKit.h> @class FHWaterFallFlowLayout; @protocol FHWaterFallFlowLayoutDelegate <NSObject> @required /** * @brief cell的高度 * * @param index 某个cell * @param itemWidth cell的宽度 * * @return cell高度 */ - (CGFloat)waterflowLayout:(FHWaterFallFlowLayout *)waterflowLayout heightForItemAtIndex:(NSUInteger)index itemWidth:(CGFloat)itemWidth; @optional /** 瀑布流的列数 */ - (CGFloat)columnCountInWaterflowLayout:(FHWaterFallFlowLayout *)waterflowLayout; /**每一列之间的间距*/ - (CGFloat)columnMarginInWaterflowLayout:(FHWaterFallFlowLayout *)waterflowLayout; /**每一行之间的间距*/ - (CGFloat)rowMarginInWaterflowLayout:(FHWaterFallFlowLayout *)waterflowLayout; /**cell边缘的间距*/ - (UIEdgeInsets)edgeInsetsInWaterflowLayout:(FHWaterFallFlowLayout *)waterflowLayout; section header //- (CGSize)collectionView:(UICollectionView *)collectionView layout:(FHWaterFallFlowLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section; // //- (CGSize)collectionView:(UICollectionView *)collectionView layout:(FHWaterFallFlowLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section; @end @interface FHWaterFallFlowLayout : UICollectionViewFlowLayout @property (nonatomic, weak) id<FHWaterFallFlowLayoutDelegate> delegate; @end
FHWaterFallFlowLayout.m文件:
#import "FHWaterFallFlowLayout.h" static const CGFloat BDefaultColumnMargin = 3; static const CGFloat BDefaultRowMargin = 3; static const UIEdgeInsets BDefaultEdgeInsets = {8, 8, 8, 8}; static const CGFloat BDefaultColumnCount = 3; @interface FHWaterFallFlowLayout() @property (nonatomic, strong) NSMutableArray * attrsArray; @property (nonatomic, strong) NSMutableArray * columnHeights; @property (nonatomic, assign) CGFloat contentHeight; @property (nonatomic, assign) CGFloat stickHight; /* * 行高 */ - (CGFloat)rowMargin; /* * 左右边距 */ - (CGFloat)columnMargin; /* * 数量 */ - (NSInteger)columnCount; /* * 内距 */ - (UIEdgeInsets)edgeInsets; @end @implementation FHWaterFallFlowLayout #pragma mark - 常见数据处理 - (CGFloat)rowMargin { if ([self.delegate respondsToSelector:@selector(rowMarginInWaterflowLayout:)]) { return [self.delegate rowMarginInWaterflowLayout:self]; } else { return BDefaultRowMargin; } } - (CGFloat)columnMargin { if ([self.delegate respondsToSelector:@selector(columnMarginInWaterflowLayout:)]) { return [self.delegate columnMarginInWaterflowLayout:self]; } else { return BDefaultColumnMargin; } } - (NSInteger)columnCount { if ([self.delegate respondsToSelector:@selector(columnCountInWaterflowLayout:)]) { return [self.delegate columnCountInWaterflowLayout:self]; } else { return BDefaultColumnCount; } } - (UIEdgeInsets)edgeInsets { if ([self.delegate respondsToSelector:@selector(edgeInsetsInWaterflowLayout:)]) { return [self.delegate edgeInsetsInWaterflowLayout:self]; } else { return BDefaultEdgeInsets; } } /* * 初始化 */ - (void)prepareLayout { [super prepareLayout]; self.contentHeight = 0; //清除以前计算的所以高度 [self.columnHeights removeAllObjects]; //默认高度 for (NSInteger i = 0; i < self.columnCount; i++) { [self.columnHeights addObject:@(self.edgeInsets.top)]; } /* *清除之前所以的布局属性,此处最为关键,每次更新布局一定要刷新 */ [self.attrsArray removeAllObjects]; //数组 (存放所以cell的布局属性) NSLog(@"[self.collectionView numberOfSections]:%ld", [self.collectionView numberOfSections]); if(0 == [self.collectionView numberOfSections]) { return; } // UICollectionViewLayoutAttributes *headerAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; // [self.attrsArray addObject:headerAttrs]; //开始创建每一个cell对应的布局属性 NSInteger count = [self.collectionView numberOfItemsInSection:0]; for (int i = 0; i < count; i++) { //创建位置 NSIndexPath * indexPath = [NSIndexPath indexPathForItem:i inSection:0]; //获取cell布局属性 UICollectionViewLayoutAttributes * attrs = [self layoutAttributesForItemAtIndexPath:indexPath]; [self.attrsArray addObject:attrs]; } } -(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect { [self sectionHeaderStickCounter]; return self.attrsArray; } #pragma mark - Header停留算法 - (void)sectionHeaderStickCounter { self.stickHight = 50; for (UICollectionViewLayoutAttributes *layoutAttributes in self.attrsArray) { if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) { //这里第1个cell是根据自己存放attribute的数组而定的 // UICollectionViewLayoutAttributes *firstCellAttrs =self.attrsArray[0]; CGFloat headerHeight =CGRectGetHeight(layoutAttributes.frame); CGPoint origin = layoutAttributes.frame.origin; // 悬停临界点的计算,self.stickHight默认设置为64 if (headerHeight-self.collectionView.contentOffset.y <= self.stickHight) { origin.y =self.collectionView.contentOffset.y - (headerHeight - self.stickHight); } CGFloat width = layoutAttributes.frame.size.width; layoutAttributes.zIndex =2048;//设置一个比cell的zIndex大的值 layoutAttributes.frame = (CGRect){ .origin = origin, .size =CGSizeMake(width, layoutAttributes.frame.size.height) }; } } } //不设置这里看不到悬停 - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { return YES; } //设置cell的布局属性 -(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes * attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; //设置布局属性的frame CGFloat collectionViewW = self.collectionView.frame.size.width; CGFloat w = (collectionViewW - self.edgeInsets.left - self.edgeInsets.right - (self.columnCount - 1)*self.columnMargin) / self.columnCount; CGFloat h = [self.delegate waterflowLayout:self heightForItemAtIndex:indexPath.item itemWidth:w]; NSInteger destColumn = 0; CGFloat minColumnHeight = [self.columnHeights[0] doubleValue]; for (NSInteger i = 1; i < self.columnCount; i++) { CGFloat columnHeight = [self.columnHeights[i] doubleValue]; if (minColumnHeight > columnHeight) { minColumnHeight = columnHeight; destColumn = i; } } CGFloat x = self.edgeInsets.left + destColumn * (w +self.columnMargin); CGFloat y = minColumnHeight; if (y != self.rowMargin) { y += self.rowMargin; } attrs.frame = CGRectMake(x, y, w, h); //更新最短那列的高度 self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame)); // 记录内容的高度 CGFloat columnHeight = [self.columnHeights[destColumn] doubleValue]; if (self.contentHeight < columnHeight) { self.contentHeight = columnHeight; } return attrs; } - (CGSize)collectionViewContentSize { return CGSizeMake(0, self.contentHeight + self.edgeInsets.bottom); } //- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { // //header // if ([UICollectionElementKindSectionHeader isEqualToString:elementKind]) { // UICollectionViewLayoutAttributes *attri = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader withIndexPath:indexPath]; // //size // CGSize size = CGSizeZero; // if ([self.delegate respondsToSelector:@selector(collectionView:layout:referenceSizeForHeaderInSection:)]) { // size = [self.delegate collectionView:self.collectionView layout:self referenceSizeForHeaderInSection:indexPath.section]; // } // for (int i=0; i<self.columnHeights.count; i++) { // if(i == indexPath.section) // { // self.columnHeights[i] = @(self.edgeInsets.top+size.height); // } // } // attri.frame = CGRectMake(0, 0, size.width , size.height); // return attri; // } // else // { UICollectionViewLayoutAttributes *attri = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter withIndexPath:indexPath]; //size CGSize size = CGSizeZero; if ([self.delegate respondsToSelector:@selector(collectionView:layout:referenceSizeForFooterInSection:)]) { size = [self.delegate collectionView:self.collectionView layout:self referenceSizeForFooterInSection:indexPath.section]; } for (int i=0; i<self.columnHeights.count; i++) { self.columnHeights[i] = @(self.edgeInsets.top+size.height); } attri.frame = CGRectMake(0, 0, size.width , size.height); return attri; // } // return nil; //} #pragma mark - 懒加载 -(NSMutableArray *)attrsArray { if (!_attrsArray) { _attrsArray = [NSMutableArray array]; } return _attrsArray; } -(NSMutableArray *)columnHeights { if (!_columnHeights) { _columnHeights = [NSMutableArray array]; } return _columnHeights; } @end
使用代码:
- (UICollectionView *)collectionView { if(!_collectionView) { //创建布局 FHWaterFallFlowLayout *flowLayout = [[FHWaterFallFlowLayout alloc]init]; flowLayout.delegate = self; //创建CollectionView _collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(5, 0, kUIScreenWidth-10+BG_1PX, FULL_HEIGHT -(kStatusBarHeight+ TABBAR_HEIGHT+18+5.0+40+48)) collectionViewLayout:flowLayout]; _collectionView.dataSource = self; _collectionView.delegate = self; _collectionView.showsHorizontalScrollIndicator = NO; _collectionView.showsVerticalScrollIndicator = YES; _collectionView.alwaysBounceVertical = YES; if (@available(iOS 11.0, *)) { _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } _collectionView.backgroundColor = [UIColor clearColor];//RGBA(246, 246, 246, 1);//BGColorHex(F9F9F9); [_collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"lineFootView"]; [_collectionView registerClass:[FHVideoCell class] forCellWithReuseIdentifier:NSStringFromClass([FHVideoCell class])]; [_collectionView registerClass:[FHImageTextCell class] forCellWithReuseIdentifier:NSStringFromClass([FHImageTextCell class])]; [_collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"collectionHeadView"]; @weakify(self); [self.collectionView addRefreshHandler:^{ @strongify(self); if(self.headerRefreshBlock) { self.headerRefreshBlock(); } } completeHandler:^{ @strongify(self); }]; [self.collectionView addBITPullToLoadMoreWithActionHandler:^{ @strongify(self); if(self.footerRefreshBlock) { self.footerRefreshBlock(); } }]; self.collectionView.showsBITPullToLoadMore = NO; } return _collectionView; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { FHFollowListUnitEntity *entity = [self.models bitobjectOrNilAtIndex:indexPath.item]; FHVideoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([FHVideoCell class]) forIndexPath:indexPath]; cell.model = entity; return cell; } #pragma mark - <WaterflowLayoutDelegate> //页面高度 - (CGFloat)waterflowLayout:(FHWaterFallFlowLayout *)waterflowLayout heightForItemAtIndex:(NSUInteger)index itemWidth:(CGFloat)itemWidth { FHFollowListUnitEntity *entity = [self.models bitobjectOrNilAtIndex:index]; if(entity && [entity isKindOfClass:[FHFollowListUnitEntity class]] && entity.cellHeight > 0) { return entity.cellHeight; } return BG_1PX; } //瀑布流列数 - (CGFloat)columnCountInWaterflowLayout:(FHWaterFallFlowLayout *)waterflowLayout { return 2; } - (CGFloat)columnMarginInWaterflowLayout:(FHWaterFallFlowLayout *)waterflowLayout { return 0; } - (CGFloat)rowMarginInWaterflowLayout:(FHWaterFallFlowLayout *)waterflowLayout { return 0; } - (UIEdgeInsets)edgeInsetsInWaterflowLayout:(FHWaterFallFlowLayout *)waterflowLayout { return UIEdgeInsetsMake(0, 0, 0, 0); }
请求与数据请求处理代码:
-(void)loadListData { if(GBCommonStatusThird == self.listEntity.commonStatus) { [self getInspiration]; return; } CHECK_JUMP_LOGIN @weakify(self); self.listEntity.isSendindRequest = YES; [NetWorkManager postWithPath:@"Index/inspiration" param:@{@"typeid":@(1),@"p":@(self.villageListView.listEntity.p)} dataClass:[FHFollowListUnitEntity class] isArray:YES].complete = ^(DYNetModel * _Nullable net, NSArray *data) { @strongify(self); self.isNeedFresh = NO; self.listEntity.isSendindRequest = NO; [self endAnimating]; if (net.errcode == DYNetModelStateSuccess) { if (self.villageListView.listEntity.p <= 1) { [self.villageListView.models removeAllObjects]; } if (data.count == 0) { self.villageListView.collectionView.showsBITPullToLoadMore = NO; if(self.villageListView.listEntity.p >1 ) { self.villageListView.listEntity.p--; } else { self.villageListView.listEntity.p = 1; } } else { self.villageListView.collectionView.showsBITPullToLoadMore = YES; self.villageListView.listEntity.maxCellHeight = [self updateCellHeightWithArr:data maxCellHeight:self.villageListView.listEntity.maxCellHeight]; if(data.count < self.inspirationListView.listEntity.p) { self.villageListView.collectionView.showsBITPullToLoadMore = NO; } else { self.villageListView.collectionView.showsBITPullToLoadMore = YES; } [self.villageListView.models addObjectsFromSafeArray:data]; } if(isCommonUnitEmptyArray(self.villageListView.models)) { [self displayNoData]; } else { [self.villageListView.collectionView reloadData]; self.villageListView.collectionView.hidden = NO; } } else if((DYNetModelStateAlreadyAdd == net.errcode) && !isCommonUnitEmptyString(net.message) && [net.message isEqualToString:@"完善房屋信息"]) { [self improveHousingInformationWithSelectCollectionView:self.villageListView.collectionView]; self.isNeedFresh = YES; } else { [self loadFailWithSelectCollectionView:self.villageListView.collectionView]; } }; } -(CGFloat )updateCellHeightWithArr:(NSArray *)arr maxCellHeight:(CGFloat)maxCellHeight { CGFloat maxHeight = 4; if(maxHeight < maxCellHeight) { maxHeight = maxCellHeight; } if(!isCommonUnitEmptyArray(arr)) { for(FHFollowListUnitEntity *entity in arr) { if(entity && [entity isKindOfClass:[FHFollowListUnitEntity class]]) { NSString *content = entity.content; if(isCommonUnitEmptyString(content)) { content = @" "; } CGFloat width = (kUIScreenWidth-10*3)/2+10; //(FULL_WIDTH -2*COMMON_SMALL_EDGE_DISTANCE-10.0)/2.0; if(1 == entity.typeid) { entity.cellHeight = 82+130*(kUIScreenWidth-10*3)/2/173; } else if(2 == entity.typeid) { if(entity.img_width > 0 && entity.img_height > 0) { entity.cellHeight = width*entity.img_height/entity.img_width+82; } else { entity.cellHeight = 333; } } else { entity.cellHeight = 126+130*(kUIScreenWidth-10*3)/2/173; } if(maxHeight < entity.cellHeight) { maxHeight = entity.cellHeight; } } } } return maxHeight; }
方法一的实现代码:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ FHFollowListUnitEntity *entity = [self.models bitobjectOrNilAtIndex:indexPath.item]; FHRecommendVideoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([FHRecommendVideoCell class]) forIndexPath:indexPath]; cell.model = entity; NSInteger remainder=indexPath.item % 2; NSInteger currentRow=indexPath.item / 2; CGFloat currentHeight= entity.cellHeight; //[self.heightArr[indexPath.row] floatValue]; CGFloat positonX=((FULL_WIDTH -2*(COMMON_EDGE_DISTANCE-4.0)-1.0)/2.0)*remainder+1.0*(remainder); CGFloat positionY=(currentRow+1)*0; for (NSInteger i=0; i<currentRow; i++) { NSInteger position=remainder+i*2; positionY+= ((FHFollowListUnitEntity *)[self.models bitobjectOrNilAtIndex:position]).cellHeight; } cell.frame = CGRectMake(positonX, positionY,(FULL_WIDTH -2*(COMMON_EDGE_DISTANCE-4.0)-1.0)/2.0,currentHeight) ; cell.type = self.type; cell.delegate = self; return cell; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { FHFollowListUnitEntity *entity = [self.models bitobjectOrNilAtIndex:indexPath.item]; if(entity && [entity isKindOfClass:[FHFollowListUnitEntity class]] && entity.cellHeight > 0) { return CGSizeMake((FULL_WIDTH -2*(COMMON_EDGE_DISTANCE-4.0)-1.0)/2.0, (entity.cellHeight)); } return CGSizeMake((FULL_WIDTH -2*(COMMON_EDGE_DISTANCE-4.0)-1.0)/2.0, (327)); } -(void)updateCellHeightWithArr:(NSArray *)arr { if(!isCommonUnitEmptyArray(arr)) { for(FHFollowListUnitEntity *entity in arr) { if(entity && [entity isKindOfClass:[FHFollowListUnitEntity class]]) { NSString *content = entity.content; // content = @"现代简约风是包容性最强的一种装修风格"; if(isCommonUnitEmptyString(content)) { content = @" "; } CGFloat width = (FULL_WIDTH -2*COMMON_EDGE_DISTANCE-9.0)/2.0; NSDictionary *attributes = @{NSFontAttributeName :DYFont(15)}; //字体属性,设置字体的font CGSize maxSize = CGSizeMake(width-8*2, MAXFLOAT); //设置字符串的宽高 CGSize size = [content boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size; CGFloat contentHeight = size.height; if(size.height > DYBaseSize(15)*2+10) { contentHeight = DYBaseSize(15)+8.5; } if(entity.img_width > 0 && entity.img_height > 0) { entity.cellHeight = 4+width*entity.img_height/entity.img_width+8 + contentHeight+8+28.0+4; } else { entity.cellHeight = 4 + 231 + 8 + contentHeight+8+28.0+4; } } } } } - (void)getVideoList { @weakify(self); NSArray *types = @[@"1",@"2"]; [NetWorkManager postWithPath:@"Dyc/getVideoList" param:@{@"typeid":types[self.type],@"p":@(self.listEntity.p),@"keyword":@""} dataClass:[FHFollowListUnitEntity class] isArray:YES].complete = ^(DYNetModel * _Nullable net, NSArray *data) { @strongify(self); self.listEntity.isSendindRequest = NO; [self.collectionView.refreshView endAnimating]; [self.collectionView.bitPullToLoadMoreView endAnimating]; if (net.errcode == DYNetModelStateSuccess) { if (self.listEntity.p <= 1) { [self.models removeAllObjects]; } if (data.count == 0) { self.collectionView.showsBITPullToLoadMore = NO; if(self.listEntity.p >1 ) { self.listEntity.p--; } else{ self.listEntity.p = 1; } } else{ self.collectionView.showsBITPullToLoadMore = YES; [self updateCellHeightWithArr:data]; [self.models addObjectsFromSafeArray:data]; } if(isCommonUnitEmptyArray(self.models)) { [self.models removeAllObjects]; [self displayNoData]; } else { [self hideTipsView]; self.collectionView.hidden = NO; [self.collectionView reloadData]; } } else { self.collectionView.hidden = YES; [self showLoadFailTipsViewWithFrame:CGRectMake(0, 0, kUIScreenWidth, FULL_HEIGHT- kStatusBarHeight- 44) topShift:-(kStatusBarHeight+44)/2.0]; [self.collectionView reloadData]; } }; }