您的位置:首页 > 产品设计 > UI/UE

UICollectionView详解

2015-11-09 21:20 609 查看

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 < 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:,则可以在切换布局的同时,使用动画来过渡。

文章转自—-http://www.onevcat.com/#blog
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: