純代碼實現(xiàn)瀑布流效果

版本記錄

版本號 時間
V1.0 2017.04.16

前言

看過很多人寫過瀑布流,最近項目中也用到了,所以自己看了一下實現(xiàn)原理,也寫了一個demo,希望對大家能有幫助,下面會貼出全部代碼,gitHub地址

詳細設計

還是先看一下文檔結構。

文檔結構

下面看詳細的代碼。

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 <UIKit/UIKit.h>

@interface JJWaterFlowCollectionVC : UICollectionViewController

@end

3.JJWaterFlowCollectionVC.m

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

@interface JJWaterFlowCollectionVC () <JJWaterFlowLayoutDelegate>

@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時加載數(shù)據(jù)

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    //如果當前footerView沒有顯示或正在加載數(shù)據(jù)時直接返回
    if (!self.footerView || self.footerView.activityIndicatorView.isAnimating) {
        return;
    }
    
    //當offset.y + collectionView的高 > footerView的Y時開始加載數(shù)據(jù)
    if ((scrollView.contentOffset.y + scrollView.bounds.size.height) > CGRectGetMaxY(self.footerView.frame)) {
        //菊花旋轉
        [self.footerView.activityIndicatorView startAnimating];
        //延時3秒,模擬加載網(wǎng)絡
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //加載數(shù)據(jù)
            [self loadData];
            [self.footerView.activityIndicatorView stopAnimating];
            self.footerView = nil;
            [self.collectionView reloadData];
        });
    }

}


@end


4. JJWaterFlowModel.h
#import <UIKit/UIKit.h>

@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 <UIKit/UIKit.h>

@class JJWaterFlowLayout;

@protocol JJWaterFlowLayoutDelegate <NSObject>

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

@end

@interface JJWaterFlowLayout : UICollectionViewFlowLayout

@property (nonatomic, assign) NSInteger columnNum;
@property (nonatomic, weak) id<JJWaterFlowLayoutDelegate>delegate;

@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的布局屬性
// 通過輸出此方法的返回值,發(fā)現(xiàn)此方法返回的數(shù)組中是每一個itme"cell"的布局屬性,里面有兩個關鍵屬性,一個是cell的索引,一個是cell的frame
// 1.此方法會計算當前顯示區(qū)域中所有cell的布局屬性,
// 2.一旦計算完成,所有的屬性會被緩存起來,不會再次計算;
// 結論:我們可以手動來計算每一個cell的frame,并保到數(shù)組中,就應該可以實現(xiàn)瀑布流效果

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

//創(chuàng)建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);
    //創(chuàng)建cell的布局屬性
    UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    attr.frame = CGRectMake(cellX, cellY, cellWidth, cellHeight);
    
    return attr;
}

// 自定義布局時一定要實現(xiàn)此方法來返回collectionView的contentSize,內(nèi)容尺寸,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個數(shù) = cell的總數(shù) - 加入前cell的個數(shù)
    NSInteger newCellCount = cellCount - self.attrsArrM.count;
    for (NSInteger i = 0; i < newCellCount; i++) {
        //創(chuàng)建每一個cell的索引
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.attrsArrM.count inSection:0];
        // 創(chuàng)建指定索引cell的布局屬性
        UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:indexPath];
        [self.attrsArrM addObject:attr];
    }
    
    //創(chuàng)建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的布局屬性添加到數(shù)組中"一定要在最后添加"
    [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 <UIKit/UIKit.h>

@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 <UIKit/UIKit.h>

@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圖。

瀑布流

如圖所示可見實現(xiàn)了瀑布流效果。

我踩過的坑

1. JJWaterFlowCollectionCell中的初始化方法怎么都不調(diào)用。

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

//但是就是不調(diào)用,controller里面的regist 和 代理方法里面的dequeue方法也寫了。
//后來查了好久,才發(fā)現(xiàn)是我大意了。JJWaterFlowCollectionVC中的屬性 

@property (nonatomic, strong) NSMutableArray *shopData;

// 數(shù)組沒有初始化,這樣就只會調(diào)用:

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

// 而不會調(diào)用下面這個方法,當然不會調(diào)用自定義cell那個類了。

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

// 不能dequeue當然不能調(diào)用cell的自定義方法了。

2. 確定數(shù)據(jù)源方法和布局還有自定義cell都調(diào)用了,還是崩了。

// 崩潰調(diào)用堆棧

*** 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都沒找到答案,曾經(jīng)嘗試了加入鏈接標志,還是不可以。最后找到了博客,我就把Masonry從cocoapods中移除,并且拖入到項目中。就好了。

后記

上面就是我利用純代碼實現(xiàn)瀑布流的效果。有什么不對的地方,請各路大神多多指教,本人水平有限,懇請指出其中問題,多多溝通,共同成長。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容