UICollectionViewLayout
2013-12-08 20:43
435 查看
这是博主的WWDC2012笔记系列中的一篇,完整的笔记列表可以参看这里。如果您是首次来到本站,也许您会有兴趣通过RSS,或者通过页面左侧的邮件订阅的方式订阅本站。
在上一篇UICollectionView的入门介绍中,大概地对iOS6新加入的强大的UICollectionView进行了一些说明。在这篇博文中,将结合WWDC2012
Session219:Advanced Collection View的内容,对Collection View进行一个深入的使用探讨,并给出一个自定义的Demo。
UICollectionView的结构回顾
首先回顾一下Collection View的构成,我们能看到的有三个部分:Cells
Supplementary Views 追加视图 (类似Header或者Footer)
Decoration Views 装饰视图 (用作背景展示)
而在表面下,由两个方面对UICollectionView进行支持。其中之一和tableView一样,即提供数据的UICollectionViewDataSource以及处理用户交互的UICollectionViewDelegate。另一方面,对于cell的样式和组织方式,由于collectionView比tableView要复杂得多,因此没有按照类似于tableView的style的方式来定义,而是专门使用了一个类来对collectionView的布局和行为进行描述,这就是UICollectionViewLayout。
这次的笔记将把重点放在UICollectionViewLayout上,因为这不仅是collectionView和tableView的最重要求的区别,也是整个UICollectionView的精髓所在。
如果对UICollectionView的基本构成要素和使用方法还不清楚的话,可以移步到我之前的一篇笔记:Session笔记——205 Introducing
Collection Views中进行一些了解。
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的实例的方式给出。
自定义的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
说了那么多,其实还是Demo最能解决问题。Apple官方给了一个flow layout和一个circle layout的例子,都很经典,需要的同学可以从这里下载。
LineLayout——对于个别UICollectionViewLayoutAttributes的调整
先看LineLayout,它继承了UICollectionViewFlowLayout这个Apple提供的基本的布局。它主要实现了单行布局,自动对齐到网格以及当前网格cell放大三个特性。如图:先看LineLayout的init方法:
1 2 3 4 5 6 7 8 9 10 11 | -(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.minimumLineSpacing = 50.0; 这个定义了每个item在水平方向上的最小间距。
UICollectionViewFlowLayout是Apple为我们准备的开袋即食的现成布局,因此之前提到的几个必须重载的方法中需要我们操心的很少,即使完全不重载它们,现在也可以得到一个不错的线状一行的gridview了。而我们的LineLayout通过重载父类方法后,可以实现一些新特性,比如这里的动对齐到网格以及当前网格cell放大。
自动对齐到网格
1 2 3 4 5 6 7 8 9 10 1112 | - (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); 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); } |
1 2 3 4 5 6 7 8 9 10 1112 | -(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; } |
CircleLayout——完全自定义的Layout,添加删除item,以及手势识别
CircleLayout的例子稍微复杂一些,cell分布在圆周上,点击cell的话会将其从collectionView中移出,点击空白处会加入一个cell,加入和移出都有动画效果。这放在以前的话估计够写一阵子了,而得益于UICollectionView,基本只需要100来行代码就可以搞定这一切,非常cheap。通过CircleLayout的实现,可以完整地看到自定义的layout的编写流程,非常具有学习和借鉴的意义。
CircleLayout
首先,布局准备中定义了一些之后计算所需要用到的参数。
1 2 3 4 5 6 7 8 9 | -(void)prepareLayout { //和init相似,必须call super的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; } |
然后,按照UICollectionViewLayout子类的要求,重载了所需要的方法:
1 2 3 4 5 6 7 8 9 10 1112 | //整个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; } |
1 2 | UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; [self.collectionView addGestureRecognizer:tapRecognizer]; |
1 2 3 4 5 6 7 8 9 10 1112 | - (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]; } } } |
initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
更正:正式版中API发生了变化(而且不止一次变化 initialLayoutAttributesForInsertedItemAtIndexPath:在正式版中已经被废除。现在在insert或者delete之前,prepareForCollectionViewUpdates:会被调用,可以使用这个方法来完成添加/删除的布局。关于更多这方面的内容以及新的示例demo,可以参看这篇博文(需要翻墙)。新的示例demo在Github上也有,链接
在CircleLayout中,实现了cell的动画。
1 2 3 4 5 6 7 8 9 10 1112 | //插入前,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:,则可以在切换布局的同时,使用动画来过渡。对于每一个cell,都将有对应的UIView动画进行对应,又是一个接近free的特性。对于我自己来说,UICollectionView可能是我转向iOS 6 SDK的最具有吸引力的特性之一,因为UIKit团队的努力和CoreAnimation的成熟,使得创建一个漂亮优雅的UI变的越来越简单了。可以断言说UICollectionView在今后的iOS开发中,一定会成为和UITableView一样的强大和最常用的类之一。在iOS 6还未正式上市前,先对其特性进行一些学习,以期尽快能使用新特性来简化开发流程,可以说是非常值得的。
相关文章推荐
- UICollectionView自定义Layout教程
- UICollectionViewLayout
- Swift 关联Storyboard中的controller解决UICollectionView must be initialized with a non-nil layout parameter
- 自定义UICollectionViewLayout(一) ----LineLayout
- UICollectionViewLayout
- 自定义UICollectionViewLayout 实现瀑布流
- UICollectionViewLayout
- iOS6新特征:UICollectionView高级使用示例之CircleLayout
- UICollectionView详解三:UICollectionViewLayout
- 第三方瀑布流layout:UICollectionViewWaterfallLayout
- UICollectionViewLayout
- UICollectionView must be initialized with a non-nil layout parameter
- 使用UICollectionView 重写UICollectionViewLayout实现瀑布流
- 【无限互联】iOS开发之瀑布流布局实现(UICollectionView拓展Layout)
- UICollectionViewLayout
- IOS瀑布流 通过自定义UICollectionViewController的Layout布局实现
- 通过layout实现可拖拽自动排序的UICollectionView
- 自定义UICollectionViewLayout实现瀑布流
- 自定义UICollectionViewLayout并添加UIDynamic - scorpiozj(转)
- 玩转UICollectionViewLayout