文章目录
  1. 1. 什么是UICollectionView
  2. 2. 实现一个简单的UICollectionView
    1. 2.1. UICollectionViewDataSource
      1. 2.1.1. 用于重用
    2. 2.2. UICollectionViewDelegate
    3. 2.3. 关于cell
    4. 2.4. UICollectionViewLayout
  3. 3. 自定义UICollectionViewLayout
  4. 4. UICollectionViewLayoutAttributes
  5. 5. UICollectionViewLayout
    1. 5.1. LineLayout——对于个别UICollectionViewLayoutAttributes的调整
    2. 5.2. CircleLayout——完全自定义的Layout,添加删除item,以及手势识别

什么是UICollectionView

UICollectionView是一种新的数据展示方式,简单来说可以把他理解成多列的UITableView。比如iBooks,一个虚拟的书架上放着各类图书,排列整齐,亦或者iPad的iOS6中的原生时钟中的各个时钟,也是UICollectionView的最简单的一个布局。

最简单的UICollectionView就是一个GirdView,可以以多列的方式来将数据进行展示,标准的UICollection包含三个部分:

  • Cells用于展示内容的主体,对于不同的cell可以指定不同尺寸和不同的内容
  • Supplementary Views追加视图,用来标记每个section的view
  • Decoration Views装饰视图,每个section的背景


实现一个简单的UICollectionView

和UITableView一样,UICollectionView同样采用datasource和delegate设计模式:datasource为View提供数据源,告诉View要实现什么及如何显示它们,delegate提供一些样式的小细节以及用户交互的相应。

UICollectionViewDataSource

    • (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section:section的数量
    • (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section:某个section里有多少个item
    • (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath:指定显示什么样的cell

实现了以上三个委托方法,基本上就可以保证CollectionView工作正常,同时还通过了Supplementary View方法

    • (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath

用于重用

在UICollectionView中在请求数据源之前要注册CellView、SupplementaryView,可以通过以下方法进行注册
-registerClass:forCellWithReuseIdentifier:
-registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
-registerNib:forCellWithReuseIdentifier:
-registerNib:forSupplementaryViewOfKind:withReuseIdentifier:

UICollectionViewDelegate

负责交互

  • cell的高亮
  • cell的选中状态
  • 可以支持长按后的菜单
    用户点击cell的时候,现在会按照以下流程向delegate进行询问:
    • collectionView:shouldHighlightItemAtIndexPath:是否应该高亮?
    • collectionView:didHighlightItemAtIndexPath:如果1回答是,那么高亮
    • collectionView:shouldSelectItemAtIndexPath:无论1结果如何,都询问是否可以被选中?
    • collectionView:didUnhighlightItemAtIndexPath:如果1回答是否,那么取消高亮
    • collectionView:didSelectItemAtIndexPath:如果3回答为是,那么选中cell

对应的高亮和选中状态分别由highlighted和selected两个属性表示。

关于cell

UICollectionViewCell相比UITableViewCell没有太多的花头,首先不存在格式各样的默认的style。UICollectionViewCell结构上相对比较简单,由下至上:

  1. cell本身作为容器View
  2. 一个大小自动适应整个cell的backgroundView,用于cell默认的背景
  3. selectedBackgroundView,是cell被选中时的背景
  4. contentView,自定义内容应被加载这个View上。

UICollectionViewLayout

负责各个cell、Supplementary View和Decoration Views进行组织,为了它们设定各自的属性,包括但不限于:位置、尺寸、透明度、层级关系、形状、等等…

  • Layout决定了UICollectionView是如何显示在界面上的。在展示之前,一般需要生成合适的UICollectionViewLayout子类对象,并将其赋予CollectionView的collectionViewLayout属性。

默认常用的Layout是UICollectionViewFlowLayout,是一个直线对齐的Layout。

  • itemSize,定义了每一个item的大小。通过设定itemSize可以全局改变所有cell的尺寸,如果想要对某个cell定制尺寸,可以使用-collectionView:layout:sizeForItemAtIndexPath:方法。
  • 间隔 指定每个item之间的间隔和每一行之间的间隔,和size类似,有全局属性,也可以对每一个item和每一个section做出设定:
    • @property (CGSize) minimumInteritemSpacing
    • @property (CGSize) minimumLineSpacing
    • -collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
    • -collectionView:layout:minimumLineSpacingForSectionAtIndex:
  • 滚动方向 由属性scrollDirection确定scroll View的方向,将影响Flow Layout的基本方向和由header及footer确定的section之间的宽度
    • UICollectionViewScrollDirectionVertical
    • UICollectionViewScrollDirectionHorizontal
  • Header和Footer尺寸 分为全局和部分,需要注意根据滚动方向不同,header和footer的高和宽中只有一个和会起作用,垂直滚动时section间宽度为该迟钝的高,而水平滚动时为宽度起作用
    • @property (CGSize) headerReferenceSize
    • @property (CGSize) footerReferenceSize
    • -collectionView:layout:referenceSizeForHeaderInSection:
    • -collectionView:layout:referenceSizeForFooterInSection:
  • 缩进
    • @property UIEdgeInsets sectionInset;
    • -collectionView:layout:insetForSectionAtIndex:

自定义UICollectionViewLayout

总结
一个UICollectionView的实现包括两个必要部分:UICollectionViewDataSource和UICollectionViewLayout,和一个交互部分:UICollectionViewDelegate。而Apple给出的UICollectionViewFlowLayout已经是一个很强力的layout方案了。

UICollectionViewLayoutAttributes

property列表:

  • @property (nonatomic) CGRect frame
  • @property (nonatomic) CGPoint center
  • @property (nonatomic) CGSize size
  • @property (nonatomic) CATransform3D transform3D
  • @property (nonatomic) CGFloat alpha
  • @property (nonatomic) NSInteger zIndex
  • @property (nonatomic, getter=isHidden) BOOL hidden

UICollectionViewLayoutAttributes的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。当UICollectionView在获取布局时将针对每一个indexPath的部件(包括cell,追加视图和装饰视图),向其上的UICollectionViewLayout实例询问该部件的布局信息。这个布局信息,就以UICollectionViewLayoutAttributes的实例的方式给出。

UICollectionViewLayout

UICollectionViewLayout的功能为向UICollectionView提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义layou的常规做法是继承UICollectionViewLayout类,然后重载下列方法。

    • (CGSize)collectioonViewContentSize
      • 返回collctionView的内容尺寸
    • (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
      • 返回rect中的所有的元素的布局属性
      • 返回的包括UICollectionViewLayoutAttributes的NSArray
      • UICollctionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的UICollctionViewLayoutAttributes
        • ayoutAttributesForCellWithIndexPath:
        • layoutAttributesForSupplementaryViewOfKind:withIndexPath:
        • layoutAttributesForDecorationViewOfKind:withIndexPath:
    • (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
      • 返回对应indexPath位置的cell布局属性
    • (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind atIndexPath:(NSIndexPath *)indexPath
      • 返回对应indexPath位置的追加视图的布局属性,如果没有追加视图可以不重载
    • (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind atIndexPath:(NSIndexPath *)indexPath
      • 返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
    • (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
      • 当边界发生改变时,是否应该刷新布局。

在初始化一个UICollectionViewLayout实例后,会有一系列的准备方法被自动调用,以保证layout实例的正确

  1. -(void)prepareLayout,默认下该方法是每都不做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。
  2. -(CGSize)collectionViewContentSize,以确定collection应该占据的尺寸,注意这里的尺寸不是指可使部分的尺寸,而是所有内容所占的尺寸。
  3. -(NSArray *)layoutAttributesForElementsInRect:(Rect)rect被调用,初始的layout外观将由该方法返回的UICollctionViewLayoutAttributes来决定。

另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect来生成更新后的布局。

LineLayout——对于个别UICollectionViewLayoutAttributes的调整

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
// LinrLayout.m
# import "LineLayout.h"


# define ITEM_SIZE 200.0
# define ACTIVE_DISTANCE 200
# define ZOOM_FACTOR 0.4

@implementation LineLayout

-(id)init
{
self = [super init];
if (self) {
self.itemSize = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
// 确定了缩进,此处为上方、下方各缩进200
self.sectionInset = UIEdgeInsetsMake(ITEM_SIZE, ITEM_SIZE/2, ITEM_SIZE, ITEM_SIZE/2);
// 每个item在水平方向的最小间距
self.minimumLineSpacing = ITEM_SIZE/4;

}
return self;
}
// 边界改变时是否重新排版
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)oldBounds
{
return YES;
}
// 初始的layout外观将由该方法返回的UICollctionViewLayoutAttributes来决定
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray* array = [super layoutAttributesForElementsInRect:rect];
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
for (UICollectionViewLayoutAttributes* attributes in array) {
if (CGRectIntersectsRect(attributes.frame, rect)) {
CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
CGFloat normalizedDistance = distance / ACTIVE_DISTANCE;
if (ABS(distance) < ACTIVE_DISTANCE) {
CGFloat zoom = 1 + ZOOM_FACTOR*(1 - ABS(normalizedDistance));
attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
attributes.zIndex = 1;
}
}
}
return array;
}

// 自动对齐到网格
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
// proposedContentOffset是没有对齐到网格时本来应该停下来的位置
CGFloat offsetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);
// 当前显示的区域
CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
// 取当前显示的item
NSArray* array = [super layoutAttributesForElementsInRect:targetRect];
// 对当前屏幕中的UICollectionViewLayoutAttributes逐个与屏幕中心进行比较,找出最接近中心的一个
for (UICollectionViewLayoutAttributes* layoutAttributes in array) {
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment)) {
offsetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

@end

例子

CircleLayout——完全自定义的Layout,添加删除item,以及手势识别

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
# import "CircleLayout.h"

# define ITEM_SIZE 70

@implementation CircleLayout

// 准备计算需要的参数
-(void)prepareLayout
{
[super prepareLayout];

CGSize size = self.collectionView.frame.size;
_cellCount = [[self collectionView] numberOfItemsInSection:0];
// 中心点
_center = CGPointMake(size.width / 2.0, size.height / 2.0);
// 半径
_radius = MIN(size.width, size.height) / 2.5;
}
// collctionView的内容大小就是collectionView的大小
-(CGSize)collectionViewContentSize
{
return [self collectionView].frame.size;
}
// 通过所在的indexPath确定位置
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
attributes.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
// 配置attributes到圆周上
attributes.center = CGPointMake(_center.x + _radius * cosf(2 * path.item * M_PI / _cellCount),
_center.y + _radius * sinf(2 * path.item * M_PI / _cellCount));
return attributes;
}
// 用来在一开始给出一套UICollectionViewLayoutAttributes
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray* attributes = [NSMutableArray array];
for (NSInteger i=0 ; i < self.cellCount; i++) {
//
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
[attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
return attributes;
}
// 插入前,cell在圆心位置,全透明
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForInsertedItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
attributes.center = CGPointMake(_center.x, _center.y);
return attributes;
}

// 删除时,cell在圆心位置,全透明,且只有原来的1/10大
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDeletedItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
attributes.center = CGPointMake(_center.x, _center.y);
attributes.transform3D = CATransform3DMakeScale(0.1, 0.1, 1.0);
return attributes;
}

@end
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
# import "ViewController.h"
# import "Cell.h"

@implementation ViewController

-(void)viewDidLoad
{
self.cellCount = 20;
// 添加一个手势识别
UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
[self.collectionView addGestureRecognizer:tapRecognizer];
[self.collectionView registerClass:[Cell class] forCellWithReuseIdentifier:@"MY_CELL"];
[self.collectionView reloadData];
self.collectionView.backgroundColor = [UIColor greenColor];
}

- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section;
{
return self.cellCount;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath;
{
Cell *cell = [cv dequeueReusableCellWithReuseIdentifier:@"MY_CELL" forIndexPath:indexPath];
return cell;
}

- (void)handleTapGesture:(UITapGestureRecognizer *)sender {

if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint initialPinchPoint = [sender locationInView:self.collectionView];
NSIndexPath* tappedCellPath = [self.collectionView indexPathForItemAtPoint:initialPinchPoint];
// 点击处没有cell
if (tappedCellPath!=nil)
{
self.cellCount = self.cellCount - 1;
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:tappedCellPath]];

} completion:nil];
}
else
{
self.cellCount = self.cellCount + 1;
[self.collectionView performBatchUpdates:^{
[self.collectionView insertItemsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]];
} completion:nil];
}
}
}

@end

例子

文章目录
  1. 1. 什么是UICollectionView
  2. 2. 实现一个简单的UICollectionView
    1. 2.1. UICollectionViewDataSource
      1. 2.1.1. 用于重用
    2. 2.2. UICollectionViewDelegate
    3. 2.3. 关于cell
    4. 2.4. UICollectionViewLayout
  3. 3. 自定义UICollectionViewLayout
  4. 4. UICollectionViewLayoutAttributes
  5. 5. UICollectionViewLayout
    1. 5.1. LineLayout——对于个别UICollectionViewLayoutAttributes的调整
    2. 5.2. CircleLayout——完全自定义的Layout,添加删除item,以及手势识别