纯代码实现瀑布流效果

版本记录

版本号 时间
V1.0 2017.04.16

前言

看过很多人写过瀑布流,最近项目中也用到了,所以自己看了一下实现原理,也写了一个demo,希望对大家能有帮助,下面会贴出全部代码,gitHub地址。

详细设计

还是先看一下文档结构。

纯代码实现瀑布流效果_第1张图片
文档结构

下面看详细的代码。

1. AppDelegate.m

#import "AppDelegate.h"
#import "JJWaterFlowCollectionVC.h"

@interface AppDelegate ()

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    JJWaterFlowCollectionVC *collectionVC = [[JJWaterFlowCollectionVC alloc] init];
    self.window.rootViewController = collectionVC;
    [self.window makeKeyAndVisible];
    
    return YES;
}

@end

2. JJWaterFlowCollectionVC.h

#import 

@interface JJWaterFlowCollectionVC : UICollectionViewController

@end

3.JJWaterFlowCollectionVC.m

#import "JJWaterFlowCollectionVC.h"
#import "JJWaterFlowLayout.h"
#import "JJWaterFlowCollectionCell.h"
#import "JJWaterFlowModel.h"
#import "JJWaterFlowFooterView.h"

@interface JJWaterFlowCollectionVC () 

@property (nonatomic, strong) NSMutableArray *shopData;
@property (nonatomic, strong) JJWaterFlowLayout *flowLayout;
@property (nonatomic, strong) JJWaterFlowFooterView *footerView;
@property (nonatomic, assign) NSInteger dataIndex;

@end

@implementation JJWaterFlowCollectionVC

static NSString * const reuseIdentifier = @"reuseIdentifierCell";
static NSString * const footerReuseIdentifier = @"footerReuseIdentifier";

#pragma mark - Override Base Function

- (instancetype)init
{
    self.flowLayout = [[JJWaterFlowLayout alloc] init];
    self.flowLayout.delegate = self;
    self.flowLayout.columnNum = 3;
    self.collectionView = [[UICollectionView alloc] initWithFrame:[UIScreen mainScreen].bounds collectionViewLayout:self.flowLayout];
    [self.collectionView registerClass:[JJWaterFlowCollectionCell class] forCellWithReuseIdentifier:reuseIdentifier];
    [self.collectionView registerClass:[JJWaterFlowFooterView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:footerReuseIdentifier];
    self.collectionView.backgroundColor = [UIColor whiteColor];
    
    self.shopData = [NSMutableArray array];
    [self loadData];
    return self;
}

#pragma mark - Object Private Function

- (void)loadData
{
    NSArray *dataArr = [JJWaterFlowModel waterFlowWithIndex:((self.dataIndex % 3) + 1)];
    [self.shopData addObjectsFromArray:dataArr];
    self.dataIndex++;
}

#pragma mark - JJWaterFlowLayoutDelegate

- (CGFloat)waterFlowLayout:(JJWaterFlowLayout *)flowLayout cellWidth:(CGFloat)cellWidth indexPath:(NSIndexPath *)indexPath
{
    JJWaterFlowModel *model = self.shopData[indexPath.item];
    return model.height / model.width * cellWidth;
}

#pragma mark - UICollectionViewDataSource

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return self.shopData.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    JJWaterFlowCollectionCell *waterFlowCell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    waterFlowCell.shopModel = self.shopData[indexPath.item];
    return waterFlowCell;
}

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
    JJWaterFlowFooterView *footerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:footerReuseIdentifier forIndexPath:indexPath];
    self.footerView = footerView;
    return footerView;

}

#pragma mark - UIScrollViewDelegate

//显示footerView时加载数据

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    //如果当前footerView没有显示或正在加载数据时直接返回
    if (!self.footerView || self.footerView.activityIndicatorView.isAnimating) {
        return;
    }
    
    //当offset.y + collectionView的高 > footerView的Y时开始加载数据
    if ((scrollView.contentOffset.y + scrollView.bounds.size.height) > CGRectGetMaxY(self.footerView.frame)) {
        //菊花旋转
        [self.footerView.activityIndicatorView startAnimating];
        //延时3秒,模拟加载网络
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //加载数据
            [self loadData];
            [self.footerView.activityIndicatorView stopAnimating];
            self.footerView = nil;
            [self.collectionView reloadData];
        });
    }

}


@end


4. JJWaterFlowModel.h
#import 

@interface JJWaterFlowModel : NSObject

@property (nonatomic, copy) NSString *icon;
@property (nonatomic, copy) NSString *price;
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;

+ (instancetype)waterFlowModelWithDict:(NSDictionary *)dict;

+ (NSArray *)waterFlowWithIndex:(NSInteger)index;

@end

5.JJWaterFlowModel.m
#import "JJWaterFlowModel.h"

@implementation JJWaterFlowModel

#pragma mark - Class Public Function

+ (instancetype)waterFlowModelWithDict:(NSDictionary *)dict{
    
    id model = [[self alloc] init];
    [model setValuesForKeysWithDictionary:dict];
    return model;
}

+ (NSArray *)waterFlowWithIndex:(NSInteger)index
{
    NSString *dataStr = [NSString stringWithFormat:@"%zd.plist",index];
    NSArray *dataArr = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:dataStr ofType:nil]];
    NSMutableArray *dataArrM = [NSMutableArray arrayWithCapacity:dataArr.count];
    
    [dataArr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        JJWaterFlowModel *model = [JJWaterFlowModel waterFlowModelWithDict:obj];
        [dataArrM addObject:model];
    }];
    
    return dataArrM.copy;
}

@end



6.JJWaterFlowLayout.h
#import 

@class JJWaterFlowLayout;

@protocol JJWaterFlowLayoutDelegate 

// 返回cell行高
- (CGFloat)waterFlowLayout:(JJWaterFlowLayout *)flowLayout cellWidth:(CGFloat)cellWidth indexPath:(NSIndexPath *)indexPath;

@end

@interface JJWaterFlowLayout : UICollectionViewFlowLayout

@property (nonatomic, assign) NSInteger columnNum;
@property (nonatomic, weak) iddelegate;

@end

7.JJWaterFlowLayout.m

#import "JJWaterFlowLayout.h"

@interface JJWaterFlowLayout ()

//记录每一列最大的Y"即当前这一列cell的总高
@property (nonatomic, strong) NSMutableArray *eachColumnHeightArrM;

//存放所有cell的布局属性
@property (nonatomic, strong) NSMutableArray *attrsArrM;

@end

@implementation JJWaterFlowLayout

#pragma mark - Override Base Function

- (instancetype)init
{
    if (self = [super init]) {
        self.sectionInset = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0);
        self.minimumLineSpacing = 5.0;
        self.minimumInteritemSpacing = 5.0;
        self.itemSize = CGSizeMake(30.0, 40.0);
        self.footerReferenceSize = CGSizeMake(50.0, 50.0);
        self.columnNum = 3;
        self.attrsArrM = [NSMutableArray array];
        self.eachColumnHeightArrM = [NSMutableArray arrayWithCapacity:self.columnNum];
        for (NSInteger i = 0; i < self.columnNum; i++) {
            self.eachColumnHeightArrM[i] = @(self.sectionInset.top);//设置默认高度
        }
    }
    return self;
}

- (void)prepareLayout
{
    [super prepareLayout];
    
    [self addAttributes];

}

//返回collectionView的布局属性
// 通过输出此方法的返回值,发现此方法返回的数组中是每一个itme"cell"的布局属性,里面有两个关键属性,一个是cell的索引,一个是cell的frame
// 1.此方法会计算当前显示区域中所有cell的布局属性,
// 2.一旦计算完成,所有的属性会被缓存起来,不会再次计算;
// 结论:我们可以手动来计算每一个cell的frame,并保到数组中,就应该可以实现瀑布流效果

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attrsArrM;
}

//创建cell的布局属性

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    //cell的尺寸
    CGFloat cellWidth = (self.collectionView.bounds.size.width - self.sectionInset.left - self.sectionInset.right - (self.columnNum - 1) * self.minimumInteritemSpacing)/self.columnNum;
    CGFloat cellHeight = [self.delegate waterFlowLayout:self cellWidth:cellWidth indexPath:indexPath];
    
    //cell位置  取出最短列的列号"每一添加新的cell都加在最矮的那一列"
    NSInteger minColumn = [self gainMinHeightColumn];
    CGFloat cellX = self.sectionInset.left + (cellWidth + self.minimumInteritemSpacing) * minColumn;
    CGFloat cellY = [self.eachColumnHeightArrM[minColumn] floatValue];
    //更新高度最小的这一列的新高度
    self.eachColumnHeightArrM[minColumn] = @(cellY + cellHeight + self.minimumLineSpacing);
    //创建cell的布局属性
    UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    attr.frame = CGRectMake(cellX, cellY, cellWidth, cellHeight);
    
    return attr;
}

// 自定义布局时一定要实现此方法来返回collectionView的contentSize,内容尺寸,collectionView的滚动范围,取出最高列的最大Y + footerView的高 + 行高

- (CGSize)collectionViewContentSize
{
    return CGSizeMake(0, [self.eachColumnHeightArrM[[self gainMaxHeightColumn]] floatValue] - self.minimumLineSpacing + self.footerReferenceSize.height);
}

#pragma mark - Object Private Function

// 添加布局特性

- (void)addAttributes
{
    [self.attrsArrM removeLastObject]; // 把最后一个footerView的布局属性移除
    NSInteger cellCount = [self.collectionView numberOfItemsInSection:0];
    
    // 新添中cell个数 = cell的总数 - 加入前cell的个数
    NSInteger newCellCount = cellCount - self.attrsArrM.count;
    for (NSInteger i = 0; i < newCellCount; i++) {
        //创建每一个cell的索引
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.attrsArrM.count inSection:0];
        // 创建指定索引cell的布局属性
        UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:indexPath];
        [self.attrsArrM addObject:attr];
    }
    
    //创建footerView的布局属性
    NSIndexPath *footerIndexPath = [NSIndexPath indexPathForItem:0 inSection:0];
    UICollectionViewLayoutAttributes *footerAttr = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter withIndexPath:footerIndexPath];
    // 设置footer的布局属性中的frame
    footerAttr.frame = CGRectMake(0, [self.eachColumnHeightArrM[[self gainMaxHeightColumn]] floatValue] - self.minimumLineSpacing, self.collectionView.bounds.size.width, self.footerReferenceSize.height);
    // 把footer的布局属性添加到数组中"一定要在最后添加"
    [self.attrsArrM addObject:footerAttr];
}

#pragma mark - Getter Function

//获取最高的那一列列号
- (NSInteger)gainMaxHeightColumn
{
    CGFloat maxHeight = 0.0;
    NSInteger maxColumn = 0;
    for (NSInteger i = 0; i < self.columnNum; i++) {
        CGFloat currentColumnHeight = [self.eachColumnHeightArrM[i] floatValue];
        if (maxHeight < currentColumnHeight) {
            maxHeight = currentColumnHeight;
            maxColumn = i;
        }
    }
    return maxColumn;
}

//获取高度最小的那一列列号
- (NSInteger)gainMinHeightColumn
{
    CGFloat minHeight = MAXFLOAT;
    NSInteger minColumn = 0;
    for (NSInteger i = 0; i < self.columnNum; i++) {
        CGFloat currentColumnHeight = [self.eachColumnHeightArrM[i] floatValue];
        if (minHeight > currentColumnHeight) {
            minHeight = currentColumnHeight;
            minColumn = i;
        }
    }
    return minColumn;
}


@end


8.JJWaterFlowCollectionCell.h
#import 

@class JJWaterFlowModel;

@interface JJWaterFlowCollectionCell : UICollectionViewCell

@property (nonatomic, strong) JJWaterFlowModel* shopModel;

@end

9.JJWaterFlowCollectionCell.m

#import "JJWaterFlowCollectionCell.h"
#import "JJWaterFlowModel.h"

@interface JJWaterFlowCollectionCell ()

@property (nonatomic, strong) UIImageView *shopImageView;
@property (nonatomic, strong) UILabel *shopPriceLabel;

@end

@implementation JJWaterFlowCollectionCell

#pragma mark - Override Base Function

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        [self setupUI];
    }
    return self;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    
    //图片
    [self.shopImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.top.equalTo(self.contentView);
        make.width.height.equalTo(self.contentView);
    }];
    
    //标签
    [self.shopPriceLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.equalTo(self.contentView);
        make.width.equalTo(self.contentView);
        make.height.equalTo(@30);
        make.bottom.equalTo(self.contentView);
    }];
}

#pragma mark - Object Private Function

- (void)setupUI
{
    //图片
    UIImageView *shopImageView = [[UIImageView alloc] init];
    [self.contentView addSubview:shopImageView];
    self.shopImageView = shopImageView;
    
    //价格标签
    UILabel *shopPriceLabel = [[UILabel alloc] init];
    shopPriceLabel.font = [UIFont systemFontOfSize:15.0];
    shopPriceLabel.textColor = [UIColor blueColor];
    shopPriceLabel.text = @"¥199";
    shopPriceLabel.textAlignment = NSTextAlignmentCenter;
    shopPriceLabel.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.6];
    [self.contentView addSubview:shopPriceLabel];
    self.shopPriceLabel = shopPriceLabel;

}

#pragma mark - Setter & Getter Function

- (void)setShopModel:(JJWaterFlowModel *)shopModel
{
    _shopModel = shopModel;
    
    self.shopImageView.image = [UIImage imageNamed:self.shopModel.icon];
    self.shopPriceLabel.text = self.shopModel.price;
}


@end


10.JJWaterFlowFooterView.h

#import 

@interface JJWaterFlowFooterView : UICollectionReusableView

@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView;

@end

11.JJWaterFlowFooterView.m

#import "JJWaterFlowFooterView.h"

@interface JJWaterFlowFooterView ()

@end

@implementation JJWaterFlowFooterView

#pragma mark - Override Base Function

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        [self setupUI];
    }
    return self;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    
    [self.activityIndicatorView sizeToFit];
    [self.activityIndicatorView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self);
    }];

}

#pragma mark - Object Private Function

- (void)setupUI
{
    self.backgroundColor = [UIColor lightGrayColor];
    
    UIActivityIndicatorView *indicatorView = [[UIActivityIndicatorView alloc] init];
    [self addSubview:indicatorView];
    self.activityIndicatorView = indicatorView;
}

@end

设计结果

我们直接看下边的gif图。

瀑布流

如图所示可见实现了瀑布流效果。

我踩过的坑

1. JJWaterFlowCollectionCell中的初始化方法怎么都不调用。

// 我的初始化方法是这么写的。
- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        [self setupUI];
    }
    return self;
}

//但是就是不调用,controller里面的regist 和 代理方法里面的dequeue方法也写了。
//后来查了好久,才发现是我大意了。JJWaterFlowCollectionVC中的属性 

@property (nonatomic, strong) NSMutableArray *shopData;

// 数组没有初始化,这样就只会调用:

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return self.shopData.count;
}

// 而不会调用下面这个方法,当然不会调用自定义cell那个类了。

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    JJWaterFlowCollectionCell *waterFlowCell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    waterFlowCell.shopModel = self.shopData[indexPath.item];
    return waterFlowCell;
}

// 不能dequeue当然不能调用cell的自定义方法了。

2. 确定数据源方法和布局还有自定义cell都调用了,还是崩了。

// 崩溃调用堆栈

*** First throw call stack:
(
    0   CoreFoundation                      0x000000010e056d4b __exceptionPreprocess + 171
    1   libobjc.A.dylib                     0x000000010d3f921e objc_exception_throw + 48
    2   CoreFoundation                      0x000000010e0c6f04 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    3   CoreFoundation                      0x000000010dfdc005 ___forwarding___ + 1013
    4   CoreFoundation                      0x000000010dfdbb88 _CF_forwarding_prep_0 + 120
    5   瀑布流                           0x000000010cd2ee2b -[JJWaterFlowCollectionCell layoutSubviews] + 203
    6   UIKit                               0x000000010f2cdab8 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1237
    7   QuartzCore                          0x000000010e550bf8 -[CALayer layoutSublayers] + 146
    8   QuartzCore                          0x000000010e544440 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 366
    9   QuartzCore                          0x000000010e5442be _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 24
    10  QuartzCore                          0x000000010e4d2318 _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 280
    11  QuartzCore                          0x000000010e4ff3ff _ZN2CA11Transaction6commitEv + 475
    12  QuartzCore                          0x000000010e4ffd6f _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 113
    13  CoreFoundation                      0x000000010dffb267 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
    14  CoreFoundation                      0x000000010dffb1d7 __CFRunLoopDoObservers + 391
    15  CoreFoundation                      0x000000010dfdf8a6 CFRunLoopRunSpecific + 454
    16  UIKit                               0x000000010f202aea -[UIApplication _run] + 434
    17  UIKit                               0x000000010f208c68 UIApplicationMain + 159
    18  瀑布流                           0x000000010cd2ec6f main + 111
    19  libdyld.dylib                       0x000000010e80868d start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

找了半天google和stackoverflow都没找到答案,曾经尝试了加入链接标志,还是不可以。最后找到了博客,我就把Masonry从cocoapods中移除,并且拖入到项目中。就好了。

后记

上面就是我利用纯代码实现瀑布流的效果。有什么不对的地方,请各路大神多多指教,本人水平有限,恳请指出其中问题,多多沟通,共同成长。

你可能感兴趣的:(纯代码实现瀑布流效果)