iOS 之UICollectionView 开发步骤 之 OC
说起来容易做起来难。
那么我就不说了,来做吧。这就是我的style。
鉴于现在的主流还是OC,那么示例程序还用OC来写,后续补写Swift程序,这里先占个坑。
废话不多说,下面开发步骤来了:
1. 创建程序
万事开头难,先创建一个程序吧,我写完这句话就去创建。取名就叫testCollectionView,但是我要上传到github上,所以,这个名字可能会重复。那么重新弄个工程吧,取名字叫做TestCollectionViewWood。哦,对了,是否只要自己的工程里面没有重复的就可以了。先试一下。
上传到github,参考:iOS 使用 github
果然,不需要考虑跟其它用户的程序重名的问题。
2. 创建Collection View
2.1. 创建过程
在Storyboard里面拖入一个Collection View,然后我调整了大小。实际上,应该不用调整,直接设置Constraints,然后Update Frames即可。我先设置下该Collection View的背景色,方便调试。纯色让我有点恶心,要是什么时候提供图片背景就好了,maybe。
设置名称为:collectionView
2.2. 设置dataSource和delegate
设置完名称,顺便写上下面的代码吧,我看到示例程序里没写可以正常运行,但是我没写就是不行(不知道为什么),折腾了半天:
self.collectionView.dataSource = self; self.collectionView.delegate = self;
无意之间,我找到了原因,可以不写上面这段代码。我是通过搜索dataSource这个关键词找到的。那么下图所示:
右击Collection View 控件,在弹出的对话框中,分别拖动红圈里的两个圆圈,到右侧的视图位置即可。这样,就可以代替上面的代码。
需要注意的是,这种方式可能会导致设置item的size无效。所以,还是不要用的好。
3. 显示内容
- 建立Cell模板,并注册模板
- 设置Collection View的属性
3.1. 注册模板
为上面创建的 collectionView 注册可用的Cell模板。输入“[self.collectionView register” 根据提示选择即可。
[self.collectionView registerClass:[CollectionViewCell class] forCellWithReuseIdentifier:@"Image_Cell"];
要注意的是,上面的代码只适用于没有xib的情况,我用到xib,则需要下面这样:
[self.collectionView registerNib:[UINib nibWithNibName:@"MyImageCell" bundle:nil] forCellWithReuseIdentifier:@"ImageCell"];
前面是类名,后面的是xib里的Identifier。
3.2. 必须的三个 Collection View 协议
- 设置section的数目
- 设置Item的数目
- 设置内容协议
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{ return 0; } -(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{ return 0; } -(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ return NULL; }
这三个协议是必须的。设置的时候,不用记住名称,也不用拷贝。第一个协议可以输入 “-num” 后根据提示选择,第二、第三个协议输入 “-collection” 后根据选择。
注意:第一个Section的个数其实是有默认值的,默认是1,如果自己也是只有一个section,则不用设置这个协议了。
3.3. 设置内容
现在设置内容非常简单,直接获取一个cell,然后设置内容即可。
MyCell *cell =[cv dequeueReusableCellWithReuseIdentifier:@”Image_Cell”]; cell.imageView.image= ... return cell;
3.4. 优化界面之每行显示两个
可以看到,上图中的显示效果是完全没有美感的。所以呢,要优化一下,每行的个数为两个。
如果每行为两个的话,那个每个的大小是多少呢?整个宽度/2??,如果是这样,那就错了,应该算上minimumInteritemSpacing。每行两个,那么,就有一个minimumInteritemSpacing。如果每行有三个Item,那么就有两个minimumInterItemSpacing,依次类推。
根据上述原理,宽度紧凑地设置为:
CGFloat width = ([[UIScreen mainScreen] bounds].size.width - self.flowLayout.minimumInteritemSpacing)/2;
效果如下:
看着行距比间距宽,那是视觉上的错误。其实都是10像素。
3.5. 优化界面之高度不同
有时,图片会有不同的分辨率,那么,高度就有可能不同才会好看。
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{ CGFloat width = ([[UIScreen mainScreen] bounds].size.width - self.flowLayout.minimumInteritemSpacing)/2; CGFloat height = 100; switch (indexPath.row) { case 0: height = 140; break; case 1: height = 240; break; case 2: height = 150; break; case 3: height = 120; break; default: break; } return CGSizeMake(width, height); }
效果如下:
3.5. 实现间隙相同
上面的效果,虽然实现了不同高度,但是导致上下的间隙不均匀,非常难看,那该怎么办?是否需要做几个CollectionView进行嵌套,还是有更还的办法?
当然有了,要不然还会算什么瀑布流。那么怎么实现呢?实现的话需要要自定义UICollectionViewFlowLayout,然后在里面重新设置cell的位置。大体思路就是这样。
现在基本上瀑布流的就是分成n列,但是每列的排列都是紧凑的。基本上,如果没有特殊要求,这n列都是等分的。也就是说,需要确定一个值,总共有几列。确定了这个列数后,就相当于知道了cell的宽度。如果是显示图片的话,宽高比要保持不变。已经有宽度了,并且可以得到UIImage的宽高比,那么就能算出同比例的高度来了。
cell的宽度和高度都知道了,就要确定起始点的坐标了。起始点的坐标x轴无非就每个列的起始位置,当然要出去边框和cell间距的值,就不用细说了。那么Y轴的值怎么弄?Y轴的值要取所有列中Y值最小的那个,然后把新的cell放该列下面。
既然已经知道实现原理了,那么,实现步骤是什么?
3.5.1. 创建UICollectionViewFlowLayout子类
3.5.2. 创建计算cell高度的协议
协议里面只有一个必须的接口,返回一个高度值。 参考: iOS 协议。
@class WaterFallFlowLayoutGS; @protocol WaterFallFlowLayoutGSDelegate <NSObject> //cell 高度 -(CGFloat)getHeightWithWaterFallFlowLayout:(WaterFallFlowLayoutGS*)flowLayout width:(CGFloat)width indexPath:(NSIndexPath*)indexPath; @end
头文件声明:
@interface WaterFallFlowLayoutGS : UICollectionViewFlowLayout -(instancetype)initWithColumCount:(int)count; @property (nonatomic, weak) id<WaterFallFlowLayoutGSDelegate> delegate; @end
协议在这里定义、协议在这么声明为属性、协议在这里引用。
3.5.3. 自定义初始化函数
指定列数。
@interface WaterFallFlowLayoutGS(){ int columnCount;//列数 } //每一列的当前高度的最大值 @property (nonatomic, strong) NSMutableDictionary *maxHeights; //所有布局属性 @property (nonatomic, strong) NSMutableArray *attributesArray; @end @implementation WaterFallFlowLayoutGS -(instancetype) initWithColumCount:(int)count{ if (self = [super init]) { columnCount = count; } return self; } @end
当然,初始化时可以设置一些影响间距的属性:
self.sectionInset = UIEdgeInsetsMake(5.0, 5.0, 5.0, 5.0); self.minimumLineSpacing = 5.0; self.minimumInteritemSpacing = 5.0;
我用它们在设置间距效果。
3.5.4. 懒加载变量
-(NSMutableDictionary *)maxHeights{ if (!_maxHeights) { _maxHeights = [NSMutableDictionary dictionary]; for (int i = 0; i < columnCount; i++) { NSString *column = [NSString stringWithFormat:@"%d",i]; self.maxHeights[column] = @"0"; } } return _maxHeights; } -(NSMutableArray *)attributesArray{ if (!_attributesArray) { _attributesArray = [NSMutableArray array]; } return _attributesArray; }
3.5.5. 设置Layout准备--初始化所有列属性
#pragma mark - 初始化所有属性值 -(void)prepareLayout{ //初始化每列的Y坐标,即在最顶端 for (int i = 0; i < columnCount; i++) { NSString *column = [NSString stringWithFormat:@"%d",i]; self.maxHeights[column] = @(self.sectionInset.top); } [self.attributesArray removeAllObjects]; //1.查看共有多少个元素 NSInteger count = [self.collectionView numberOfItemsInSection:0]; //2.遍历每个item属性 for (int i = 0; i < count; i++) { //3.取出布局属性 UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]; //4.添加到数组中 [self.attributesArray addObject:attr]; } }
3.5.6. 设置CollectionView的ContentSize
#pragma mark - 设置整个collectionView的ContentSize -(CGSize)collectionViewContentSize{ __block NSString *maxColumn = @"0"; [self.maxHeights enumerateKeysAndObjectsUsingBlock:^(NSString *column, NSNumber *maxY, BOOL * stop) { if ([maxY floatValue] > [self.maxHeights[maxColumn] floatValue]) { maxColumn = column; } }]; return CGSizeMake(0, [self.maxHeights[maxColumn] floatValue] + self.sectionInset.bottom); }
3.5.7. 设置所有cell的属性
#pragma mark - 设置所有cell的大小及位置 -(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{ return self.attributesArray; }
3.5.8. 计算每个cell的属性
#pragma mark - 每列的大小、位置等属性 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{ //每列中都可能已经有多个元素,找出所有列中 Y坐标 的最大值中的最小值 __block NSString *miniColumn = @"0"; [self.maxHeights enumerateKeysAndObjectsUsingBlock:^(NSString *column, NSNumber *maxY, BOOL * stop) { if ([maxY floatValue] < [self.maxHeights[miniColumn] floatValue]) { miniColumn = column; } }]; //计算frame CGFloat width = (CGRectGetWidth(self.collectionView.frame) - self.sectionInset.left - self.sectionInset.right - self.minimumInteritemSpacing * (columnCount - 1))/columnCount; CGFloat height = [self.delegate getHeightWithWaterFallFlowLayout:self width:width indexPath:indexPath]; CGFloat x = self.sectionInset.left + (width + self.minimumInteritemSpacing)*[miniColumn intValue]; CGFloat y = [self.maxHeights[miniColumn] floatValue] + self.minimumInteritemSpacing; self.maxHeights[miniColumn] = @(height + y); UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; attr.frame = CGRectMake(x, y, width, height); return attr; }
3.5.9. 使用自定义的FlowLayout
// // ViewController.m // testCollectionView // // Created by WoodGao on 15/12/2. // Copyright 2015年 wood. All rights reserved. // #import "ViewController.h" #import "MyImageCell.h" #import "WaterFallFlowLayoutGS.h" @interface ViewController () <UICollectionViewDataSource,UICollectionViewDelegate, WaterFallFlowLayoutGSDelegate> @property (weak, nonatomic) IBOutlet UICollectionView *collectionView; @property (strong, nonatomic) WaterFallFlowLayoutGS *waterFlow; @property (strong, nonatomic) NSMutableArray *imgArr; @end @implementation ViewController -(void) initImageArray { if (nil == _imgArr) { NSMutableArray *tempArr = [[NSMutableArray alloc] init]; for (int i=1; i<=15; i++) { UIImage *img = [UIImage imageNamed:[NSString stringWithFormat:@"%d",i]]; [tempArr addObject:img]; } _imgArr = tempArr; } } //根据宽度,计算高度 -(CGFloat) heightForImage:(UIImage*)image width:(CGFloat)width { return image.size.height/image.size.width * width; } - (void)viewDidLoad { [super viewDidLoad]; _waterFlow = [[WaterFallFlowLayoutGS alloc]initWithColumCount:2]; _waterFlow.delegate = self; self.collectionView.collectionViewLayout = _waterFlow; [self.collectionView registerNib:[UINib nibWithNibName:@"MyImageCell" bundle:nil] forCellWithReuseIdentifier:@"MyImageCell"]; self.collectionView.dataSource = self; self.collectionView.delegate = self; [self initImageArray]; } -(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{ return _imgArr.count; } -(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ MyImageCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"MyImageCell" forIndexPath:indexPath]; cell.image.image = _imgArr[indexPath.item]; return cell; } -(CGFloat)getHeightWithWaterFallFlowLayout:(WaterFallFlowLayoutGS *)flowLayout width:(CGFloat)width indexPath:(NSIndexPath *)indexPath{ CGFloat height = [self heightForImage:_imgArr[indexPath.item] width:width]; return height; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{ NSLog(@"当前选中的item 是 %ld",(long)indexPath.row); } @end
效果图如下:
3.6. 网络下载图片
3.x. 点击图片时,最大化该图,并实现动画
3.x. 移动cell
3.x. 其实可以实现部分列的布局
思路是这样:在创建的时候定义每个cell的类型,类型不同,则表现形式就不同。不同的类型在被点击的时候,也可以表现出不同的效果。