ios瀑布流,我们已经解除过蛮多的吧,但是大家基本上都是网上找demo,然后改改参数就拿过来自己用了吧,这次带大家手动制作一个瀑布流,顺便也让大家学习下瀑布流的原理,好了,话不多说,动起手来吧
搞起!!!!
效果图:
![](https://img.haomeiwen.com/i13273079/961b8f6a3ee179cb.png)
看着以上的效果图,先给大家讲一下设计思路,大家先把思路理解通透了,这样大家才有可下手的点,对吧。
首先我们可以看到,图中的方块分为3列,属于等宽不等高系列,即将出现的方块,总是在已经出现了的所有方块的最顶部(图1中,4的出现紧跟着2,因为方块2的底部处于最上方)
看图
我们可以记录这三列中每一列最下边的方块的底部y值,当需要出现下一个方块的时候,我们判断下y1,y2,y3中最小的值,然后将出现的方块放在最小的y值下面,然后更新对应列的y值,这样每当出现新方块的时候,我们都找准y值最小的那一列,然后将新方块插入。这样思路是不是就很清楚了呢。
看图说明(结合例子说明)
照着上面的思路,我们用图1的例子来讲解此方法
首先初始化y1,y2,y3的值均为0,
同时设置一个maxY=0 , 来记录所有方块中最低的那个方块的y值,方便下一个footerView和headerView设置frame的y值
检测是否存在头部视图,图中有头部视图Header(高度为30),所有我们的
y1,y2,y3 = 30
maxY=30
插入方块1(高度为100)的时候:
此时 y1 = 30 , y2= 30,y3= 30
这时候我们检测到y1最小(如果有相同的值,默认从左到右),这时候我们将方块一插入,更新y1的值,y1 = 130;
maxY=130
插入方块2(高度为60)的时候:
此时 y1 = 130 , y2= 30,y3= 30
这时候我们检测到y2最小(如果有相同的值,默认从左到右),这时候我们将方块2插入,更新y2的值,y2 = 90;
maxY=130
插入方块3(高度为120)的时候:
此时 y1 = 130 , y2= 90,y3= 30
这时候我们检测到y3最小,这时候我们将方块3插入,更新y3的值,y3 = 150;
maxY=150
插入方块4(高度为100)的时候:
此时 y1 = 130 , y2= 90,y3= 150
这时候我们检测到y2最小,这时候我们将方块4插入,更新y2的值,y2 = 190;
maxY=190
插入方块5(高度为100)的时候:
此时 y1 = 130 , y2= 190,y3= 150
这时候我们检测到y1最小,这时候我们将方块5插入,更新y1的值,y1 = 230;
maxY=230
.
.
.
.
以此类推
当一个section中的cell出现完毕的时候,出现footerview的时候,我们将footerview的frame.y设置为均maxY,即为230,
!!!!
这里我们要把y1,y2,y3的值均设置为maxY=230,重新开始下一轮section的排布。
..
..
..
这样大家是不是非常好理解了呀,因为我们知道了每个cell和headerview和footerview的frame.y的值,至于cell的frame.x的值当然也很好获取,新插入的方块在第几列,我们就把x设置成那一列的x即可、
好了 ,思路就给大家讲到这里了,感觉大家是不是都跃跃欲试了呀,恨不得马上就开始码起来了呢。
正文(代码构思)
.
首先我们先自定义一个JWaterFlowLayout继承于UICollectionViewFlowLayout,然后我们在此文件中做cell的布局设置。
基于上述的思路,我们首先要为JWaterFlowLayout,设置相应的属性配置。
/// 所有方块的布局属性
@property (nonatomic ,strong)NSMutableArray* attrsArray;
/// 记录所有方块中最底部的cell.frame.y + cell.size.frame.height
@property (nonatomic ,assign)CGFloat maxBottomY;
/// 每一列中最底部方块的cell.frame.y + cell.size.frame.height
@property (nonatomic ,strong)NSMutableArray* allCloumnBottomY_Arr;
/// 总列数
@property (nonatomic, assign)NSInteger cloumnNum;
///行间距
@property (nonatomic, assign)CGFloat rowMargin;
///列间距
@property (nonatomic, assign)CGFloat cloumnMargin;
//边缘边距
@property (nonatomic, assign)UIEdgeInsets cellEdgInset;
当然这些属性,我们只希望存在于.m文件中,并不暴露给.h文件,所以我们可以在.h文件中,定一个协议,并让协议来实现这些配置的赋值,并设置代理。
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class JWaterFlowLauput;
@protocol JWaterFlowLayoutDelegate <NSObject>
@required //必须要实现的方法
/// 设置列数
/// @param flowLayout flowLayout
- (NSInteger)cloumnCountInWaterFlowLayout:(JWaterFlowLauput*)flowLayout;
@optional //可选方法
/// 配置cell的边缘间距
/// @param flowLayout flowLayout
- (UIEdgeInsets)cellEdgeInsetsInWaterFlowLayout:(JWaterFlowLauput*)flowLayout;
/// 配置cell的行间距
/// @param flowLayout flowLayout
- (CGFloat)cellRowMarginInWaterFlowLayout:(JWaterFlowLauput*)flowLayout;
/// 配置cell的列间距
/// @param flowLayout flowLayout description
- (CGFloat)cellCloumnMarginInWaterFlowLayout:(JWaterFlowLauput*)flowLayout;
@end
@interface JWaterFlowLauput : UICollectionViewFlowLayout
//代理
@property (nonatomic , weak) id<JWaterFlowLayoutDelegate> delegate;
@end
这样的话,我们可以在外层,进行属性配置,当然我们在协议中,设置了可选的实现方法,那么即表示,当代理并没有实现此方法的时候,我们需要给这些属性值设置一个默认值,那么接下来,我们在.m文件中来创建一些默认值。
#import "JWaterFlowLauput.h"
/**默认列数*/
static const NSInteger JDefaultCloumnNum = 2 ;
/**默认cell之间的行间距*/
static const CGFloat JDefaultRowMargin = 10 ;
/**默认cell之间的列间距*/
static const CGFloat JDefaultCloumnMargin = 10 ;
/**默认cell之间的列间距*/
static const UIEdgeInsets JDefaultCellEdgInset = {10, 10, 10, 10};
然后我们在getter方法中来获取这些参数值,
// 属性配置
- (NSInteger)cloumnNum{
return [self.delegate cloumnCountInWaterFlowLayout:self];
}
- (UIEdgeInsets)cellEdgInset{
if([self.delegate respondsToSelector:@selector(cellEdgeInsetsInWaterFlowLayout:)]){
return [self.delegate cellEdgeInsetsInWaterFlowLayout:self];
}else{
return JDefaultCellEdgInset;
}
}
- (CGFloat)rowMargin{
if([self.delegate respondsToSelector:@selector(cellRowMarginInWaterFlowLayout:)]){
return [self.delegate cellRowMarginInWaterFlowLayout:self];
}else{
return JDefaultRowMargin;
}
}
- (CGFloat)cloumnMargin{
if([self.delegate respondsToSelector:@selector(cellCloumnMarginInWaterFlowLayout:)]){
return [self.delegate cellCloumnMarginInWaterFlowLayout:self];
}else{
return JDefaultCloumnMargin;
}
}
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
return self.attrsArray;
}
然后开始本文中的重点核心代码吧
重写prepareLayout
/// 重写-prepareLayout-方法
- (void)prepareLayout{
[super prepareLayout];
//初始化参数
self.maxBottomY = 0;
[self.allCloumnBottomY_Arr removeAllObjects];
[self.attrsArray removeAllObjects];
//给每一列添加对应的顶部y值
for (NSInteger i = 0; i < self.cloumnNum; i ++) {
[self.allCloumnBottomY_Arr addObject:@(self.cellEdgInset.top)];
}
//huo每一个cell的attrs
for (NSInteger sec = 0; sec < self.sectionNum; sec ++) {
//获取每一个sction中的cell的总h个数
NSInteger rowNum = [self.collectionView numberOfItemsInSection:sec];
for (NSInteger row = 0 ; row < rowNum; row ++) {
UICollectionViewLayoutAttributes * attr = [self.collectionView layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:row inSection:sec]];
[self.attrsArray addObject:attr];
}
}
}
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
return self.attrsArray;
}
- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
//创建空的attrs
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
//初始化frame,将最后的frame赋值e给cell。
CGRect finalFrame = CGRectZero;
CGFloat x = 0;//cell的otigin.x
CGFloat y = 0;//cell的otigin.y
CGFloat w = 0;//cell的size.width
CGFloat h = 0;//cell的size.height
//首先获取每个cell的宽度
w = (self.collectionView.frame.size.width - self.cellEdgInset.left - self.cellEdgInset.right - (self.cloumnNum - 1)*self.rowMargin)/self.cloumnNum;
//cell的高度
h = [self.delegate itemSizeInWaterFlowLayout:self indexPath:indexPath].height;
//接着我们找出所有列中高度最短的那一列出来
NSInteger minColumnIndex = 0;
CGFloat minBottom_Y = [self.allCloumnBottomY_Arr[0] doubleValue];
for (NSInteger cloumnIndex = 0; cloumnIndex < self.allCloumnBottomY_Arr.count; cloumnIndex ++) {
CGFloat indexBottom_Y = [self.allCloumnBottomY_Arr[cloumnIndex] doubleValue];
if(indexBottom_Y < minBottom_Y){//
minColumnIndex = cloumnIndex;
minBottom_Y = indexBottom_Y;
}
}
//接着取到了最短的那一列之后,就可以得到cell的x值
x = self.cellEdgInset.left + (w + self.rowMargin) * minColumnIndex;
//接着把cell添加到最短那一列的下面,记住,别忘记了列间距
y = minBottom_Y + self.cloumnMargin ;
//最后cell的frame确定了
finalFrame = CGRectMake(x, y, w, h);
//这时候我们需要更新一下每一列的最底部的距离
self.allCloumnBottomY_Arr[minColumnIndex] = @(CGRectGetMaxY(finalFrame));
//同时记录一下 所有方块中最底部的y值,未必就是刚刚加上的方块的最底部
if(self.maxBottomY < [self.allCloumnBottomY_Arr[minColumnIndex] doubleValue]){
self.maxBottomY = [self.allCloumnBottomY_Arr[minColumnIndex] doubleValue];
}
attrs.frame = finalFrame;
return attrs;
}
- (CGSize)collectionViewContentSize{
return CGSizeMake(0, self.maxBottomY + self.cellEdgInset.bottom);
}
可能大家看下来会发现,我们代码中似乎并没有用到maxBottomY参数,这是因为我们暂时没有做头部视图和尾部视图的考虑。后面会带大家做进阶。大家慢慢看
好了 ,接下来 我们去创建collectionview来看看我们实现的效果吧
#import "ViewController.h"
#import "JWaterFlowLauput.h"
@interface ViewController ()<JWaterFlowLayoutDelegate,UICollectionViewDelegate,UICollectionViewDataSource>
@property(nonatomic ,strong)UICollectionView* collectionView;
@property(nonatomic ,strong)JWaterFlowLauput* flowLayout;
@property(nonatomic ,strong)NSMutableArray* dataSource;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
[self initDatas];
_flowLayout = [[JWaterFlowLauput alloc] init];
_flowLayout.delegate = self;
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) collectionViewLayout:_flowLayout];
_collectionView.backgroundColor = [UIColor whiteColor];
_collectionView.delegate = self;
_collectionView.dataSource = self;
[_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellIDDD"];
[self.view addSubview:_collectionView];
// Do any additional setup after loading the view.
}
- (void)initDatas{
self.dataSource = [NSMutableArray array];
for (int i = 0 ; i < 30 ; i ++) {
[self.dataSource addObject:@(arc4random()%200 + 30)];
}
}
/// 设置总section数
/// @param flowLayout flowLayout
- (NSInteger)secionCountInWaterFlowLayout:(JWaterFlowLauput*)flowLayout{
return 1;
}
/// 设置列数
/// @param flowLayout flowLayout
- (NSInteger)cloumnCountInWaterFlowLayout:(JWaterFlowLauput*)flowLayout{
return 3;
}
/// cell的size
/// @param flowLayout flowLayout
- (CGSize)itemSizeInWaterFlowLayout:(JWaterFlowLauput*)flowLayout indexPath:(NSIndexPath*)indexPath{
return CGSizeMake(self.view.frame.size.width / 3,[self.dataSource[indexPath.row] floatValue]);
}
/// 配置cell的边缘间距
/// @param flowLayout flowLayout
- (UIEdgeInsets)cellEdgeInsetsInWaterFlowLayout:(JWaterFlowLauput*)flowLayout{
return UIEdgeInsetsMake(10, 10, 10, 10);
}
/// 配置cell的行间距
/// @param flowLayout flowLayout
- (CGFloat)cellRowMarginInWaterFlowLayout:(JWaterFlowLauput*)flowLayout{
return 10;
}
/// 配置cell的列间距
/// @param flowLayout flowLayout description
- (CGFloat)cellCloumnMarginInWaterFlowLayout:(JWaterFlowLauput*)flowLayout{
return 10;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return self.dataSource.count;
}
// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath:
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellIDDD" forIndexPath:indexPath];
cell.backgroundColor = [UIColor colorWithRed:arc4random()%255/255.0 green:arc4random()%255/255.0 blue:arc4random()%255/255.0 alpha:1.0];
UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(10, 10, 50, 20)];
label.text = [@(indexPath.row + 1) stringValue];
label.textColor = [UIColor whiteColor];
[cell addSubview:label];
return cell;
}
@end
好了 ,实践出真知,把项目运行起来吧。。
![](https://img.haomeiwen.com/i13273079/ec13fe08e2458417.png)
是不是完美实现了呢 ,😝 ,就像个200斤的胖子一样开行。
------这里是分割线---------------
进阶
我们现在的项目中并没有出现头部和尾部,接下来,我们修改下之前的方法,为他们添加下头部和尾部视图吧。
这里我们之前定义的maxBottomY的用场就派上了,仔细往下瞧吧
首先我们修改下协议方法:
新增头部视图和尾部视图的代理方法
@optional //可选方法
/// 配置cell的边缘间距
/// @param flowLayout flowLayout
- (UIEdgeInsets)cellEdgeInsetsInWaterFlowLayout:(JWaterFlowLauput*)flowLayout;
/// 配置cell的行间距
/// @param flowLayout flowLayout
- (CGFloat)cellRowMarginInWaterFlowLayout:(JWaterFlowLauput*)flowLayout;
/// 配置cell的列间距
/// @param flowLayout flowLayout description
- (CGFloat)cellCloumnMarginInWaterFlowLayout:(JWaterFlowLauput*)flowLayout;
/// 配置section的头部视图
/// @param flowLayout flowLayout
/// @param indexPath indexPath
- (CGSize)headerViewSizeInInWaterFlowLayout:(JWaterFlowLauput*)flowLayout indexPath:(NSIndexPath*)indexPath;
/// 配置section的尾部视图
/// @param flowLayout fla
/// @param indexPath zz
- (CGSize)footerViewSizeInInWaterFlowLayout:(JWaterFlowLauput*)flowLayout indexPath:(NSIndexPath*)indexPath;
@end
接下来重新修改下prepareLayout方法
/// 重写-prepareLayout-方法
- (void)prepareLayout{
[super prepareLayout];
//初始化参数
self.maxBottomY = 0;
[self.allCloumnBottomY_Arr removeAllObjects];
[self.attrsArray removeAllObjects];
//给每一列添加对应的顶部y值
for (NSInteger i = 0; i < self.cloumnNum; i ++) {
[self.allCloumnBottomY_Arr addObject:@(self.cellEdgInset.top)];
}
//huo每一个cell的attrs
for (NSInteger sec = 0; sec < self.sectionNum; sec ++) {
//如果代理中实现了头部视图的方法,则代表存在头部视图,那么需要把headerview的attrs也添加进数组
if([self.delegate respondsToSelector:@selector(headerViewSizeInInWaterFlowLayout:indexPath:)]){
UICollectionViewLayoutAttributes * headerAttr = [self.collectionView layoutAttributesForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:sec]];
[self.attrsArray addObject:headerAttr];
}
//获取每一个sction中的cell的总h个数
NSInteger rowNum = [self.collectionView numberOfItemsInSection:sec];
for (NSInteger row = 0 ; row < rowNum; row ++) {
UICollectionViewLayoutAttributes * attr = [self.collectionView layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:row inSection:sec]];
[self.attrsArray addObject:attr];
}
//如果代理中实现了尾部视图的方法,则代表存在头部视图,那么需要把fotterview的attrs也添加进数组
if([self.delegate respondsToSelector:@selector(footerViewSizeInInWaterFlowLayout:indexPath:)]){
UICollectionViewLayoutAttributes * footAttr = [self.collectionView layoutAttributesForSupplementaryElementOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:sec]];
[self.attrsArray addObject:footAttr];
}
}
}
接下来实现头部和尾部的attrs
//实现头部和尾部的attrs,并且返回
- (UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath{
UICollectionViewLayoutAttributes *attrs;
//初始化frame,将最后的frame赋值e给头部或者尾部
CGRect finalFrame = CGRectZero;
CGFloat x = 0;//view的otigin.x
CGFloat y = self.maxBottomY + self.cloumnMargin;//view的otigin.y
CGFloat w = 0;
CGFloat h = 0;//view的size.height
if([elementKind isEqualToString:UICollectionElementKindSectionHeader]){
w = [self.delegate headerViewSizeInInWaterFlowLayout:self indexPath:indexPath].width;
h = [self.delegate headerViewSizeInInWaterFlowLayout:self indexPath:indexPath].height;
//更新maxY的值
self.maxBottomY = y + h ;
//更新每一列的最底部的值
for (NSInteger i = 0; i < self.allCloumnBottomY_Arr.count; i ++) {
self.allCloumnBottomY_Arr[i] = @( self.maxBottomY);
}
}else{
w = [self.delegate footerViewSizeInInWaterFlowLayout:self indexPath:indexPath].width;
h = [self.delegate footerViewSizeInInWaterFlowLayout:self indexPath:indexPath].height;
//更新maxY的值
self.maxBottomY = y + h ;
//更新每一列的最底部的值
for (NSInteger i = 0; i < self.allCloumnBottomY_Arr.count; i ++) {
self.allCloumnBottomY_Arr[i] = @( self.maxBottomY);
}
}
finalFrame = CGRectMake(x, y, w, h);
attrs.frame = finalFrame;
return attrs;
}
好了 添加完之后,我们试着往viewcontroller中添加一下头部和尾部吧
#pragma mark 配置头部和尾部
/// 配置section的头部视图
/// @param flowLayout flowLayout
/// @param indexPath indexPath
- (CGSize)headerViewSizeInInWaterFlowLayout:(JWaterFlowLauput*)flowLayout indexPath:(NSIndexPath*)indexPath{
return CGSizeMake(self.collectionView.frame.size.width, 40);
}
/// 配置section的尾部视图
/// @param flowLayout fla
/// @param indexPath zz
- (CGSize)footerViewSizeInInWaterFlowLayout:(JWaterFlowLauput*)flowLayout indexPath:(NSIndexPath*)indexPath{
return CGSizeMake(self.collectionView.frame.size.width, 50);
}
- (UICollectionReusableView*)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
UICollectionReusableView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"sectionHeader" forIndexPath:indexPath];
headerView.backgroundColor = [UIColor greenColor];
return headerView;
}else{
UICollectionReusableView *footerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"sectionFoot" forIndexPath:indexPath];
footerView.backgroundColor = [UIColor yellowColor];
return footerView;
}
}
然后我们把datasoure的个数改成10个 ,然后运行看看效果吧:
![](https://img.haomeiwen.com/i13273079/f450c721fa879cf2.png)
掌声响起,demo我就不发了,重在理解和自己亲手制作。 感谢各位
项目下载地址
git下载地址
网友评论