美文网首页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瀑布流,没你想象得那么难

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