Github地址:-CollectionViewLayout-CollectionViewFlowLayout-
这里详解了三个demo去帮助大家更好的了解CollectionViewLayout和CollectionViewFlowLayout
自定义流水布局--CollectionViewFlowLayout---水平布局实现一个相册功能
一
在UIScrollView的基础上进行循环利用
那怎么去做循环利用呢?
第一种方案:
实时监控ScrollView的滚动,一旦有一个家伙离开屏幕,我们就把它放进一个数组或者是集合里面去,到时候我要用,我就把它拿过去用
但是这个是很麻烦的,因为你总是得判断它有没有离开屏幕
第二种方案:
用苹果自带的几个类:TableView或者是CollectionView
因为它们本来就具备循环利用的功能
但是TableView一看就不符合要求,因为它默认就是上下竖直滚动,不是左右水平滚动
当然我们也可以用非主流的方式,让TableView实现水平滚动
让TableView的Transform来个90°,让它里面所有的cell也翻个90°,都转过来。但这种做法有点奇葩,开发中还是不要这么搞
所以我们可以用CollectionView
CollectionView在我们的印象中是展示像那种九宫格的样子,而且也是上下竖直滚动
但是CollectionView和TableView的区别就是:
CollectionView它默认就支持水平滚动,你只要修改它一个属性为水平方向就行了。而TableView默认支持竖直滚动,没有属性去支持它水平滚动,除非你去搞一些非主流的做法
二
CollectionView一定要传一个不空的Layout那个参数,因为默认的布局是九宫格,它按这种方式排的原因是它有一个流水布局。正因为给它传了一个流水布局,所以它就一行满了,就流向下一行,流水一样流下去流过来
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:[UICollectionViewlayout alloc] init]];
数据源方法 -
numberOfItemsInSection是告诉它一组有多少个格子
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 50 ;
}
cellForItemAtIndexPath告诉它每个格子长出来是怎样的一个cell,因为每个格子都是一个CollectionViewCell
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
// 先要注册
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CYCellId forIndexPath:indexPath];
cell.backgroundColor = [UIColor orangeColor]
return cell;
}
TableView和CollectionView的排布有很大的区别
TableView的排布是一行一行往下排布,而CollectionView的排布是完全取决于Layout,也就是说,你传给它的Layout不一样,它的排布就不一样。它的布局决定了cell的排布
也就是说,今后你想要CollectionView的cell排布丰富多彩,你只需要改变它的布局就行了
scrollDirection决定了它的滚动方向,设置它滚动的方向为水平
// 水平滚动
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
itemSize决定了CollectionView布局的里面的cell的大小
layout.itemSize = CGSizeMake(100, 100);
你将CollectionView高度改小点,比如200,那么你的高度不够显示两排,就会如下显示:
而且你会发现不用担心循环利用的问题,CollectionView内部已经帮你做好了
三
我们现在已经实现流水布局水平滚动,而且做好了循环利用。如果要做一层改进,那么我们就要自定义布局,自己来写一套布局,所以现在我们继承于UICollectionViewFlowLayout
我们要自定义CollectionView的布局有两种方案
1.继承UICollectionViewLayout
一般是继承于UICollectionViewLayout就行了
而且UICollectionViewFlowLayout继承于UICollectionViewLayout
但是如果你自定义继承于UICollectionViewLayout,代表着你没有流水布局功能,也就是在你不想要流水布局功能的时候就选择继承UICollectionViewLayout
2.继承UICollectionViewFlowLayout
四
所以我们自定义流水布局CYLineLayout
在CYLineLayout.h文件中
#import
@interface CYLineLayout : UICollectionViewFlowLayout
@end
在CYLineLayout.m文件中重写某些方法去实现:
1.cell的放大与缩小
2.停止滚动的时候:cell居中
进入头文件可以发现要重写的一些方法
UICollectionViewLayoutAttributes
1.它是描述布局属性的
2.一个cell对应一个UICollectionViewLayoutAttributes对象
3.UICollectionViewLayoutAttributes对象决定了cell的展示样式(frame)说白了就是决定你的cell摆在哪里,怎么去摆
layoutAttributesForElementsInRect这个方法的返回值是一个数组(数组里面存放着rect范围内所有元素的布局属性)
这个方法的返回值决定了rect范围内所有元素的排布(frame)
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// 获得super已经计算好的布局属性(在super已经算好的基础上,再去做一些改进)
NSArray *array = [super layoutAttributesForElementsInRect:rect];
// 计算collectionView最中心点的x值
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;
// 在原有布局属性的基础上进行微调
for (UICollectionViewLayoutAttributes *attrs in array) {
// cell的中心点x和collectionView最中心点的x值 的间距
CGFloat delta = ABS(attrs.center.x - centerX);
// 根据间距值计算cell的缩放比例
CGFloat scale = 1 - delta / self.collectionView.frame.size.width;
// 设置缩放比例
attrs.transform = CGAffineTransformMakeScale(scale, scale);
}
return array;
}
计算collectionView中心点的x值
要记住collectionView的坐标原点是以内容contentSize的原点为原点
计算collectionView中心点的x值,千万不要用collectionView的宽度除以2。而是用collectionView的偏移量加上collectionView宽度的一半
坐标原点弄错了就没有可比性了,因为后面要判断cell的中心点与collectionView中心点的差值
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;
cell的中心点x 和CollectionView最中心点的x值 的间距
CGFloat delta = ABS(attrs.center.x - centerX);
ABS(A)
// 表示取绝对值
我们再根据间距值delta去算cell的缩放比例scale
间距值delta和缩放比例scale是成反比的
间距值delta的范围为0--self.collectionView.frame.size.width * 0.5
CGFloat scale = 1 - delta / self.collectionView.frame.size.width;
// 用1-(),是因为间距值delta和缩放比例scale是成反比的
设置缩放比例
attrs.transform = CGAffineTransformMakeScale(scale, scale);
但是设置后你会发现基本没啥反应,显示还乱七八糟的,这是什么原因呢?
我们是想要稍微动一下就修改一下,但是现在没法达到我动一下就根据最新的中心点X来再算一遍一边比例。没有实现这个代码
因为这里还需要实现一个方法
这个方法是shouldInvalidateLayoutForBoundsChange: 它的特点是:
默认return NO
当collectionView的显示范围发生改变的时候,判断是否需要重新刷新布局
一旦重新刷新布局,就会重新调用下面的方法:
1.prepareLayout
2.layoutAttributesForElementsInRect:方法
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
这样之后,你会发现,你稍微挪一下,它就重新算一遍,比例就会缩放, 达到了我们的要求
而且非常流畅,因为它有循环利用
五
还要实现一个方法:targetContentOffsetForProposedContentOffset:()方法。它的返回值,就决定了collectionView停止滚动时的偏移量
这个方法在你手离开屏幕之前会调用,也就是cell即将停止滚动的时候 (记住这一点)
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
// 计算出最终显示的矩形框
CGRect rect;
rect.origin.y = 0;
rect.origin.x = proposedContentOffset.x;
rect.size = self.collectionView.frame.size;
// 获得super已经计算好的布局属性
NSArray *array = [super layoutAttributesForElementsInRect:rect];
// 计算collectionView最中心点的x值
CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;
// 存放最小的间距值
CGFloat minDelta = MAXFLOAT;
for (UICollectionViewLayoutAttributes *attrs in array) {
if (ABS(minDelta) > ABS(attrs.center.x - centerX)) {
minDelta = attrs.center.x - centerX;
}
}
// 修改原有的偏移量
proposedContentOffset.x += minDelta;
return proposedContentOffset;
}
获得super已经计算好的布局属性
NSArray *array = [super layoutAttributesForElementsInRect:rect];
这里为什么不用self
因为如果调self,又会来到layoutAttributesForElementsInRect:()方法的for循环中, 将transform再算一遍。而我们只想要拿到中心点X值。靠父类就行了
我们调super这个方法,因为它当时已经算好了cell的中心点等X的值了。所以这里调super可能更好一点
计算collectionView最中心点的x值
CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;
这里为什么不按前面
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;
来算呢?
因为targetContentOffsetForProposedContentOffset:()方法在你手离开屏幕之前会调用,也就是cell即将停止滚动的时候,这个时候我们要算的是最后停下来偏移量。
假如我们用力往左边一甩,你的手已经离开,算的偏移量是你手离开时候的偏移量,而不是我们最终的偏移量,也就是说这么算的话,我们就算错了
你是应该拿到最终停下来的cell和CollectionView的中心点的X值进行比较的。所以你应该最终的值,而不是手松开的那一刻的偏移量的值
那我们怎么知道手松开的那一刻最终的偏移量X的值呢?
这个方法返回的参数(CGPoint)proposedContentOffset,这是它本应该停留的位置,最终停留的的值。而(CGPoint)targetContentOffsetForProposedContentOffset:这个是你最终返回的值,也就是你要它停留到哪儿的值(这个参数决定你要cell最后停留在哪儿)
同上面可知,我们最后拿到的矩形框也是不能乱传的,也是要拿到最终的哪一个矩形框(不明白,就想像一下,你往左边或者右边用手指一甩的时候,手离开的时候是一个值,最终停下来是一个值,而现在我们需要的是最终的值)
// 计算出最终显示的矩形框
CGRect rect; rect.origin.y = 0; rect.origin.x = proposedContentOffset.x; rect.size = self.collectionView.frame.size;
然后我们要找最短的偏移量,找到它,然后就让他偏移它的那个值,让它的中心点回到collectionView的中心点,也就是说重合。这样就实现了不管你怎么去甩,等cell停下来的时候。都会有一个cell它会停留在矩形框CollectionView的中心
// 存放最小的间距值
CGFloat minDelta = MAXFLOAT;
for (UICollectionViewLayoutAttributes *attrs in array) {
if (ABS(minDelta) > ABS(attrs.center.x - centerX)) {
minDelta = attrs.center.x - centerX;
}
}
// 修改原有的偏移量
proposedContentOffset.x += minDelta;
return proposedContentOffset;
一开始先保证minDelta是最大的,保证谁都比你小。 第一次算出来的绝对值就肯定比你小,然后把它赋值给你minDelta。这样就算出来了最小的间距值
算出来最小间距值后,你通过分析应该会发现,不管是往左偏还是往右偏,要想让cell回到中心点,最后你的偏移量应该是用:你本来应该 的偏移量+(cell的中心点X值—collectionView中心点X值)
所以上面在比较的时候用绝对值,计算的时候不用绝对值,minDelta最后就有正数也有负数
修改后让它回到中间
最后不管你怎么滑,它都会停在中间
六
有一个小缺陷,你会发现,一打开程序,你往左或往右滑到最左或者最右的时候,cell总是默认粘着边上,这个不太和谐,我们需要它距离左右两边都有一个距离,那我们该怎么做呢?
这就是让我们把所有的cell,让它们往右边或者左边挪一段距离,所以就增加内边距就可以了。怎么添加内边距呢?
collectionView是继承ScrollView的,所以设置它的ContentInset就可以了
还一种方法通过这个布局它本来就有一个属性sectionInset ,这本来就是来控制内边距的,控制整个布局的。而且这个属性只需要设置一次
这里有一个给collectionView专门用来布局的方法---prepareLayout,这里一般是做初始化操作
/**
* 用来做布局的初始化操作(不建议在init方法中进行布局的初始化操作--可能布局还未加到View中去,就会返回为空)
*/
- (void)prepareLayout
{
[super prepareLayout];
// 设置内边距
CGFloat inset = (self.collectionView.frame.size.width - self.itemSize.width) * 0.5;
self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset);
}
七
总的来说我们若要继承自这个流水布局来实现这个功能的话,肯定是要重写一些方法,告诉它一些内部的行为,它才知道怎么去显示那个东西,我们用了一下的方法:
我们首先得实现prepareLayout方法,做一些初始化
然后,我们实现layoutAttributesForElementsInRect:方法。目的是拿出它计算好的布局属性来做一个微调,这样可以导致我们的cell可以变大或者变小
然后实现targetContentOffsetForProposedContentOffset:方法。它的目的是告诉当我手松开,cell停止滚动的时候,他应该去哪儿,所以这个方法就决定了collectionView停止滚动时的偏移量
最后shouldInvalidateLayoutForBoundsChange:这个方法的价值就是告诉它你只要稍微往左或者往右挪一下,你就重新刷新,只要你重新刷新,它就会重新根据你cell的中心点的X值距离你collectionView中心点的X值来决定你的缩放比例。这样就保证了我们每动一点点,比例都在变,所以我们要动一下刷新一下。也就是当collectionView的显示范围发生改变的时候,是否需要重新刷新布局,一旦重新刷新布局,就会重新调用下面的方法:1.prepareLayout2.layoutAttributesForElementsInRect:方法
关于做这个效果有一个挺牛逼的三方框架:iCarousel大家可以参考一下
八
在CYLineLayout.h文件中
#import
@interface CYLineLayout : UICollectionViewFlowLayout
@end
在CYLineLayout.h文件中
#import "CYLineLayout.h"
@implementation CYLineLayout
- (instancetype)init
{
if (self = [super init]) {
}
return self;
}
/**
* 当collectionView的显示范围发生改变的时候,是否需要重新刷新布局
* 一旦重新刷新布局,就会重新调用下面的方法:
1.prepareLayout
2.layoutAttributesForElementsInRect:方法
*/
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
/**
* 用来做布局的初始化操作(不建议在init方法中进行布局的初始化操作)
*/
- (void)prepareLayout
{
[super prepareLayout];
// 水平滚动
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
// 设置内边距
CGFloat inset = (self.collectionView.frame.size.width - self.itemSize.width) * 0.5;
self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset);
}
/**
UICollectionViewLayoutAttributes *attrs;
1.一个cell对应一个UICollectionViewLayoutAttributes对象
2.UICollectionViewLayoutAttributes对象决定了cell的frame
*/
/**
* 这个方法的返回值是一个数组(数组里面存放着rect范围内所有元素的布局属性)
* 这个方法的返回值决定了rect范围内所有元素的排布(frame)
*/
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// 获得super已经计算好的布局属性
NSArray *array = [super layoutAttributesForElementsInRect:rect] ;
// 计算collectionView最中心点的x值
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;
// 在原有布局属性的基础上,进行微调
for (UICollectionViewLayoutAttributes *attrs in array) {
// cell的中心点x 和 collectionView最中心点的x值 的间距
CGFloat delta = ABS(attrs.center.x - centerX);
// 根据间距值 计算 cell的缩放比例
CGFloat scale = 1 - delta / self.collectionView.frame.size.width;
// 设置缩放比例
attrs.transform = CGAffineTransformMakeScale(scale, scale);
}
return array;
}
/**
* 这个方法的返回值,就决定了collectionView停止滚动时的偏移量
*/
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
// 计算出最终显示的矩形框
CGRect rect;
rect.origin.y = 0;
rect.origin.x = proposedContentOffset.x;
rect.size = self.collectionView.frame.size;
// 获得super已经计算好的布局属性
NSArray *array = [super layoutAttributesForElementsInRect:rect];
// 计算collectionView最中心点的x值
CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;
// 存放最小的间距值
CGFloat minDelta = MAXFLOAT;
for (UICollectionViewLayoutAttributes *attrs in array) {
if (ABS(minDelta) > ABS(attrs.center.x - centerX)) {
minDelta = attrs.center.x - centerX;
}
}
// 修改原有的偏移量
proposedContentOffset.x += minDelta;
return proposedContentOffset;
}
@end
九
假如我们要监听cell的点击,要怎么办呢?上面这讲的这些都和CollectionViewCell的点击没有关系,只是和布局有关。监听CollectionViewCell的点击和CollectionViewCell的布局没有任何关系,布局只负责展示,格子里面是什么内容,还是取决于cell
布局的作用仅仅是控制cell的排布
控制器先成为CollectionViewCell的代理:UICollectionViewDelegate
现在要把数据填充上去,让它显示相册了,所以自定义CollectionViewCell--CYPhotoCell,由于里面是固定死的,所以加一个Xib文件,里面加一个ImageView,拖线给一个属性,给ImageView一个标识photo
给cell里面的相片加上一个相册相框的效果--两种方案:
第一种方案:在Xib的ImageView的布局上下左右都给一个10的间距,给一个white的背景颜色
第二种方案:给我们的ImageView加一个图层就可以了
- (void)awakeFromNib {
self.imageView.layer.borderColor = [UIColor whiteColor].CGColor;
self.imageView.layer.borderWidth = 10;
}
在CYPhotoCell.h文件中
#import
@interface CYPhotoCell : UICollectionViewCell
/** 图片名 */
@property (nonatomic, copy) NSString *imageName;
@end
在CYPhotoCell.m文件中
#import "CYPhotoCell.h"
@interface CYPhotoCell()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation CYPhotoCell
- (void)awakeFromNib {
self.imageView.layer.borderColor = [UIColor whiteColor].CGColor;
self.imageView.layer.borderWidth = 10;
}
- (void)setImageName:(NSString *)imageName
{
_imageName = [imageName copy];
self.imageView.image = [UIImage imageNamed:imageName];
}
@end
在ViewController.m文件中
#import "ViewController.h"
#import "CYLineLayout.h"
#import "CYPhotoCell.h"
@interface ViewController ()
@end
@implementation ViewController
static NSString * const CYPhotoId = @"photo";
- (void)viewDidLoad {
[super viewDidLoad];
// 创建布局
CYLineLayout *layout = [[CYLineLayout alloc] init];
layout.itemSize = CGSizeMake(100, 100);
// 创建CollectionView
CGFloat collectionW = self.view.frame.size.width;
CGFloat collectionH = 200;
CGRect frame = CGRectMake(0, 150, collectionW, collectionH);
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
collectionView.dataSource = self;
collectionView.delegate = self;
[self.view addSubview:collectionView];
// 注册
[collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([CYPhotoCell class]) bundle:nil] forCellWithReuseIdentifier:CYPhotoId];
}
#pragma mark -
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 20;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CYPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CYPhotoId forIndexPath:indexPath];
cell.imageName = [NSString stringWithFormat:@"%zd", indexPath.item + 1];
return cell;
}
#pragma mark -
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"------%zd", indexPath.item);
}
@end
最后就实现了:
十
自定义流水布局
自定义布局 - 继承UICollectionViewFlowLayout
重写prepareLayout方法
作用:
- 在这个方法中做一些初始化操作
注意:
- 一定要调用[super prepareLayout]
重写layoutAttributesForElementsInRect:方法
作用:
- 这个方法的返回值是个数组
- 这个数组中存放的都是UICollectionViewLayoutAttributes对象
- UICollectionViewLayoutAttributes对象决定了cell的排布方式(frame等)
重写shouldInvalidateLayoutForBoundsChange:方法
作用:
- 如果返回YES,那么collectionView显示的范围发生改变时,就会重新刷新布局
一旦重新刷新布局,就会按顺序调用下面的方法:
- prepareLayout
- layoutAttributesForElementsInRect:
重写targetContentOffsetForProposedContentOffset:方法
作用:
- 返回值决定了collectionView停止滚动时最终的偏移量(contentOffset)
参数:
- proposedContentOffset:原本情况下,collectionView停止滚动时最终的偏移量
- velocity:滚动速率,通过这个参数可以了解滚动的方向(根据X和Y的正负)
自定义布局--CollectionViewLayout--格子布局
分析一下这个布局的排布是有规律的:
这里的相册布局和上面的流水布局不同
我们较上面的不需要更改太多东西,只是修改它的布局方式就行了
六个为一组
对应cell相差两个高度
一个这样的布局如何实现?
首先这里不不好用流水布局,流水布局的ItemSize是一样大的
肯定也牵扯到了循环利用,所以仍然用CollectionView,所以就用一个最根的布局--CollectionViewLayout
CollectionViewLayout它不像流水布局,内部没有任何方法给你去排,所以你只有继承自它,然后自己去写一套排布方式,排布是由我们来算
将上面文件中的CYLineLayout删除,New一个File--CYGridLayout继承自CollectionViewLayout
// 创建UICollectionViewLayoutAttributes
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
说白了我这个UICollectionViewLayoutAttributes是描述一个cell用的
indexPath代表了对应某个位置的cell,也就是说我这个UICollectionViewLayoutAttributes是描述哪个位置的cell
通过观察可以发现规律
在ViewController.m文件中修改一下collectionView的frame和布局
#import "ViewController.h"
#import "CYGridLayout.h"
#import "CYPhotoCell.h"
@interface ViewController ()
@end
@implementation ViewController
static NSString * const CYPhotoId = @"photo";
- (void)viewDidLoad {
[super viewDidLoad];
// 创建布局
CYGridLayout *layout = [[CYGridLayout alloc] init];
// 创建CollectionView
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
collectionView.dataSource = self;
collectionView.delegate = self;
[self.view addSubview:collectionView];
// 注册
[collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([CYPhotoCell class]) bundle:nil] forCellWithReuseIdentifier:CYPhotoId];
}
CYGridLayout里面去实现collectionView具体的布局
在CYGridLayout.m文件中
#import "CYGridLayout.h"
@interface CYGridLayout()
/** 所有的布局属性 */
@property (nonatomic, strong) NSMutableArray *attrsArray;
@end
@implementation CYGridLayout
- (NSMutableArray *)attrsArray
{
if (!_attrsArray) {
_attrsArray = [NSMutableArray array];
}
return _attrsArray;
}
- (void)prepareLayout
{
[super prepareLayout];
[self.attrsArray removeAllObjects];
NSInteger count = [self.collectionView numberOfItemsInSection:0];
for (int i = 0; i < count; i++) {
// 创建UICollectionViewLayoutAttributes
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
// 设置布局属性
CGFloat width = self.collectionView.frame.size.width * 0.5;
if (i == 0) {
CGFloat height = width;
CGFloat x = 0;
CGFloat y = 0;
attrs.frame = CGRectMake(x, y, width, height);
} else if (i == 1) {
CGFloat height = width * 0.5;
CGFloat x = width;
CGFloat y = 0;
attrs.frame = CGRectMake(x, y, width, height);
} else if (i == 2) {
CGFloat height = width * 0.5;
CGFloat x = width;
CGFloat y = height;
attrs.frame = CGRectMake(x, y, width, height);
} else if (i == 3) {
CGFloat height = width * 0.5;
CGFloat x = 0;
CGFloat y = width;
attrs.frame = CGRectMake(x, y, width, height);
} else if (i == 4) {
CGFloat height = width * 0.5;
CGFloat x = 0;
CGFloat y = width + height;
attrs.frame = CGRectMake(x, y, width, height);
} else if (i == 5) {
CGFloat height = width;
CGFloat x = width;
CGFloat y = width;
attrs.frame = CGRectMake(x, y, width, height);
} else {
UICollectionViewLayoutAttributes *lastAttrs = self.attrsArray[i - 6];
CGRect lastFrame = lastAttrs.frame;
lastFrame.origin.y += 2 * width;
attrs.frame = lastFrame;
}
// 添加UICollectionViewLayoutAttributes
[self.attrsArray addObject:attrs];
}
}
运行程序:
你会发现无法使它往上滚动,这是为啥呢?
因为你现在时继承自最根本的布局CollectionViewLayout,很多东西是得自己去设置了才会有,来到头文件,你会发现
要重写它的(CGSize)collectionViewContentSize方法,告诉它你这个CollectionView的内容尺寸,来决定它怎么滚。所以你现在无法滚动是因为CollectionView的ContentSize没有确定
/**
* 返回collectionView的内容大小
*/
- (CGSize)collectionViewContentSize
{
int count = (int)[self.collectionView numberOfItemsInSection:0];
int rows = (count + 3 - 1) / 3;
CGFloat rowH = self.collectionView.frame.size.width * 0.5;
return CGSizeMake(0, rows * rowH);
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
return self.attrsArray;
}
这里在性能优化上是还有点小问题的,因为我们一口气把所有东西都算完了。你如果觉得费时,完全可以把计算放在子线程中,然后返回到主线程刷新UI(CollectionViewLayout布局中有一个刷新方法,你调一下就行了)
计算不是重点,你是可以总结出计算的规律的。重点是:继承自CollectionViewLayout你需要注意什么?
1.一旦你重写了layoutAttributesForElementsInRect这个方法,就意味着所有东西你得自己写了,你的Attributes对象得自己创建了,因为它的父类不会帮你创建
2.一旦你继承自CollectionViewLayout,意味着你这个collectionViewContentSize都得告诉它了,这个是得你自己去算的
3.如果你是希望一口气把所有东西算完,不希望它在滚动过程中再算,你可以在prepareLayout方法里面先算清楚,算完后尽管它传的矩形框都不一样,但是我返回的还是同一份。
这里给出一个思想:
以后,你凡事牵扯到内容是很多很多的,你想做什么循环利用,而且布局又乱七八糟的,我们用CollectionViewLayout就可以了。我们只有继承自这个CollectionViewLayout,然后我们实现layoutAttributesForElementsInRect这个方法,在那里去告诉它,你的cell怎么去排。并且继承自CollectionViewLayout,意味着很多东西都要重写,如:collectionViewContentSize
这样就实现了:
自定义布局--CollectionViewLayout--布局之间的切换
要求:
实现一个环形布局和水平布局的相册,点击屏幕能够进行不同布局之间的切换
点击cell的时候可以删除cell
首先通过分析,在上面第一个案例的基础上,再添加一个环形布局--CYCircleLayout,肯定也是只能继承自CollectionViewLayout
在这里CYCircleLayout里面就只需要实现prepareLayout方法和layoutAttributesForElementsInRect方法,不需再要重写实现collectionViewContentSize的方法,因为它不需要滚动,所以CollectionViewLayout里面所有方法的实现是看你的需求的
#import "CYCircleLayout.h"
@interface CYCircleLayout()
/** 布局属性 */
@property (nonatomic, strong) NSMutableArray *attrsArray;
@end
@implementation CYCircleLayout
- (NSMutableArray *)attrsArray
{
if (!_attrsArray) {
_attrsArray = [NSMutableArray array];
}
return _attrsArray;
}
- (void)prepareLayout
{
[super prepareLayout];
[self.attrsArray removeAllObjects];
NSInteger count = [self.collectionView numberOfItemsInSection:0];
for (int i = 0; i < count; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attrsArray addObject:attrs];
}
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
return self.attrsArray;
}
我们可以看出,每个相片cell的中心点都在一个圆上,所以我们要将它摆正,肯定不是设置它们的frame,而是去设置它center这个值,我只要保证它的center那个值在那个圆上就可以了
也就是说我们要算出每个相片cell的中心点的X和Y值,通过中心点来布局它,而不是通过frame的original的X和Y(这样太麻烦,不好算)
这里我们只要确定圆心就好算了
圆心(X和Y值分别是CollectionView宽度和高度的一半)
而且每张相片的中心点距离圆心的距离为半径
你会发现每个相片cell的中心点的X,Y和圆心的X,Y之间的差值是有规律的:
Y值--圆心点的Y值-(Y*cosa)= cell的Y值,X值同样道理去算
角度a的大小取决于cell的个数(假如20个cell--->a = 360° / 20)
所以我们只要算出平分角度就行了
比如说第一个cell为索引0,角度就是0,第二个为索引1,角度就是a, 第三个为索引2,角度就是a2......第i个为索引i-1,角度就是a(i-1 )
于是乎
这里记住:如果你是继承自CollectionViewLayout,如果你要换布局话,有一个方法是一定得实现的--layoutAttributesForItemAtIndexPath:方法。只有继承CollectionViewLayout才需要,流水布局不需要,因为流水布局内部早已经帮你实现了这个方法
/**
* 这个方法需要返回indexPath位置对应cell的布局属性
*/
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger count = [self.collectionView numberOfItemsInSection:0];
CGFloat radius = 70;
// 圆心的位置
CGFloat oX = self.collectionView.frame.size.width * 0.5;
CGFloat oY = self.collectionView.frame.size.height * 0.5;
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attrs.size = CGSizeMake(50, 50);
if (count == 1) {
attrs.center = CGPointMake(oX, oY);
} else {
CGFloat angle = (2 * M_PI / count) * indexPath.item;
CGFloat centerX = oX + radius * sin(angle);
CGFloat centerY = oY + radius * cos(angle);
attrs.center = CGPointMake(centerX, centerY);
}
return attrs;
}
点击屏幕切换布局
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if ([self.collectionView.collectionViewLayout isKindOfClass:[CYLineLayout class]]) {
[self.collectionView setCollectionViewLayout:[[CYCircleLayout alloc] init] animated:YES];
} else {
CYLineLayout *layout = [[CYLineLayout alloc] init];
layout.itemSize = CGSizeMake(100, 100);
[self.collectionView setCollectionViewLayout:layout animated:YES];
}
}
点击cell就把cell删掉
这里要注意的是:
你要把cell删掉了,对应的模型或者说 数据也是得改变的
可变数组,先把所有图片名放进去
@interface ViewController ()
/** collectionView */
@property (nonatomic, weak) UICollectionView *collectionView;
/** 数据 */
@property (nonatomic, strong) NSMutableArray *imageNames;
@end
@implementation ViewController
static NSString * const CYPhotoId = @"photo";
- (NSMutableArray *)imageNames
{
if (!_imageNames) {
_imageNames = [NSMutableArray array];
for (int i = 0; i<20; i++) {
[_imageNames addObject:[NSString stringWithFormat:@"%zd", i + 1]];
}
}
return _imageNames;
}
数据源里面的东西也是得改变的
#pragma mark -
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.imageNames.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CYPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CYPhotoId forIndexPath:indexPath];
cell.imageName = self.imageNames[indexPath.item];
return cell;
}
你要把cell删掉,也得保证把模型也删掉了(不可能你cell删掉了,数据还是这么多,那就出问题了)
#pragma mark -
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
[self.imageNames removeObjectAtIndex:indexPath.item];
[self.collectionView deleteItemsAtIndexPaths:@[indexPath]];
}
删除到最后一个的时候,让最后一个cell的位置来到圆心
if (count == 1) {
attrs.center = CGPointMake(oX, oY);
} else {
CGFloat angle = (2 * M_PI / count) * indexPath.item;
CGFloat centerX = oX + radius * sin(angle);
CGFloat centerY = oY + radius * cos(angle);
attrs.center = CGPointMake(centerX, centerY);
}
这样所有的逻辑就理清楚了
在CYCircleLayout.m文件中
#import "CYCircleLayout.h"
@interface CYCircleLayout()
/** 布局属性 */
@property (nonatomic, strong) NSMutableArray *attrsArray;
@end
@implementation CYCircleLayout
- (NSMutableArray *)attrsArray
{
if (!_attrsArray) {
_attrsArray = [NSMutableArray array];
}
return _attrsArray;
}
- (void)prepareLayout
{
[super prepareLayout];
[self.attrsArray removeAllObjects];
NSInteger count = [self.collectionView numberOfItemsInSection:0];
for (int i = 0; i < count; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attrsArray addObject:attrs];
}
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
return self.attrsArray;
}
/**
* 这个方法需要返回indexPath位置对应cell的布局属性
*/
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger count = [self.collectionView numberOfItemsInSection:0];
CGFloat radius = 70;
// 圆心的位置
CGFloat oX = self.collectionView.frame.size.width * 0.5;
CGFloat oY = self.collectionView.frame.size.height * 0.5;
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attrs.size = CGSizeMake(50, 50);
if (count == 1) {
attrs.center = CGPointMake(oX, oY);
} else {
CGFloat angle = (2 * M_PI / count) * indexPath.item;
CGFloat centerX = oX + radius * sin(angle);
CGFloat centerY = oY + radius * cos(angle);
attrs.center = CGPointMake(centerX, centerY);
}
return attrs;
}
@end
在ViewController.m文件中
#import "ViewController.h"
#import "CYLineLayout.h"
#import "CYCircleLayout.h"
#import "CYPhotoCell.h"
@interface ViewController ()
/** collectionView */
@property (nonatomic, weak) UICollectionView *collectionView;
/** 数据 */
@property (nonatomic, strong) NSMutableArray *imageNames;
@end
@implementation ViewController
static NSString * const CYPhotoId = @"photo";
- (NSMutableArray *)imageNames
{
if (!_imageNames) {
_imageNames = [NSMutableArray array];
for (int i = 0; i<20; i++) {
[_imageNames addObject:[NSString stringWithFormat:@"%zd", i + 1]];
}
}
return _imageNames;
}
- (void)viewDidLoad {
[super viewDidLoad];
// 创建布局
CYCircleLayout *layout = [[CYCircleLayout alloc] init];
// 创建CollectionView
CGFloat collectionW = self.view.frame.size.width;
CGFloat collectionH = 200;
CGRect frame = CGRectMake(0, 150, collectionW, collectionH);
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
collectionView.dataSource = self;
collectionView.delegate = self;
[self.view addSubview:collectionView];
self.collectionView = collectionView;
// 注册
[collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([CYPhotoCell class]) bundle:nil] forCellWithReuseIdentifier:CYPhotoId];
// 继承UICollectionViewLayout
// 继承UICollectionViewFlowLayout
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if ([self.collectionView.collectionViewLayout isKindOfClass:[CYLineLayout class]]) {
[self.collectionView setCollectionViewLayout:[[CYCircleLayout alloc] init] animated:YES];
} else {
CYLineLayout *layout = [[CYLineLayout alloc] init];
layout.itemSize = CGSizeMake(100, 100);
[self.collectionView setCollectionViewLayout:layout animated:YES];
}
}
#pragma mark -
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.imageNames.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CYPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CYPhotoId forIndexPath:indexPath];
cell.imageName = self.imageNames[indexPath.item];
return cell;
}
#pragma mark -
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
[self.imageNames removeObjectAtIndex:indexPath.item];
[self.collectionView deleteItemsAtIndexPaths:@[indexPath]];
}
@end
这样就实现了
如果觉得对你有帮助,Give me a star
© 著作权归作者所有
写了 79591 字,被 1072 人关注,获得了 1468 个喜欢
花自飘零水自流
如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!
赞赏支持
2楼 · 2015.11.15 10:37
思路学到了
3楼 · 2015.11.15 22:14
不错不错正愁这块问题没有解决呢
4楼 · 2015.11.16 19:49
不错 自己试一下先。
5楼 · 2015.12.25 16:42
吊
6楼 · 2016.01.07 17:10
很赞,思路很清晰,细节也讲解得很不错😄
7楼 · 2016.01.14 13:10
CollectionViewLayout布局中有一个刷新方法 指的是哪个方法?
8楼 · 2016.01.17 00:29
喜欢,正好刚自学到这个
10楼 · 2016.06.18 20:25
感谢 ! 感谢您 !
能对你有帮助我也很开心!
11楼 · 2016.07.11 17:27
希望楼主回复,你的cyCircleLayout和cygridlayout切换为什么会崩溃呢,求解啊
EagleOne:@李淮扬_Eagle你写文中贴的两个是正常的
12楼 · 2016.07.14 10:40
博主我发现一个bug求解!就是第一横向滚动布局的demo,如果将layout.itemSize = CGSizeMake(100, 100);改为成CGSizeMake(55, 100),在滚动到最后一cell后,轻轻拉一点,不超过cell宽度的一半,collectionView不会滚动到最后一个cell的中点位置。我觉得是float的算法问题,不知道博主能不能解决
我解决了,bug应该和我想的一致,问题出在float 上,加上两个判断就好了
// 这句是判断第一个cell 的bug
if (proposedContentOffset.x < 0) {
proposedContentOffset.x = 0;
}
// 这句是最后的cell 的bug
if (proposedContentOffset.x + self.collectionView.frame.size.width > self.collectionView.contentSize.width) {
proposedContentOffset.x = self.collectionView.contentSize.width - self.collectionView.frame.size.width;
}
哦对了,加在
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
这个函数返回之前。
我其实觉得貌似所有写这个时函数都要加一遍。。
13楼 · 2016.07.15 10:59
迄今为止写的最详细的CollectionView 文章,
14楼 · 2016.07.15 11:03
我有个搜索文字历史的需求 用 collectionView 高度相等,宽度不一的 cell
我如何设置cell的最大间距相等,
因为我发现, flowLayout 会自动均分 cell 间的间距, 这样就造成了间距不一的情况.
lwhldy:最后解决了吗?能请教下方法吗?我的需求和你的一样
Unc1eWang:@lwhldy自定义flowlayout 就行了
lwhldy:@Unc1eWang谢谢回复,还请教一下,需要继承collectionViewlayout吗 还是只要继承UICollectionViewFlowLayout就OK?具体重写那个父类方法呢?
16楼 · 2016.09.12 18:39
你好,第一种水平滚动的效果。如何一开始就默认滚动到指定的indexPath.row呢?调用
- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated;
貌似不行呢?
作者:Tuberose
链接:http://www.jianshu.com/p/83f2d6ac7e68
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
网友评论