版本記錄
版本號 | 時間 |
---|---|
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)瀑布流的效果。有什么不對的地方,請各路大神多多指教,本人水平有限,懇請指出其中問題,多多溝通,共同成長。