UICollectionView详解五:瀑布流

前面四个章节,我已经详细的讲解了UICollectionView的使用,这一节,我用一个非常实用的例子“瀑布流”来进一步说明UICollectionView的强大作用。

先分析一下瀑布流的特点:

1. 所有item的宽度是一致的。

2. 所有item应该是等比例缩放的。

3. 所有item的高度应该是通过实际宽度与缩放比例计算而得出的。

4. 要保证每一列的底部的y值均匀分布,不能偏差很大。

5. 瀑布流不是常规的流式布局,所以应该使用UICollectionViewLayout,对UICollectionViewLayout不明白的,请参考我前面写的章节,请点击这里。

下面是运行效果图:

1. 竖屏

UICollectionView详解五:瀑布流_第1张图片


2. 横屏

UICollectionView详解五:瀑布流_第2张图片


好的,下面,我们来一步步的实现这个效果。

1.准备数据源(我使用的是plist文件,实际开发中游可能是json数据,不过是差不多的):

UICollectionView详解五:瀑布流_第3张图片


对数据源的说明:

注意:需要服务器端提供图片的宽度和高度的信息(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结构图

UICollectionView详解五:瀑布流_第4张图片


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. 在看具体代码之前,我们先看看瀑布流的具体结构示意图。

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 方法中。代码实现流程图:

UICollectionView详解五:瀑布流_第6张图片

@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


你可能感兴趣的:(瀑布流,autolayout)