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

时间: 2024-11-09 02:05:13

UICollectionView详解五:瀑布流的相关文章

iOS开发——UI篇OC篇&amp;UICollectionView详解+实例

UICollectionView详解+实例 实现步骤: 一.新建两个类 1.继承自UIScrollView的子类,比如HMWaterflowView * 瀑布流显示控件,用来显示所有的瀑布流数据 2.继承自UIView的子类,比如HMWaterflowViewCell * 代表着瀑布流数据中的一个单元(一个格子) 3.总结 HMWaterflowView和HMWaterflowViewCell的关系实际上类似于 UITableView和UITableViewCell的关系 二.设计HMWater

一步一步学ios UITextView(多行文本框)控件的用法详解(五5.8)

本文转载至 http://wuchaorang.2008.blog.163.com/blog/static/48891852201232014813990/ 1.创建并初始化 创建UITextView的文件,并在.h文件中写入如下代码: [csharp] view plaincopy #import <UIKit/UIKit.h> @interface TextViewController : UIViewController <UITextViewDelegate> { UITe

UICollectionView详解

今天,将和大家一起学习UICollectionView,UIcollectionView自出来后,一直受追捧,确实好用.今天有朋友问我如何添加heardView,我简单的回答:tableview如何添加,那么CollectionView就怎么添加,后来经过自己实验发现确实不是那回事,所以列出一些自己犯的错误,供大家参考. 1.首先实例化一个 UICollectionViewFlowLayout因为要设置item,itemSize,等一些属性. UICollectionViewFlowLayout

转:Windows下的PHP开发环境搭建——PHP线程安全与非线程安全、Apache版本选择,及详解五种运行模式。

原文来自于:http://www.ituring.com.cn/article/128439 Windows下的PHP开发环境搭建——PHP线程安全与非线程安全.Apache版本选择,及详解五种运行模式. 今天为在Windows下建立PHP开发环境,在考虑下载何种PHP版本时,遭遇一些让我困惑的情况,为了解决这些困惑,不出意料地牵扯出更多让我困惑的问题. 为了将这些困惑一网打尽,我花了一下午加一晚上的时间查阅了大量资料,并做了一番实验后,终于把这些困惑全都搞得清清楚楚了. 说实话,之所以花了这么

.NET DLL 保护措施详解(五)常规条件下的破解

为了证实在常规手段破解下能有效保护程序核心功能(演示版本对AES加解密算法及数据库的密钥(一段字符串)进行了保护),特对此DLL保护思路进行相应的测试,包含了反编译及反射测试,看是否能得到AES加解密算法的密钥及数据库字符串. 反编译: 我这里使用了.net dll反编译工具ILSpy,以下为真实截图. 1. NetProtect.BLLDemo.dll 2. NetProtect.ConsoleApplication1.exe 3. NetProtect.CoreClr.dll 综合上图,可以

Android基础入门教程——8.3.8 Paint API之—— Xfermode与PorterDuff详解(五)

Android基础入门教程--8.3.8 Paint API之-- Xfermode与PorterDuff详解(五) 标签(空格分隔): Android基础入门教程 本节引言: 好的,上一节中,我们又写了一个关于Xfermode图片混排的例子--擦美女衣服的Demo,加上前面的 利用Xfermode来实现圆角或圆形ImageView,相信大家对Xfermode已经不再像以前那么陌生了,或者 说有点熟悉了,嗯,本节我们来写Xfermode的最后一个例子,通过Xfermode的ProterDuff.

iOS开发- UICollectionView详解+实例

iOS开发- UICollectionView详解+实例 本章通过先总体介绍UICollectionView及其常用方法,再结合一个实例,了解如何使用UICollectionView. UICollectionView 和 UICollectionViewController 类是iOS6 新引进的API,用于展示集合视图,布局更加灵活,可实现多列布局,用法类似于UITableView 和 UITableViewController 类. 使用UICollectionView 必须实现UICol

Kafka详解五、Kafka Consumer的底层API- SimpleConsumer

Kafka提供了两套API给Consumer The high-level Consumer API The SimpleConsumer API 第一种高度抽象的Consumer API,它使用起来简单.方便,但是对于某些特殊的需求我们可能要用到第二种更底层的API,那么先介绍下第二种API能够帮助我们做哪些事情 一个消息读取多次 在一个处理过程中只消费Partition其中的一部分消息 添加事务管理机制以保证消息被处理且仅被处理一次 使用SimpleConsumer有哪些弊端呢? 必须在程序

详解 内存操作流

目录 内存操作流: 字节内存操作流: 字符内存操作流: (请观看本人博文--<详解 I/O流>) 内存操作流 与之前所讲的几个流有很大的差别 容本人在这里卖个关子,相信同学们在之后的讲解中就会明白本人为何说初此话了. 那么,话不多说,开始本篇博文主题的讲解吧: 内存操作流: 概念: 此流之所以被叫做内存操作流的原因是: 此流是在内存中建立缓冲区以实现数据操作的流 正因为此原因,close()方法是无效的. 而且此流并没有将数据存入硬盘中,而是存入了内存中的一个缓冲区(即:数组)中 那么,现在,