自定义布局和自定义流水布局(CollectionViewLayout和CollectionViewFlowLayout)

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

iOS开发实践

© 著作权归作者所有

举报文章

关注Tuberose

写了 79591 字,被 1072 人关注,获得了 1468 个喜欢

花自飘零水自流

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

赞赏支持

喜欢

116

更多分享

24条评论只看作者关闭评论

按喜欢排序按时间正序按时间倒序

zZ爱吃菜

2楼 · 2015.11.15 10:37

思路学到了

回复举报

Slyneil

3楼 · 2015.11.15 22:14

不错不错正愁这块问题没有解决呢

回复举报

CoderRyan

4楼 · 2015.11.16 19:49

不错 自己试一下先。

回复举报

newbiecoder

5楼 · 2015.12.25 16:42

回复举报

鋒芒毕露

6楼 · 2016.01.07 17:10

很赞,思路很清晰,细节也讲解得很不错😄

回复举报

寂静的天空

7楼 · 2016.01.14 13:10

CollectionViewLayout布局中有一个刷新方法 指的是哪个方法?

回复举报

WilliamAlex大叔

8楼 · 2016.01.17 00:29

喜欢,正好刚自学到这个

回复举报

Aprilx

10楼 · 2016.06.18 20:25

感谢 ! 感谢您 !

回复举报

Tuberose@Aprilx哈哈

能对你有帮助我也很开心!

2016.06.18 20:27回复举报

添加新评论

EagleOne

11楼 · 2016.07.11 17:27

希望楼主回复,你的cyCircleLayout和cygridlayout切换为什么会崩溃呢,求解啊

回复举报

EagleOne@李淮扬_Eagle你写文中贴的两个是正常的

2016.07.11 17:28回复举报

添加新评论

kerten

12楼 · 2016.07.14 10:40

博主我发现一个bug求解!就是第一横向滚动布局的demo,如果将layout.itemSize = CGSizeMake(100, 100);改为成CGSizeMake(55, 100),在滚动到最后一cell后,轻轻拉一点,不超过cell宽度的一半,collectionView不会滚动到最后一个cell的中点位置。我觉得是float的算法问题,不知道博主能不能解决

回复举报

kerten@kerten

我解决了,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

这个函数返回之前。

我其实觉得貌似所有写这个时函数都要加一遍。。

2016.07.14 15:56回复举报

添加新评论

Unc1eWang

13楼 · 2016.07.15 10:59

迄今为止写的最详细的CollectionView 文章,

回复举报

Unc1eWang

14楼 · 2016.07.15 11:03

我有个搜索文字历史的需求 用 collectionView  高度相等,宽度不一的 cell

我如何设置cell的最大间距相等,

因为我发现, flowLayout 会自动均分 cell 间的间距, 这样就造成了间距不一的情况.

回复举报

lwhldy:最后解决了吗?能请教下方法吗?我的需求和你的一样

2017.01.06 23:01回复举报

Unc1eWang@lwhldy自定义flowlayout 就行了

2017.01.06 23:03回复举报

lwhldy@Unc1eWang谢谢回复,还请教一下,需要继承collectionViewlayout吗 还是只要继承UICollectionViewFlowLayout就OK?具体重写那个父类方法呢?

2017.01.06 23:19回复举报

添加新评论

言末

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

來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

推荐阅读更多精彩内容