美文网首页ioscollectionView
自定义布局和自定义流水布局(CollectionViewLayo

自定义布局和自定义流水布局(CollectionViewLayo

作者: 大冲哥 | 来源:发表于2017-10-26 10:13 被阅读307次

    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

    來源:简书

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

    相关文章

      网友评论

        本文标题:自定义布局和自定义流水布局(CollectionViewLayo

        本文链接:https://www.haomeiwen.com/subject/ikmhpxtx.html