版本记录
版本号 | 时间 |
---|---|
V1.0 | 2017.04.16 |
前言
看过很多人写过瀑布流,最近项目中也用到了,所以自己看了一下实现原理,也写了一个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
@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中移除,并且拖入到项目中。就好了。
后记
上面就是我利用纯代码实现瀑布流的效果。有什么不对的地方,请各路大神多多指教,本人水平有限,恳请指出其中问题,多多沟通,共同成长。