UITableView中我们使用datasource和delegate分别处理我们的数据和交互,而且UITableView默认提供了两种样式供我们选择如何呈现数据,在IOS6中苹果提供了UICollectionView用来更自由地定制呈现我们的数据。

UICollectionView使用包括三个部分:

1.设置数据(使用UICollectionViewDataSource)

2.设置数据呈现方式(使用UICollectionViewLayout)

3.设置界面交互(使用UICollectionViewDelegate)

其中1,3和UITableView一致,可见UICollectionView比UITableView更具有一般性(我们可以使用UICollectionView实现UITableView的效果)

本篇博客的outline如下(本文参考http://www.onevcat.com/2012/06/introducing-collection-views/,代码下载地址为https://github.com/zanglitao/UICollectionViewDemo

1:基本介绍

2:UICollectionViewDataSource和UICollectionViewDelegate介绍

3:使用UICollectionViewFlowLayout

4:UICollectionViewFlowLayout的扩展

5:使用自定义UICollectionViewLayout

6:添加和删除数据

7:布局切换

 

基本介绍

UICollectionView是一种新的数据展示方式,简单来说可以把他理解成多列的UITableView(请一定注意这是UICollectionView的最最简单的形式)。如果你用过iBooks的话,可能你还对书架布局有一定印象:一个虚拟书架上放着你下载和购买的各类图书,整齐排列。其实这就是一个UICollectionView的表现形式,或者iPad的iOS6中的原生时钟应用中的各个时钟,也是UICollectionView的最简单的一个布局,如图:

iOS6 iPad版时钟应用 最简单的UICollectionView就是一个GridView,可以以多列的方式将数据进行展示。标准的UICollectionView包含三个部分,它们都是UIView的子类:

  • Cells 用于展示内容的主体,对于不同的cell可以指定不同尺寸和不同的内容,这个稍后再说
  • Supplementary Views 追加视图 如果你对UITableView比较熟悉的话,可以理解为每个Section的Header或者Footer,用来标记每个section的view
  • Decoration Views 装饰视图 这是每个section的背景,比如iBooks中的书架就是这个

 

不管一个UICollectionView的布局如何变化,这三个部件都是存在的。再次说明,复杂的UICollectionView绝不止上面的几幅图。

 

UICollectionViewDataSource和UICollectionViewDelegate介绍

UICollectionViewDataSource用来设置数据,此协议包含的方法如下

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section; //设置每个section包含的item数目

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath; //返回对应indexPath的cell

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView; //返回section的数目,此方法可选,默认返回1

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; //返回Supplementary Views,此方法可选

 

对于Decoration Views,提供方法并不在UICollectionViewDataSource中,而是直接UICollectionViewLayout类中的(因为它仅仅是视图相关,而与数据无关),放到稍后再说。

与UITableViewCell相似的是UICollectionViewCell也支持重用,典型的UITbleViewCell重用写法如下

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MY_CELL_ID"];  
if (!cell) {    //如果没有可重用的cell,那么生成一个  
    cell = [[UITableViewCell alloc] init]; 
} 
//配置cell,blablabla 
return cell 

 

UICollectionViewCell重用写法于UITableViewCell一致,但是现在更简便的是如果我们直接在storyboard中对cell设置了identifier,或者使用了以下方法进行注册

  • -registerClass:forCellWithReuseIdentifier:
  • -registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
  • -registerNib:forCellWithReuseIdentifier:
  • -registerNib:forSupplementaryViewOfKind:withReuseIdentifier:

那么可以更简单地实现重用

- (UICollectionView*)collectionView:(UICollectionView*)cv cellForItemAtIndexPath:(NSIndexPath*)indexPath { 
    MyCell *cell = [cv dequeueReusableCellWithReuseIdentifier:@”MY_CELL_ID”]; 
    // Configure the cell's content 
    cell.imageView.image = ... 
    return cell; 
}

上面的4个语句分别提供了nib和class方法对collectionViewCell和supplementaryView进行注册

 

UICollectionViewDelegate处理交互,包括cell点击事件,cell点击后高亮效果以及长按菜单等设置,当用户点击cell后,会依次执行协议中以下方法

  1. -collectionView:shouldHighlightItemAtIndexPath: 是否应该高亮?
  2. -collectionView:didHighlightItemAtIndexPath: 如果1回答为是,那么高亮
  3. -collectionView:shouldSelectItemAtIndexPath: 无论1结果如何,都询问是否可以被选中?
  4. -collectionView:didUnhighlightItemAtIndexPath: 如果1回答为是,那么现在取消高亮
  5. -collectionView:didSelectItemAtIndexPath: 如果3回答为是,那么选中cell

状态控制要比以前灵活一些,对应的高亮和选中状态分别由highlighted和selected两个属性表示。

关于Cell

相对于UITableViewCell来说,UICollectionViewCell没有这么多花头。首先UICollectionViewCell不存在各式各样的默认的style,这主要是由于展示对象的性质决定的,因为UICollectionView所用来展示的对象相比UITableView来说要来得灵活,大部分情况下更偏向于图像而非文字,因此需求将会千奇百怪。因此SDK提供给我们的默认的UICollectionViewCell结构上相对比较简单,由下至上:

  • 首先是cell本身作为容器view
  • 然后是一个大小自动适应整个cell的backgroundView,用作cell平时的背景
  • 再其上是selectedBackgroundView,是cell被选中时的背景
  • 最后是一个contentView,自定义内容应被加在这个view上

这次Apple给我们带来的好康是被选中cell的自动变化,所有的cell中的子view,也包括contentView中的子view,在当cell被选中时,会自动去查找view是否有被选中状态下的改变。比如在contentView里加了一个normal和selected指定了不同图片的imageView,那么选中这个cell的同时这张图片也会从normal变成selected,而不需要额外的任何代码。

 

使用UICollectionViewFlowLayout

UICollectionViewLayout用来处理数据的布局,通过它我们可以设置每个cell,Supplementary View以及Decoration Views的呈现方式,比如位置,大小,透明度,形状等等属性

Layout决定了UICollectionView是如何显示在界面上的。在展示之前,一般需要生成合适的UICollectionViewLayout子类对象,并将其赋予CollectionView的collectionViewLayout属性,苹果还提供了一个现成的UICollectionViewFlowLayout,通过这个layout我们可以很简单地实现流布局,UICollectionViewFlowLayout常用的配置属性如下

  • CGSize itemSize:它定义了每一个item的大小。通过设定itemSize可以全局地改变所有cell的尺寸,如果想要对某个cell制定尺寸,可以使用-collectionView:layout:sizeForItemAtIndexPath:方法。
  • CGFloat minimumLineSpacing:每一行的间距
  • CGFloat minimumInteritemSpacing:item与item的间距
  • UIEdgeInsets sectionInset:每个section的缩进
  • UICollectionViewScrollDirection scrollDirection:设定是垂直流布局还是横向流布局,默认是UICollectionViewScrollDirectionVertical
  • CGSize headerReferenceSize:设定header尺寸
  • CGSize footerReferenceSize:设定footer尺寸

上面都是全局属性的设置,我们可以通过delegate中的方法对进行定制,通过实现以下这些方法设定的属性的优先级比全局设定的要高

@protocol UICollectionViewDelegateFlowLayout <UICollectionViewDelegate>
@optional

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section;

@end

 

接下来我们使用使用UICollectionViewFlowLayout完成一个简单demo

1:设置我们的cell

//SimpleFlowLayoutCell.h
@interface SimpleFlowLayoutCell : UICollectionViewCell
@property(nonatomic,strong)UILabel *label;
@end

//SimpleFlowLayoutCell.m
@implementation SimpleFlowLayoutCell

-(id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    
    if (self) {
        self.label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, frame.size.width, frame.size.height)];
        self.label.textAlignment = NSTextAlignmentCenter;
        self.label.textColor = [UIColor blackColor];
        self.label.font = [UIFont boldSystemFontOfSize:15.0];
        self.backgroundColor = [UIColor lightGrayColor];
        
        [self.contentView addSubview:self.label];
        
        self.contentView.layer.borderWidth = 1.0f;
        self.contentView.layer.borderColor = [UIColor blackColor].CGColor;
    }
    
    return self;
}

@end

2:设置追加视图

//SimpleFlowLayoutSupplementaryView.h
@interface SimpleFlowLayoutSupplementaryView : UICollectionReusableView
@property(nonatomic,strong)UILabel *label;
@end

//SimpleFlowLayoutSupplementaryView.m
@implementation SimpleFlowLayoutSupplementaryView

-(id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    
    if (self) {
        self.label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, frame.size.width, frame.size.height)];
        self.label.textAlignment = NSTextAlignmentCenter;
        self.label.textColor = [UIColor blackColor];
        self.label.font = [UIFont boldSystemFontOfSize:15.0];
        self.backgroundColor = [UIColor lightGrayColor];
        
        [self addSubview:self.label];
        
        self.layer.borderWidth = 1.0f;
        self.layer.borderColor = [UIColor blackColor].CGColor;
    }
    
    return self;
}

@end

 

3:使用流布局初始化我们的UICollectionView

- (void)viewDidLoad {
    [super viewDidLoad];

    self.collectionView = [[UICollectionView alloc] initWithFrame:[UIScreen mainScreen].bounds collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
    self.collectionView.backgroundColor = [UIColor whiteColor];
    self.collectionView.delegate = self;
    self.collectionView.dataSource = self;
    
    [self.collectionView registerClass:[SimpleFlowLayoutCell class] forCellWithReuseIdentifier:@"MY_CELL"];
    //追加视图的类型是UICollectionElementKindSectionHeader,也可以设置为UICollectionElementKindSectionFooter
    [self.collectionView registerClass:[SimpleFlowLayoutSupplementaryView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"MY_SUPPLEMENT"];
    
    [self.view addSubview:self.collectionView];
}

 

4:配置datasource

//每个section中有32个item
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return  32;
}


- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    SimpleFlowLayoutCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellidentifier forIndexPath:indexPath];
    cell.label.text = [NSString stringWithFormat:@"%d",indexPath.item];
    return cell;
}

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return 2;
}

// The view that is returned must be retrieved from a call to -dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
    SimpleFlowLayoutSupplementaryView *view = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"MY_SUPPLEMENT" forIndexPath:indexPath];
    view.label.text = [NSString stringWithFormat:@"section header %d",indexPath.section];
    return view;
}

此时运行程序可以看到如下界面

 程序并没有显示我们设置的header视图,这是因为我们使用的是UICollectionViewFlowLayout默认配置,当前header视图高度为0,我们可以通过设置UICollectionViewFlowLayout的

headerReferenceSize属性改变大小,也可以通过协议方法返回特定section的header大小,这里我们先使用后者

我们添加以下方法

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
    return CGSizeMake(44, 44);
}

此时再运行就能得到以下结果

 

5:配置layout

上面的代码使用了flowlayout默认的配置,包括itemsize,行间距,item间距,追加视图大小等等都是默认值,我们可以改变这些值

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    self.collectionView = [[UICollectionView alloc] initWithFrame:[UIScreen mainScreen].bounds collectionViewLayout:layout];
    self.collectionView.backgroundColor = [UIColor whiteColor];
    self.collectionView.delegate = self;
    self.collectionView.dataSource = self;
    
    [self.collectionView registerClass:[SimpleFlowLayoutCell class] forCellWithReuseIdentifier:@"MY_CELL"];
    //追加视图的类型是UICollectionElementKindSectionHeader,也可以设置为UICollectionElementKindSectionFooter
    [self.collectionView registerClass:[SimpleFlowLayoutSupplementaryView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"MY_SUPPLEMENT"];
    
    [self.view addSubview:self.collectionView];
    
    //配置UICollectionViewFlowLayout属性
    //每个itemsize的大小
    layout.itemSize = CGSizeMake(80, 50);
    //行与行的最小间距
    layout.minimumLineSpacing = 44;
    
    //每行的item与item之间最小间隔(如果)
    layout.minimumInteritemSpacing = 20;
    //每个section的头部大小
    layout.headerReferenceSize = CGSizeMake(44, 44);
    //每个section距离上方和下方20,左方和右方10
    layout.sectionInset = UIEdgeInsetsMake(20, 10, 20, 10);
    //垂直滚动(水平滚动设置UICollectionViewScrollDirectionHorizontal)
    layout.scrollDirection = UICollectionViewScrollDirectionVertical;
}

运行结果如下

 

6:修改特定cell大小

包括上面配置header高度时使用的方法- collectionView:layout:referenceSizeForHeaderInSection:

UICollectionViewDelegateFlowLayout还提供了方法对特定cell大小,间距进行设置

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 0) {
        return CGSizeMake(80, 40);
    } else {
        return CGSizeMake(40, 40);
    }
}

7:设置delegate,通过delegate中的方法可以设置cell的点击事件,这部分和UITableView差不多

 

UICollectionViewFlowLayout的扩展

上一部分我们直接使用了UICollectionViewFlowLayout,我们也可以继承此布局实现更多的效果,苹果官方给出了一个flowlayout的demo,实现滚动时item放大以及网格对齐的功能

 

1:新建我们的cell类

//LineLayoutCell.h
@interface LineLayoutCell : UICollectionViewCell
@property (strong, nonatomic) UILabel* label;
@end

//LineLayoutCell.m
@implementation LineLayoutCell

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, frame.size.width, frame.size.height)];
        self.label.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
        self.label.textAlignment = NSTextAlignmentCenter;
        self.label.font = [UIFont boldSystemFontOfSize:50.0];
        self.label.backgroundColor = [UIColor underPageBackgroundColor];
        self.label.textColor = [UIColor blackColor];
        [self.contentView addSubview:self.label];;
        self.contentView.layer.borderWidth = 1.0f;
        self.contentView.layer.borderColor = [UIColor whiteColor].CGColor;
    }
    return self;
}

@end

 

2:storyboard中新建UICollectionViewController,设置类为我们自定义的LineCollectionViewController,并设置Layout为我们自定义的LineLayout

 

3:在我们自定义的LineCollectionViewController中配置数据源

//LineCollectionViewController.h
@interface LineCollectionViewController : UICollectionViewController
@end

//LineCollectionViewController.m
@implementation LineCollectionViewController

-(void)viewDidLoad
{
    [self.collectionView registerClass:[LineLayoutCell class] forCellWithReuseIdentifier:@"MY_CELL"];
}

- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section;
{
    return 60;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath;
{
    LineLayoutCell *cell = [cv dequeueReusableCellWithReuseIdentifier:@"MY_CELL" forIndexPath:indexPath];
    cell.label.text = [NSString stringWithFormat:@"%d",indexPath.item];
    return cell;
}
@end

 

4:设置LineLayout 

我们设置数据横向滚动,item大小为CGSizeMake(200, 200),并设置每列数据上下各间隔200,这样一行只有一列数据

//由于使用了storyboard的关系,需要使用initWithCoder
-(id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        self.itemSize = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
        self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
        self.sectionInset = UIEdgeInsetsMake(200, 0.0, 200, 0.0);
        self.minimumLineSpacing = 50.0;
    }
    return self;
}

 

然后设置item滚动居中,只需要实现方法-targetContentOffsetForProposedContentOffset:withScrollingVelocity,此方法第一个参数为不加偏移量预期滚动停止时的ContentOffset,返回值类型为CGPoint,代表x,y的偏移

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    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);
    
    //获取该区域的UICollectionViewLayoutAttributes集合
    NSArray* array = [super layoutAttributesForElementsInRect:targetRect];
    
    
    for (UICollectionViewLayoutAttributes* layoutAttributes in array) {
        CGFloat itemHorizontalCenter = layoutAttributes.center.x;
        //循环结束后offsetAdjustment的值就是预期滚定停止后离水平方向中心点最近的item的中心店
        if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment)) {
            offsetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
    }
    
    //返回偏移量
    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

 上面的代码中出现了一个新的类 UICollectionViewLayoutAttributes

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的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。和DataSource的行为十分类似,当UICollectionView在获取布局时将针对每一个indexPath的部件(包括cell,追加视图和装饰视图),向其上的UICollectionViewLayout实例询问该部件的布局信息(在这个层面上说的话,实现一个UICollectionViewLayout的时候,其实很像是zap一个delegate,之后的例子中会很明显地看出),这个布局信息,就以UICollectionViewLayoutAttributes的实例的方式给出。

 

接下来设置item滚动过程中放大缩小效果

#define ACTIVE_DISTANCE 200
#define ZOOM_FACTOR 0.3
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    //获取rect区域的UICollectionViewLayoutAttributes集合
    NSArray* array = [super layoutAttributesForElementsInRect:rect];
    CGRect visibleRect;
    visibleRect.origin = self.collectionView.contentOffset;
    visibleRect.size = self.collectionView.bounds.size;
    
    for (UICollectionViewLayoutAttributes* attributes in array) {
        //只处理可视区域内的item
        if (CGRectIntersectsRect(attributes.frame, rect)) {
            //可视区域中心点与item中心点距离
            CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
            
            CGFloat normalizedDistance = distance / ACTIVE_DISTANCE;
            if (ABS(distance) < ACTIVE_DISTANCE) {
                //放大系数
                //当可视区域中心点和item中心点距离为0时达到最大放大倍数1.3
                //当可视区域中心点和item中心点距离大于200时达到最小放大倍数1,也就是不放大
                //距离在0~200之间时放大倍数在1.3~1
                CGFloat zoom = 1 + ZOOM_FACTOR*(1 - ABS(normalizedDistance));
                attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
                attributes.zIndex = 1;
            }
        }
    }
    return array;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)oldBounds
{
    return YES;
} 

对于个别UICollectionViewLayoutAttributes进行调整,以达到满足设计需求是UICollectionView使用中的一种思路。在根据位置提供不同layout属性的时候,需要记得让-shouldInvalidateLayoutForBoundsChange:返回YES,这样当边界改变的时候,-invalidateLayout会自动被发送,才能让layout得到刷新。

 

5:运行程序查看结果

 

使用自定义UICollectionViewLayout

 如果我们想实现更加复杂的布局,那就必须自定义我们自己的UICollectionView,实现一个自定义layout的常规做法是继承UICollectionViewLayout类,然后重载下列方法

  • -(CGSize)collectionViewContentSize:返回collectionView内容的尺寸,
  • -(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect:返回rect范围内所有元素的属性数组,属性是UICollectionViewLayoutAttributes,通过这个属性数组就能决定每个元素的布局样式

UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过以下三种不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的UICollectionViewLayoutAttributes    

  1. layoutAttributesForCellWithIndexPath:
  2. layoutAttributesForSupplementaryViewOfKind:withIndexPath:
  3. layoutAttributesForDecorationViewOfKind:withIndexPath:

 

  • - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path:返回对应于indexPath的元素的属性
  • -(UICollectionViewLayoutAttributes _)layoutAttributesForItemAtIndexPath:(NSIndexPath _)indexPath:返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
  • -(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString_)decorationViewKind atIndexPath:(NSIndexPath _)indexPath:返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
  • -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds:当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息

 

另外需要了解的是,在初始化一个UICollectionViewLayout实例后,会有一系列准备方法被自动调用,以保证layout实例的正确。

首先,-(void)prepareLayout将被调用,默认下该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。

之后,-(CGSize) collectionViewContentSize将被调用,以确定collection应该占据的尺寸。注意这里的尺寸不是指可视部分的尺寸,而应该是所有内容所占的尺寸。collectionView的本质是一个scrollView,因此需要这个尺寸来配置滚动行为。

接下来-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect被调用,这个没什么值得多说的。初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。

另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIView的setNeedsLayout方法十分类似。在

-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect来生成更新后的布局。

 

苹果官方给出了一个circlelayout的demo

 1:新建我们的cell类

//CircleLayoutCell.h
@interface CircleLayoutCell : UICollectionViewCell
@end

//CircleLayoutCell.m
@implementation CircleLayoutCell
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.contentView.layer.cornerRadius = 35.0;
        self.contentView.layer.borderWidth = 1.0f;
        self.contentView.layer.borderColor = [UIColor whiteColor].CGColor;
        self.contentView.backgroundColor = [UIColor underPageBackgroundColor];
    }
    return self;
}
@end

 

2:storyboard中新建UICollectionViewController,设置类为我们自定义的CircleCollectionViewController,并设置Layout为我们自定义的CircleLayout

 

3:在我们自定义的CircleCollectionViewController中配置数据源

//CircleCollectionViewController.h
@interface CircleCollectionViewController : UICollectionViewController
@end

//CircleCollectionViewController.m
@interface CircleCollectionViewController ()
@property (nonatomic, assign) NSInteger cellCount;
@end

@implementation CircleCollectionViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.cellCount = 20;
    [self.collectionView registerClass:[CircleLayoutCell class] forCellWithReuseIdentifier:@"MY_CELL"];
    self.collectionView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
}

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

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

@end

  

4:设置CircleLayout 

首先在prepareLayout中设置界面圆心的位置以及半径

-(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;
}

其实对于一个size不变的collectionView来说,除了_cellCount之外的中心和半径的定义也可以扔到init里去做,但是显然在prepareLayout里做的话具有更大的灵活性。因为每次重新给出layout时都会调用prepareLayout,这样在以后如果有collectionView大小变化的需求时也可以自动适应变化

 

之后设置内容collectionView内容的尺寸,这个demo中内容尺寸就是屏幕可视区域

-(CGSize)collectionViewContentSize
{
    return [self collectionView].frame.size;
}

 

接下来在-layoutAttributesForElementsInRect中返回各个元素属性组成的属性数组

-(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;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
    //初始化一个UICollectionViewLayoutAttributes
    UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
    
    //元素的大小
    attributes.size = CGSizeMake(70, 70);
    
    //元素的中心点
    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;
}

 

5:运行程序查看结果

 

添加和删除数据

我们经常需要在collectionview中动态地添加一个元素或者删除一个元素,collectionview提供了下面的函数处理数据的删除与添加

  • -deleteItemsAtIndexPaths:删除对应indexPath处的元素
  • -insertItemsAtIndexPaths:在indexPath位置处添加一个元素
  • -performBatchUpdates:completion:这个方法可以用来对collectionView中的元素进行批量的插入,删除,移动等操作

继续上面的CircleLayout的demo,我们为collectionView添加点击事件,如果点击某个元素则删除此元素,如果点击元素外的区域则在第一个位置新加一个元素

//CircleCollectionViewController.m
@implementation CircleCollectionViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.cellCount = 20;
    [self.collectionView registerClass:[CircleLayoutCell class] forCellWithReuseIdentifier:@"MY_CELL"];
    self.collectionView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
    
    
    UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
    [self.collectionView addGestureRecognizer:tapRecognizer];
}

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

- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath;
{
    CircleLayoutCell *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];
        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

有时候我们希望给删除和添加元素加点动画,layout中提供了下列方法处理动画

  • initialLayoutAttributesForAppearingItemAtIndexPath:
  • initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
  • finalLayoutAttributesForDisappearingItemAtIndexPath:
  • finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:

需要注意的是以上4个方法会对所有显示的元素调用,所以我们需要两个数组放置刚添加或者删除的元素,只对它们进行动画处理,在insert或者delete之前prepareForCollectionViewUpdates:会被调用,insert或者delete之后finalizeCollectionViewUpdates:会被调用,我们可以在这两个方法中设置和销毁我们的数组

CircleLayout的完整代码如下

//CircleLayout.m
#define ITEM_SIZE 70

@interface CircleLayout()

// arrays to keep track of insert, delete index paths
@property (nonatomic, strong) NSMutableArray *deleteIndexPaths;
@property (nonatomic, strong) NSMutableArray *insertIndexPaths;

@end

@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;
}

-(CGSize)collectionViewContentSize
{
    return [self collectionView].frame.size;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
    //初始化一个UICollectionViewLayoutAttributes
    UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
    
    //元素的大小
    attributes.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
    
    //元素的中心点
    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;
}

-(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;
}

- (void)prepareForCollectionViewUpdates:(NSArray *)updateItems
{
    // Keep track of insert and delete index paths
    [super prepareForCollectionViewUpdates:updateItems];
    
    self.deleteIndexPaths = [NSMutableArray array];
    self.insertIndexPaths = [NSMutableArray array];
    
    for (UICollectionViewUpdateItem *update in updateItems)
    {
        if (update.updateAction == UICollectionUpdateActionDelete)
        {
            [self.deleteIndexPaths addObject:update.indexPathBeforeUpdate];
        }
        else if (update.updateAction == UICollectionUpdateActionInsert)
        {
            [self.insertIndexPaths addObject:update.indexPathAfterUpdate];
        }
    }
}

- (void)finalizeCollectionViewUpdates
{
    [super finalizeCollectionViewUpdates];
    // release the insert and delete index paths
    self.deleteIndexPaths = nil;
    self.insertIndexPaths = nil;
}

// Note: name of method changed
// Also this gets called for all visible cells (not just the inserted ones) and
// even gets called when deleting cells!
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    // Must call super
    UICollectionViewLayoutAttributes *attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
    
    if ([self.insertIndexPaths containsObject:itemIndexPath])
    {
        // only change attributes on inserted cells
        if (!attributes)
            attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
        
        // Configure attributes ...
        attributes.alpha = 0.0;
        attributes.center = CGPointMake(_center.x, _center.y);
    }
    
    return attributes;
}

// Note: name of method changed
// Also this gets called for all visible cells (not just the deleted ones) and
// even gets called when inserting cells!
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    // So far, calling super hasn't been strictly necessary here, but leaving it in
    // for good measure
    UICollectionViewLayoutAttributes *attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath];
    
    if ([self.deleteIndexPaths containsObject:itemIndexPath])
    {
        // only change attributes on deleted cells
        if (!attributes)
            attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
        
        // Configure attributes ...
        attributes.alpha = 0.0;
        attributes.center = CGPointMake(_center.x, _center.y);
        attributes.transform3D = CATransform3DMakeScale(0.1, 0.1, 1.0);
    }
    
    return attributes;
}

@end

  

布局切换

UICollectionView最大的好处是数据源,交互与布局的独立和解耦,我们可以方便地使用一套数据在几种布局中切换,直接更改collectionView的collectionViewLayout属性可以立即切换布局。而如果通过setCollectionViewLayout:animated:,则可以在切换布局的同时,使用动画来过渡。对于每一个cell,都将有对应的UIView动画进行对应

 

posted on 2014-12-29 14:17  幸福小弥  阅读(8674)  评论(0编辑  收藏  举报