瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布局的网站是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];
}
};
}