UICollectionView详解五:瀑布流
前面四个章节,我已经详细的讲解了UICollectionView的使用,这一节,我用一个非常实用的例子“瀑布流”来进一步说明UICollectionView的强大作用。
先分析一下瀑布流的特点:
1. 所有item的宽度是一致的。
2. 所有item应该是等比例缩放的。
3. 所有item的高度应该是通过实际宽度与缩放比例计算而得出的。
4. 要保证每一列的底部的y值均匀分布,不能偏差很大。
5. 瀑布流不是常规的流式布局,所以应该使用UICollectionViewLayout,对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