详解CollectionViewLayout和CollectionViewFlowLayout

  • 作者:王颖博
  • 最后编辑:2016年05月16日
  • 标签: iOS

you say what ? I say yes!


1
2
3
4
5
前面为我自己缩写的,是我自己的项目,后面是根据一位简书大神的作品.
我项目链接:**[YBCollectionViewLayout](https://github.com/wangyingbo/YBCollectionViewLayout)**
目中包含了三个小的demo。
具体实现过程参考项目里的read.md文件
原博主链接<http://www.jianshu.com/p/83f2d6ac7e68> ---
  • 第一个是横向滑动的collectionView,优化了滑动结束的显示问题,当滑动结束的时候只有一个cell在最中央,其他cell缩放。 - 截图0

  • 第二个demo是简单实现了瀑布流的原理,代码什么简单。 - 截图1

  • 第三个demo是在布局间的切换,当点击屏幕的时候自动刷新布局。 - 截图2

  • 这里详解了三个demo去帮助大家更好的了解CollectionViewLayout和CollectionViewFlowLayout-

  • Github地址:-CollectionViewLayout-CollectionViewFlowLayout-

- 自定义流水布局–CollectionViewFlowLayout—水平布局实现一个相册功能

  • 在UIScrollView的基础上进行循环利用
    • 那怎么去做循环利用呢?
  • 第一种方案:
    • 实时监控ScrollView的滚动,一旦有一个家伙离开屏幕,我们就把它放进一个数组或者是集合里面去,到时候我要用,我就把它拿过去用
    • 但是这个是很麻烦的,因为你总是得判断它有没有离开屏幕
  • 第二种方案:
    • 用苹果自带的几个类:TableView或者是CollectionView
    • 因为它们本来就具备循环利用的功能
    • 但是TableView一看就不符合要求,因为它默认就是上下竖直滚动,不是左右水平滚动
      • 当然我们也可以用非主流的方式,让TableView实现水平滚动
      • 让TableView的Transform来个90°,让它里面所有的cell也翻个90°,都转过来。但这种做法有点奇葩,开发中还是不要这么搞
      • 所以我们可以用CollectionView
    • CollectionView在我们的印象中是展示像那种九宫格的样子,而且也是上下竖直滚动
    • 但是CollectionView和TableView的区别就是:
      • CollectionView它默认就支持水平滚动,你只要修改它一个属性为水平方向就行了。而TableView默认支持竖直滚动,没有属性去支持它水平滚动,除非你去搞一些非主流的做法

  • CollectionView一定要传一个不空的Layout那个参数,因为默认的布局是九宫格,它按这种方式排的原因是它有一个流水布局。正因为给它传了一个流水布局,所以它就一行满了,就流向下一行,流水一样流下去流过来
1
    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:[UICollectionViewlayout alloc] init]];
  • **数据源方法 - **
  • numberOfItemsInSection是告诉它一组有多少个格子
1
2
3
4
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 50 ;
}
  • cellForItemAtIndexPath告诉它每个格子长出来是怎样的一个cell,因为每个格子都是一个CollectionViewCell
1
2
3
4
5
6
7
8
- (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决定了它的滚动方向,设置它滚动的方向为水平
1
2
    // 水平滚动
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
  • itemSize决定了CollectionView布局的里面的cell的大小
1
    layout.itemSize = CGSizeMake(100, 100);
  • 你将CollectionView高度改小点,比如200,那么你的高度不够显示两排,就会如下显示:

  • 而且你会发现不用担心循环利用的问题,CollectionView内部已经帮你做好了


  • 我们现在已经实现流水布局水平滚动,而且做好了循环利用。如果要做一层改进,那么我们就要自定义布局,自己来写一套布局,所以现在我们继承于UICollectionViewFlowLayout
  • 我们要自定义CollectionView的布局有两种方案
    • 1.继承UICollectionViewLayout
      • 一般是继承于UICollectionViewLayout就行了
      • 而且UICollectionViewFlowLayout继承于UICollectionViewLayout
      • 但是如果你自定义继承于UICollectionViewLayout,代表着你没有流水布局功能,也就是在你不想要流水布局功能的时候就选择继承UICollectionViewLayout
    • 2.继承UICollectionViewFlowLayout

      **四 **

  • 所以我们自定义流水布局CYLineLayout
  • 在CYLineLayout.h文件中
1
2
3
4
5
#import <UIKit/UIKit.h>

@interface CYLineLayout : UICollectionViewFlowLayout

@end

  • 在CYLineLayout.m文件中重写某些方法去实现:
    • 1.cell的放大与缩小
    • 2.停止滚动的时候:cell居中
  • 进入头文件可以发现要重写的一些方法

  • UICollectionViewLayoutAttributes
    • 1.它是描述布局属性的
    • 2.一个cell对应一个UICollectionViewLayoutAttributes对象
    • 3.UICollectionViewLayoutAttributes对象决定了cell的展示样式(frame)说白了就是决定你的cell摆在哪里,怎么去摆


  • layoutAttributesForElementsInRect这个方法的返回值是一个数组(数组里面存放着rect范围内所有元素的布局属性)
  • 这个方法的返回值决定了rect范围内所有元素的排布(frame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (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中心点的差值
1
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;

  • cell的中心点x 和CollectionView最中心点的x值 的间距
1
     CGFloat delta = ABS(attrs.center.x - centerX);
1
2
     ABS(A)
     // 表示取绝对值 

  • 我们再根据间距值delta去算cell的缩放比例scale
    • 间距值delta和缩放比例scale是成反比的
    • 间距值delta的范围为0–self.collectionView.frame.size.width * 0.5
1
2
CGFloat scale = 1 - delta / self.collectionView.frame.size.width;
// 用1-(),是因为间距值delta和缩放比例scale是成反比的
  • 设置缩放比例
1
attrs.transform = CGAffineTransformMakeScale(scale, scale);

  • 但是设置后你会发现基本没啥反应,显示还乱七八糟的,这是什么原因呢?
    • 我们是想要稍微动一下就修改一下,但是现在没法达到我动一下就根据最新的中心点X来再算一遍一边比例。没有实现这个代码
    • 因为这里还需要实现一个方法
  • 这个方法是shouldInvalidateLayoutForBoundsChange: 它的特点是:
    • 默认return NO
    • 当collectionView的显示范围发生改变的时候,判断是否需要重新刷新布局
    • 一旦重新刷新布局,就会重新调用下面的方法:
      • 1.prepareLayout
      • 2.layoutAttributesForElementsInRect:方法
1
2
3
4
5
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return YES;
  
}
  • 这样之后,你会发现,你稍微挪一下,它就重新算一遍,比例就会缩放, 达到了我们的要求
  • 而且非常流畅,因为它有循环利用

  • 还要实现一个方法:targetContentOffsetForProposedContentOffset:()方法。它的返回值,就决定了collectionView停止滚动时的偏移量
  • 这个方法在你手离开屏幕之前会调用,也就是cell即将停止滚动的时候 (记住这一点)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (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已经计算好的布局属性
1
    NSArray *array = [super layoutAttributesForElementsInRect:rect];
  • 这里为什么不用self
    • 因为如果调self,又会来到layoutAttributesForElementsInRect:()方法的for循环中, 将transform再算一遍。而我们只想要拿到中心点X值。靠父类就行了
    • 我们调super这个方法,因为它当时已经算好了cell的中心点等X的值了。所以这里调super可能更好一点

  • 计算collectionView最中心点的x值
    1
    
     CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;
    
    • 这里为什么不按前面
      1
      
      CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;
      

      来算呢?

    • 因为targetContentOffsetForProposedContentOffset:()方法在你手离开屏幕之前会调用,也就是cell即将停止滚动的时候,这个时候我们要算的是最后停下来偏移量。
    • 假如我们用力往左边一甩,你的手已经离开,算的偏移量是你手离开时候的偏移量,而不是我们最终的偏移量,也就是说这么算的话,我们就算错了
    • 你是应该拿到最终停下来的cell和CollectionView的中心点的X值进行比较的。所以你应该最终的值,而不是手松开的那一刻的偏移量的值
      • 那我们怎么知道手松开的那一刻最终的偏移量X的值呢?
        • 这个方法返回的参数(CGPoint)proposedContentOffset,这是它本应该停留的位置,最终停留的的值。而(CGPoint)targetContentOffsetForProposedContentOffset:这个是你最终返回的值,也就是你要它停留到哪儿的值(这个参数决定你要cell最后停留在哪儿)

  • 同上面可知,我们最后拿到的矩形框也是不能乱传的,也是要拿到最终的哪一个矩形框(不明白,就想像一下,你往左边或者右边用手指一甩的时候,手离开的时候是一个值,最终停下来是一个值,而现在我们需要的是最终的值)
1
2
    // 计算出最终显示的矩形框 
    CGRect rect; rect.origin.y = 0; rect.origin.x = proposedContentOffset.x; rect.size = self.collectionView.frame.size;


  • 然后我们要找最短的偏移量,找到它,然后就让他偏移它的那个值,让它的中心点回到collectionView的中心点,也就是说重合。这样就实现了不管你怎么去甩,等cell停下来的时候。都会有一个cell它会停留在矩形框CollectionView的中心
1
2
3
4
5
6
7
8
9
10
    // 存放最小的间距值
         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,这里一般是做初始化操作
1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 用来做布局的初始化操作(不建议在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文件中
1
2
3
4
5
#import <UIKit/UIKit.h>

@interface CYLineLayout : UICollectionViewFlowLayout

@end
  • 在CYLineLayout.h文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#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加一个图层就可以了
1
2
3
4
- (void)awakeFromNib {
       self.imageView.layer.borderColor = [UIColor whiteColor].CGColor; 
       self.imageView.layer.borderWidth = 10;
 }
  • 在CYPhotoCell.h文件中
1
2
3
4
5
6
#import <UIKit/UIKit.h>

@interface CYPhotoCell : UICollectionViewCell
/** 图片名 */
@property (nonatomic, copy) NSString *imageName;
@end
  • 在CYPhotoCell.m文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#import "ViewController.h"
#import "CYLineLayout.h"
#import "CYPhotoCell.h"

@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
@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 - <UICollectionViewDataSource>
- (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 - <UICollectionViewDelegate>
- (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

      1
      2
      3
      
       // 创建UICollectionViewLayoutAttributes
       NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
       UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
      
  • 说白了我这个UICollectionViewLayoutAttributes是描述一个cell用的
  • indexPath代表了对应某个位置的cell,也就是说我这个UICollectionViewLayoutAttributes是描述哪个位置的cell
  • 通过观察可以发现规律


  • 在ViewController.m文件中修改一下collectionView的frame和布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import "ViewController.h"
#import "CYGridLayout.h"
#import "CYPhotoCell.h"

@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
@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文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#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没有确定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 返回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里面所有方法的实现是看你的需求的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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才需要,流水布局不需要,因为流水布局内部早已经帮你实现了这个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 这个方法需要返回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;
}
  • 点击屏幕切换布局
1
2
3
4
5
6
7
8
9
10
- (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删掉了,对应的模型或者说 数据也是得改变的
  • 可变数组,先把所有图片名放进去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
/** 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;
}

  • 数据源里面的东西也是得改变的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma mark - <UICollectionViewDataSource>
- (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删掉了,数据还是这么多,那就出问题了)
1
2
3
4
5
6
#pragma mark - <UICollectionViewDelegate>
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    [self.imageNames removeObjectAtIndex:indexPath.item];
    [self.collectionView deleteItemsAtIndexPaths:@[indexPath]];
}
  • 删除到最后一个的时候,让最后一个cell的位置来到圆心
1
2
3
4
5
6
7
8
9
    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文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#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文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#import "ViewController.h"
#import "CYLineLayout.h"
#import "CYCircleLayout.h"
#import "CYPhotoCell.h"

@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
/** 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:[XMGLineLayout class]]) {
        [self.collectionView setCollectionViewLayout:[[XMGCircleLayout alloc] init] animated:YES];
    } else {
        XMGLineLayout *layout = [[XMGLineLayout alloc] init];
        layout.itemSize = CGSizeMake(100, 100);
        [self.collectionView setCollectionViewLayout:layout animated:YES];
    }
}


#pragma mark - <UICollectionViewDataSource>
- (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 - <UICollectionViewDelegate>
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    [self.imageNames removeObjectAtIndex:indexPath.item];
    [self.collectionView deleteItemsAtIndexPaths:@[indexPath]];
}
@end


  • 这样就实现了