UICollectionView详解

UICollectionView入门介绍

什么是UICollectionView

  • UICollectionView是一种新的数据展示方式,简单来说可以把他理解成多列的UITableView(请一定注意这是UICollectionView的最最简单的形式)。

  • 标准的UICollectionView包含三个部分,它们都是UIView的子类:

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

这里写图片描述

实现一个简单的UICollectionView

  • 实现一个UICollectionView和实现一个UITableView基本没有什么大区别,它们都同样是datasource和delegate设计模式的:datasource为view提供数据源,告诉view要显示些什么东西以及如何显示它们,delegate提供一些样式的小细节以及用户交互的相应。

  • UICollectionViewDataSource

    • section的数量 -numberOfSectionsInCollection:
    • 某个section里有多少个item -collectionView:numberOfItemsInSection:
    • 对于某个位置应该显示什么样的cell -collectionView:cellForItemAtIndexPath:

实现以上三个委托方法,基本上就可以保证CollectionView工作正常了。

  • 提供Supplementary View的方法
collectionView:viewForSupplementaryElementOfKind:atIndexPath:

关于重用

  • 在UICollectionView中,不仅cell可以重用,Supplementary View和Decoration View也是可以并且应当被重用的。
    • 在iOS5中,Apple对UITableView的重用做了简化,以往要写类似这样的代码:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MY_CELL_ID"];  
if (!cell) {    //如果没有可重用的cell,那么生成一个  
    cell = [[UITableViewCell alloc] init]; 
} 
//配置cell,blablabla 
return cell 
如果我们在TableView向数据源请求数据之前使用-registerNib:forCellReuseIdentifier:方法为@“MYCELLID"注册过nib的话,就可以省下每次判断并初始化cell的代码,要是在重用队列里没有可用的cell的话,runtime将自动帮我们生成并初始化一个可用的cell。
  • 在UICollectionView中Apple继承使用了这个特性,并且把其进行了一些扩展。使用以下方法进行注册:
 -registerClass:forCellWithReuseIdentifier:
 -registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
 -registerNib:forCellWithReuseIdentifier:
 -registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
  • 相比UITableView有两个主要变化:

    • 一是加入了对某个Class的注册,这样即使不用提供nib而是用代码生成的view也可以被接受为cell了;
    • 二是不仅只是cell,Supplementary View也可以用注册的方法绑定初始化了。
  • 在对collection view的重用ID注册后,就可以像UITableView那样简单的写cell配置了:

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

需要吐槽的是,对collection view,取重用队列的方法的名字和UITableView里面不一样了,在Identifier前面多加了Reuse五个字母,语义上要比以前清晰,命名规则也比以前严谨了

UICollectionViewDelegate

  • 由UICollectionViewDelegate负责:

    • cell的高亮
    • cell的选中状态
    • 可以支持长按后的菜单
  • 关于用户交互,UICollectionView也做了改进。

    • 每个cell现在有独立的高亮事件和选中事件的delegate,用户点击cell的时候,现在会按照以下流程向delegate进行询问:
1. -collectionView:shouldHighlightItemAtIndexPath: 是否应该高亮?
2. -collectionView:didHighlightItemAtIndexPath: 如果1回答为是,那么高亮
3. -collectionView:shouldSelectItemAtIndexPath: 无论1结果如何,都询问是否可以被选中?
4. -collectionView:didUnhighlightItemAtIndexPath: 如果1回答为是,那么现在取消高亮
5. -collectionView:didSelectItemAtIndexPath: 如果3回答为是,那么选中cell

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

关于Cell

  • UICollectionViewCell不存在各式各样的默认的style,这主要是由于展示对象的性质决定的

    • 因此SDK提供给我们的默认的UICollectionViewCell结构上相对比较简单,由下至上:
      • 1.首先是cell本身作为容器view
      • 2.然后是一个大小自动适应整个cell的backgroundView,用作cell平时的背景
      • 3.再其上是selectedBackgroundView,是cell被选中时的背景
      • 4.最后是一个contentView,自定义内容应被加在这个view上

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

UICollectionViewLayout

  • UICollectionViewLayout可以说是UICollectionView的大脑和中枢,它负责了将各个cell、Supplementary View和Decoration Views进行组织,为它们设定各自的属性,包括但不限于:
    • 位置
    • 尺寸
    • 透明度
    • 层级关系
    • 形状
    • 等等等等…
  • Layout决定了UICollectionView是如何显示在界面上的。在展示之前,一般需要生成合适的UICollectionViewLayout子类对象,并将其赋予CollectionView的collectionViewLayout属性。

Apple为我们提供了一个最简单可能也是最常用的默认layout对象,UICollectionViewFlowLayout。Flow Layout简单说是一个直线对齐的layout,最常见的Grid View形式即为一种Flow Layout配置。照片架界面就是一个典型的Flow 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确定scrollview的方向,将影响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:

总结:
一个UICollectionView的实现包括两个必要部分:
UICollectionViewDataSource和UICollectionViewLayout,和一个交互部分:UICollectionViewDelegate。

几个自定义的Layout

  • UICollectionView的强大之处,就在于各种layout的自定义实现,以及它们之间的切换。先看几个相当exiciting的例子吧~

    • 比如,堆叠布局
      这里写图片描述

    • 圆形布局:
      这里写图片描述

    • Cover Flow布局:
      这里写图片描述

  • 所有这些布局都采用了同样的数据源和委托方法,因此完全实现了model和view的解耦。


UICollectionView进阶

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

自定义的UICollectionViewLayout

  • UICollectionViewLayout的功能为向UICollectionView提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。
    • 实现一个自定义layout的常规做法是继承UICollectionViewLayout类,然后重载下列方法:
-(CGSize)collectionViewContentSize   // 返回collectionView的内容的尺寸
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

// 返回rect中的所有的元素的布局属性
// 返回的是包含UICollectionViewLayoutAttributes的NSArray
// UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的UICollectionViewLayoutAttributes:

 ① layoutAttributesForCellWithIndexPath:
 ② layoutAttributesForSupplementaryViewOfKind:withIndexPath:
 ③ layoutAttributesForDecorationViewOfKind:withIndexPath:
- (UICollectionViewLayoutAttributes_)layoutAttributesForItemAtIndexPath:(NSIndexPath _)indexPath
// 返回对应于indexPath的位置的cell的布局属性
- (UICollectionViewLayoutAttributes_)layoutAttributesForSupplementaryViewOfKind:(NSString _)kind atIndexPath:(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来生成更新后的布局。

案例 demo

LineLayout——对于个别UICollectionViewLayoutAttributes的调整

  • 先看LineLayout,它继承了UICollectionViewFlowLayout这个Apple提供的基本的布局。它主要实现了单行布局,自动对齐到网格以及当前网格cell放大三个特性。如图:

    这里写图片描述

  • 先看LineLayout的init方法:

-(id)init
{
    self = [super init];
    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;
}
self.sectionInset = UIEdgeInsetsMake(200, 0.0, 200, 0.0); 确定了缩进,此处为上方和下方各缩进200个point。由于cell的size已经定义了为200x200,因此屏幕上在缩进后就只有一排item的空间了。

self.minimumLineSpacing = 50.0; 这个定义了每个item在水平方向上的最小间距。

LineLayout通过重载父类方法后,可以实现一些新特性,比如这里的动对齐到网格以及当前网格cell放大。
  • 自动对齐到网格
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    //proposedContentOffset是没有对齐到网格时本来应该停下的位置 
    //计算出实际中心位置
    CGFloat offsetAdjustment = MAXFLOAT;
    CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);

    //取当前屏幕中的UICollectionViewLayoutAttributes
    CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    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;
        }
    }

    //返回调整好的point
    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
  • 当前item放大
//布局属性
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    //取父类的UICollectionViewLayoutAttributes
    NSArray* array = [super layoutAttributesForElementsInRect:rect];

    //可视rect
    CGRect visibleRect;
    visibleRect.origin = self.collectionView.contentOffset;
    visibleRect.size = self.collectionView.bounds.size;

    //设置item的缩放
    for (UICollectionViewLayoutAttributes* attributes in array) {
        if (CGRectIntersectsRect(attributes.frame, rect)) {
            CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;//item到中心点的距离
            CGFloat normalizedDistance = distance / ACTIVE_DISTANCE;//距离除以有效距离得到标准化距离
            //距离小于有效距离才生效
            if (ABS(distance) < ACTIVE_DISTANCE) {
                CGFloat zoom = 1 + ZOOM_FACTOR*(1 - ABS(normalizedDistance));//缩放率范围1~1.3,与标准距离负相关
                attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);//x,y轴方向变换
                attributes.zIndex = 1;
            }
        }
    }   
    return array;
}

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

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

  • CircleLayout的例子稍微复杂一些,cell分布在圆周上,点击cell的话会将其从collectionView中移出,点击空白处会加入一个cell,加入和移出都有动画效果。

    这里写图片描述

  • 首先,布局准备中定义了一些之后计算所需要用到的参数。

-(void)prepareLayout
{
    //和init相似,必须call super的prepareLayout以保证初始化正确
    [super prepareLayout];

    //其实对于一个size不变的collectionView来说,除了_cellCount之外的中心和半径的定义也可以扔到init里去做,但是显然在prepareLayout里做的话具有更大的灵活性。因为每次重新给出layout时都会调用prepareLayout,这样在以后如果有collectionView大小变化的需求时也可以自动适应变化
    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;
}
  • 然后,按照UICollectionViewLayout子类的要求,重载了所需要的方法:
//整个collectionView的内容大小就是collectionView的大小(没有滚动)
-(CGSize)collectionViewContentSize
{
    return [self collectionView].frame.size;
}

//通过所在的indexPath确定位置。
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
    UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path]; //生成空白的attributes对象,其中只记录了类型是cell以及对应的位置是indexPath
    //配置attributes到圆周上
    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;
}

//用来在一开始给出一套UICollectionViewLayoutAttributes
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray* attributes = [NSMutableArray array];
    for (NSInteger i=0 ; i &lt; self.cellCount; i++) {
        //这里利用了-layoutAttributesForItemAtIndexPath:来获取attributes
        NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        [attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
    }    
    return attributes;
}
  • 现在已经得到了一个circle layout。为了实现cell的添加和删除,需要为collectionView加上手势识别,这个很简单,在ViewController中:
UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];  
[self.collectionView addGestureRecognizer:tapRecognizer];
  • 对应的处理方法handleTapGesture:
- (void)handleTapGesture:(UITapGestureRecognizer *)sender {
    if (sender.state == UIGestureRecognizerStateEnded) {
        CGPoint initialPinchPoint = [sender locationInView:self.collectionView];
        NSIndexPath* tappedCellPath = [self.collectionView indexPathForItemAtPoint:initialPinchPoint]; //获取点击处的cell的indexPath
        if (tappedCellPath!=nil) { //点击处没有cell
            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];
        }
    }
}
  • performBatchUpdates:completion: 再次展示了block的强大的一面..这个方法可以用来对collectionView中的元素进行批量的插入,删除,移动等操作,同时将触发collectionView所对应的layout的对应的动画。相应的动画由layout中的下列四个方法来定义:
initialLayoutAttributesForAppearingItemAtIndexPath:

initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:

finalLayoutAttributesForDisappearingItemAtIndexPath:

finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
// 更正:正式版中API发生了变化 initialLayoutAttributesForInsertedItemAtIndexPath:在正式版中已经被废除。现在在insert或者delete之前,prepareForCollectionViewUpdates:会被调用,可以使用这个方法来完成添加/删除的布局。
  • 在CircleLayout中,实现了cell的动画。
//插入前,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;
}

布局之间的切换

  • 有时候可能需要不同的布局,Apple也提供了方便的布局间切换的方法。直接更改collectionView的collectionViewLayout属性可以立即切换布局。而如果通过setCollectionViewLayout:animated:,则可以在切换布局的同时,使用动画来过渡。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值