美文网首页Collection view
iOS瀑布流,没你想象得那么难

iOS瀑布流,没你想象得那么难

作者: Mr姜饼 | 来源:发表于2020-05-09 17:29 被阅读0次

ios瀑布流,我们已经解除过蛮多的吧,但是大家基本上都是网上找demo,然后改改参数就拿过来自己用了吧,这次带大家手动制作一个瀑布流,顺便也让大家学习下瀑布流的原理,好了,话不多说,动起手来吧
搞起!!!!

效果图:


image.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

好了 ,实践出真知,把项目运行起来吧。。

image.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个 ,然后运行看看效果吧:

image.png

掌声响起,demo我就不发了,重在理解和自己亲手制作。 感谢各位

项目下载地址
git下载地址

相关文章

  • iOS瀑布流,没你想象得那么难

    ios瀑布流,我们已经解除过蛮多的吧,但是大家基本上都是网上找demo,然后改改参数就拿过来自己用了吧,这次带大家...

  • [565]帮助他人没你想象得那么难

    仔细观察了一下,很多的富人都喜欢拿自己的钱去帮助他人,有这么一些我们所熟知的名人,比如,比尔盖茨,他专门成立了一个...

  • iOS UICollectionView autolayout

    iOS UICollectionView autolayout UICollectionViewCell 瀑布流加...

  • 心流没那么难

    昨天开始用“番茄Todo”,效果比较明显。我竟然一刷《曾国藩传》结束。 设置一个番茄钟,把手机扔到拿不到的位置,拿...

  • 关于自定义瀑布流的布局(Swift)

    瀑布流 UICollectionView Swift 最近整理以前iOS开发中用到的功能代码,发觉对瀑布流的布局有...

  • iOS使用UICollectionView实现瀑布流

    UICollectionView实现瀑布流 在iOS中可以实现瀑布流的目前已知的有2种方案: 使用UIScroll...

  • 走出原生家庭难不难?其实没你想象得那么难!

    上个月底,我和李鲆、肖雪萍两位老师连麦,谈如何走出原生家庭。 结果听众太热情了,以至于我准备的内容,只分享了一点点...

  • :没你想象的那么难

    其实,你遇见的事,你不想面对的人,不想面对的事,没你想象的那么难。 一件事情,未做之前,未来临之前总是胆战心惊、忧...

  • 改变,没你想象那么难

    是什么事情触动了我的改变? 坐在回住处的公交上,我一直在思考这个问题。下午的面试,自己的表现不仅把老板吓了一跳,也...

  • iOS瀑布流

    说来惭愧,使用collectionView这么久了,还从来没自己写过瀑布流,废话不多说,先上效果图: 数据来源 数...

网友评论

    本文标题:iOS瀑布流,没你想象得那么难

    本文链接:https://www.haomeiwen.com/subject/pwpcdctx.html