前面四个章节,我已经详细的讲解了UICollectionView的使用,这一节,我用一个非常实用的例子“瀑布流”来进一步说明UICollectionView的强大作用。
先分析一下瀑布流的特点:
1. 所有item的宽度是一致的。
2. 所有item应该是等比例缩放的。
3. 所有item的高度应该是通过实际宽度与缩放比例计算而得出的。
4. 要保证每一列的底部的y值均匀分布,不能偏差很大。
5. 瀑布流不是常规的流式布局,所以应该使用UICollectionViewLayout
下面是运行效果图:
1. 竖屏
2. 横屏
好的,下面,我们来一步步的实现这个效果。
1.准备数据源(我使用的是plist文件,实际开发中游可能是json数据,不过是差不多的):
对数据源的说明:
注意:需要服务器端提供图片的宽度和高度的信息(h,w两个值)。如果我们不把宽度和高度信息放在数据源中,那么当图片信息获取后,我们自己还要在前端自己计算宽度和高度。在网络不好的情况下,有的图片也许长时间加载不到,那么我们就不知道怎么去布局了。如果提供了图片的宽度和高度信息,就算图片没有加载到,但是宽度和高度信息是可以获取到的,这个时候,我们可以放置占位图片,等图片加载完毕后,再替换掉占位图片。
2. 建立对应的模型
@interface LFShop : NSObject
/*图片的宽度*/
@property (nonatomic,assign) CGFloat w;
/*图片的高度*/
@property (nonatomic,assign) CGFloat h;
/*图片的url*/
@property (nonatomic,copy) NSString *img;
/*图片的价格信息*/
@property (nonatomic,copy) NSString *price;
@end
3. 自定义UICollectionViewCell,用来显示最终的图片信息
@class LFShop;
@interface LFWaterFlowCell : UICollectionViewCell
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) IBOutlet UIButton *priceBtn;
@property (nonatomic,strong) LFShop *shop;
@end
@implementation LFWaterFlowCell
-(void)setShop:(LFShop *)shop {
_shop = shop;
[self.imageView sd_setImageWithURL:[NSURL URLWithString:shop.img] placeholderImage:[UIImage imageNamed:@"placeholder.jpg"] options:SDWebImageRetryFailed];
[self.priceBtn setTitle:shop.price forState:UIControlStateNormal];
}
@end
xib结构图
4. 在控制器ViewController.m中初始化UICollectionView,及设置数据源方法
@interface ViewController ()<UICollectionViewDataSource,UICollectionViewDelegate,LFWaterFlowLayoutDelegate>
@property (nonatomic,strong) NSMutableArray *shops;
@property (nonatomic,weak) LFWaterFlowLayout *layout;
@property (nonatomic,weak) UICollectionView *collectionView;
@property (nonatomic,assign,getter=isLoadRotate) BOOL loadRotate;
@end
static NSString *const identifer = @"LFWaterFlowCell";
@implementation ViewController
#pragma mark - Lazy Load
-(NSMutableArray *)shops {
if (!_shops) {
NSArray *defaultArray = [LFShop objectArrayWithFilename:@"2.plist"];
_shops = [NSMutableArray array];
[_shops addObjectsFromArray:defaultArray];
}
return _shops;
}
#pragma mark - init
- (void)viewDidLoad {
[super viewDidLoad];
[self collectionViewInit];
}
- (void)collectionViewInit {
LFWaterFlowLayout *layout = [[LFWaterFlowLayout alloc] init];
layout.delegate = self;
self.layout = layout;
//layout.insets = UIEdgeInsetsMake(20, 20, 20, 20);
//layout.count = 4;
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
collectionView.dataSource = self;
collectionView.delegate = self;
collectionView.backgroundColor = [UIColor darkGrayColor];
[self.view addSubview:collectionView];
// autolayout全屏幕显示
[collectionView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
[collectionView registerNib:[UINib nibWithNibName:@"LFWaterFlowCell" bundle:nil] forCellWithReuseIdentifier:identifer];
self.collectionView = collectionView;
}
#pragma mark - UICollectionView
// Datasource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.shops.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
LFWaterFlowCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifer forIndexPath:indexPath];
cell.shop = self.shops[indexPath.item];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
LFShop *shop = self.shops[indexPath.item];
NSLog(@"Item Price:%@",shop.price);
}
@end
代码中,大家可以看到,我定义了一个LFWaterFlowLayout,它就是用来对UICollectionView进行布局的。
5. 在看具体代码之前,我们先看看瀑布流的具体结构示意图。
LFWaterFlowLayout.h对应的定义代码如下:
@class LFWaterFlowLayout;
@protocol LFWaterFlowLayoutDelegate <NSObject>
/*通过代理获得每个cell的高度(之所以用代理取得高度的值,就是为了解耦,这里定义的LFWaterFlowLayout不依赖与任务模型数据)*/
- (CGFloat)waterFlowLayout:(LFWaterFlowLayout *)waterFlowLayout heightForWidth:(CGFloat)width atIndexPath:(NSIndexPath *)indexPath;
@end
@interface LFWaterFlowLayout : UICollectionViewLayout
/*cell的列间距*/
@property (nonatomic,assign) CGFloat columnMargin;
/*cell的行间距*/
@property (nonatomic,assign) CGFloat rowMargin;
/*cell的top,right,bottom,left间距*/
@property (nonatomic,assign) UIEdgeInsets insets;
/*显示多少列*/
@property (nonatomic,assign) NSInteger count;
@property (nonatomic,assign) id<LFWaterFlowLayoutDelegate> delegate;
@end
LFWaterFlowLayout.m 中具体的实现代码:
这里面的难点就是怎么计算每一个cell所在的位置。主要代码在layoutAttributesForItemAtIndexPath 方法中。代码实现流程图:
@interface LFWaterFlowLayout()
/* Key: 第几列; Value: 保存每列的cell的底部y值 */
@property (nonatomic,strong) NSMutableDictionary *cellInfo;
@end
@implementation LFWaterFlowLayout
#pragma mark - 初始化属性
- (instancetype)init {
self = [super init];
if (self) {
self.columnMargin = 10;
self.rowMargin = 10;
self.insets = UIEdgeInsetsMake(10, 10, 10, 10);
self.count = 3;
}
return self;
}
- (NSMutableDictionary *)cellInfo {
if (!_cellInfo) {
_cellInfo = [NSMutableDictionary dictionary];
}
return _cellInfo;
}
#pragma mark - 重写父类的方法,实现瀑布流布局
#pragma mark - 当尺寸有所变化时,重新刷新
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
- (void)prepareLayout {
[super prepareLayout];
// 可以在每次旋转屏幕的时候,重新计算
for (int i=0; i<self.count; i++) {
NSString *index = [NSString stringWithFormat:@"%d",i];
self.cellInfo[index] = @(self.insets.top);
}
}
#pragma mark - 处理所有的Item的layoutAttributes
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
// 每次重新布局之前,先清除掉以前的数据(因为屏幕滚动的时候也会调用)
__weak typeof (self) wSelf = self;
[self.cellInfo enumerateKeysAndObjectsUsingBlock:^(NSString *columnIndex, NSNumber *minY, BOOL *stop) {
wSelf.cellInfo[columnIndex] = @(wSelf.insets.top);
}];
NSMutableArray *array = [NSMutableArray array];
NSInteger count = [self.collectionView numberOfItemsInSection:0];
for (int i=0; i<count; i++) {
UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
[array addObject:attrs];
}
return array;
}
#pragma mark - 处理单个的Item的layoutAttributes
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
// 获取cell底部Y值最小的列
__block NSString *minYForColumn = @"0";
__weak typeof (self) wSelf = self;
[self.cellInfo enumerateKeysAndObjectsUsingBlock:^(NSString *columnIndex, NSNumber *minY, BOOL *stop) {
if ([minY floatValue] < [wSelf.cellInfo[minYForColumn] floatValue]) {
minYForColumn = columnIndex;
}
}];
CGFloat width = (self.collectionView.frame.size.width - self.insets.left - self.insets.right - self.columnMargin * (self.count - 1)) / self.count;
CGFloat height = [self.delegate waterFlowLayout:self heightForWidth:width atIndexPath:indexPath];
CGFloat x = self.insets.left + (width + self.columnMargin) * [minYForColumn integerValue];
CGFloat y = self.rowMargin + [self.cellInfo[minYForColumn] floatValue];
self.cellInfo[minYForColumn] = @(y + height);
// 创建属性
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attrs.frame = CGRectMake(x, y, width, height);
return attrs;
}
#pragma mark - CollectionView的滚动范围
- (CGSize)collectionViewContentSize {
CGFloat width = self.collectionView.frame.size.width;
__block CGFloat maxY = 0;
[self.cellInfo enumerateKeysAndObjectsUsingBlock:^(NSString *columnIndex, NSNumber *itemMaxY, BOOL *stop) {
if ([itemMaxY floatValue] > maxY) {
maxY = [itemMaxY floatValue];
}
}];
return CGSizeMake(width, maxY + self.insets.bottom);
}
@end
最后记得要设置 collectionViewContentSize,并且保持距离屏幕底部有insets.bottom的距离。
然后,我们在ViewController.m中遵守LFWaterFlowLayoutDelegate协议并实现其代理方法,计算出每个cell的高度
#pragma mark - LFWaterFlowLayoutDelegate
- (CGFloat)waterFlowLayout:(LFWaterFlowLayout *)waterFlowLayout heightForWidth:(CGFloat)width atIndexPath:(NSIndexPath *)indexPath {
LFShop *shop = self.shops[indexPath.item];
return shop.h / shop.w * width;
}
6. 支持横屏竖屏切换功能,本质就是改变LFWaterFlowLayout的count属性值。所以我们在ViewController.m中添加以下代码:
#pragma mark - 首次加载的时候,应该调用旋转方法
- (void)viewWillAppear:(BOOL)animated {
// 首次加载的时候,单独处理
self.loadRotate = YES;
CGSize orignal = [UIScreen mainScreen].bounds.size;
[self viewWillTransitionToSize:orignal withTransitionCoordinator:nil];
[super viewWillAppear:animated];
}
#pragma mark - 屏幕旋转
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
CGSize screenSize = [UIScreen mainScreen].bounds.size;
CGFloat width = size.width;
if (screenSize.width == width) {
if (self.isLoadRotate) {
self.loadRotate = NO;
} else {
// Actual Width
width = size.height;
}
}
CGFloat maxWidth = screenSize.width > screenSize.height ? screenSize.width : screenSize.height;
// LandScape
if (width == maxWidth) {
self.layout.count = 5;
} else { // Potrait
self.layout.count = 3;
}
}
7. 集成上拉下拉刷新功能
修改ViewController.m中的viewDidLoad 方法,添加addRefresh方法
- (void)viewDidLoad {
[super viewDidLoad];
[self collectionViewInit];
[self addRefresh];
}
- (void)addRefresh {
[self.collectionView addHeaderWithTarget:self action:@selector(loadNew)];
[self.collectionView addFooterWithTarget:self action:@selector(loadMore)];
}
- (void)loadNew {
NSArray *newResult = [LFShop objectArrayWithFilename:@"1.plist"];
NSRange range = NSMakeRange(0, newResult.count);
// 添加更多的新数据
[self.shops insertObjects:newResult atIndexes:[NSIndexSet indexSetWithIndexesInRange:range]];
[self.collectionView reloadData];
[self.collectionView headerEndRefreshing];
}
- (void)loadMore {
NSArray *moreResult = [LFShop objectArrayWithFilename:@"3.plist"];
[self.shops addObjectsFromArray:moreResult];
[self.collectionView reloadData];
[self.collectionView footerEndRefreshing];
}
至此,整个的瀑布流功能就算完成了。
备注:被实例中引用了很多的第三方库
1. MJExtension,下载地址: https://github.com/CoderMJLee/MJExtension
2. MJRefresh,下载地址: https://github.com/CoderMJLee/MJRefresh
3. SDWebImage,下载地址: https://github.com/rs/SDWebImage
4. AutoLayout,下载地址: https://github.com/smileyborg/UIView-AutoLayout