自定义UICollectionViewLayout

UICollectionView在iOS6中第一次被介绍,也是UIKit视图类中的一颗新星。它和UITableView共享API设计,但也在UITableView上作了一些扩展。UICollectionView最强大、同时显著超出UITableView的特点就是其彻底灵活的布局结构。在这篇文章中,咱们将会实现一个至关复杂的自定义collection view布局,而且顺便讨论一下这个类设计的重要部分。项目的示例代码在 GitHub上。
 
布局对象
UITableView和UICollectionView都是由 data-source和delegate驱动的。他们为其显示的子视图集扮演为愚蠢的容器(dumb containers),对他们真实的内容(contents)绝不知情。
 
UICollectionView进一步抽象了。它将其子视图的位置,大小和外观的控制权委托给一个单独的布局对象。经过提供一个自定义布局对象,你几乎能够实现任何你能想象到的布局。布局继承自UICollectionViewLayout这个抽象基类。iOS6中以UICollectionViewFlowLayout类的形式提出了一个具体的布局实现。
 
flow layout能够被用来实现一个标准的grid view,这多是在collection view中最多见的使用案例了。尽管大多数人都这么想,可是Apple很聪明,没有明确的命名这个类为UICollectionViewGridLayout。而使用了更为通用的术语flow layout,这更好的描述了该类的能力:它经过一个接一个的放置cell来创建本身的布局,当须要的时候,插入横排或竖排的分栏符。经过自定义滚动方向,大小和cell之间的间距,flow layout也能够在单行或单列中布局cell。实际上,UITableView的布局能够想象成flow layout的一种特殊状况。
 
在你准备本身写一个UICollectionViewLayout的子类以前,你须要问你本身,你是否可以使用UICollectionViewFlowLayout实现你内心的布局。这个类是很容易定制的,而且能够继承自己进行近一步的定制。感兴趣的看 这篇文章
 
Cells和其余Views
为了适应任意布局,collection view创建一个了相似,但比table view更灵活的视图层级(view hierarchy)。像往常同样,你的主要内容显示在cell中,cell能够被任意分组到section中。Collection view的cells必须是UICollectionViewCell的子类。除了cells,collection view额外管理着两种视图:supplementary views和decoration views。
 
collection view中的Supplementary views至关于table view的section header和footer views。像cells同样,他们的内容都由数据源对象驱动。然而,和table view中用法不同,supplementary view并不必定会做为header或footer view;他们的数量和放置的位置彻底由布局控制。
 
Decoration views纯粹为一个装饰品。他们彻底属于布局对象,并被布局对象管理,他们并不从数据源获取他们的contents。当布局对象指定它须要一个decoration view的时候,collection view会自动建立,并为其应用布局对象提供的布局参数。并不须要准备任何自定义视图的内容。
 
Supplementary views和decoration views必须是UICollectionResuableView的子类。每一个你布局所使用的视图类都须要在collection view中注册,这样当data source让他从reuse pool中出列时,它才可以建立新的实例。若是你是使用的Interface Builder,则能够经过在可视编辑器中拖拽一个cell到collection view上完成cell在collection view中的注册。一样的方法也能够用在supplementary view上,前提是你使用了UICollectionViewFlowLayout。若是没有,你只能经过调用registerClass:或者registerNib:方法手动注册视图类了。你须要在viewDidLoad中作这些操做。
 
 
自定义布局
做为一个很是有意义的自定义collection view布局的例子,咱们不妨设想一个典型的日历应用程序中的周(week)视图。日历一次显示一周,星期中的每一天显示在列中。每个日历事件将会在咱们的collection view中以一个cell显示,位置和大小表明事件起始日期时间和持续时间。
 
通常有两种类型的collection view布局:
 
1.独立于内容的布局计算。这正是你所知道的像UITableView和UICollectionViewFlowLayout这些状况。每一个cell的位置和外观不是基于其显示的内容,但全部cell的显示顺序是基于内容的顺序。能够把默认的flow layout作为例子。每一个cell都基于前一个cell放置(或者若是没有足够的空间,则从下一行开始)。布局对象没必要访问实际数据来计算布局。
 
2.基于内容的布局计算。咱们的日历视图正是这样类型的例子。为了计算显示事件的起始和结束时间,布局对象须要直接访问collection view的数据源。在不少状况下,布局对象不只须要取出当前可见cell的数据,还须要从全部记录中取出一些决定当前哪些cell可见的数据。
 
在咱们的日历示例中,布局对象若是访问某一个矩形内cells的属性,那就必须迭代数据源提供的全部事件来决定哪些位于要求的时间窗口中。 与一些相对简单,数据源独立计算的flow layout比起来,这足够计算出cell在一个矩形内的index paths了(假设网格中全部cells的大小都同样)。
 
若是有一个依赖内容的布局,那就是暗示你须要写自定义的布局类了,同时不能使用自定义的UICollectionViewFlowLayout。因此这正是咱们须要作的事情。
 
UICollectionViewLayout的文档列出了子类须要重写的方法。
 
collectionViewContentSize
因为collection view对它的content并不知情,因此布局首先要提供的信息就是滚动区域大小,这样collection view才能正确的管理滚动。布局对象必须在此时计算它内容的总大小,包括supplementary views和decoration views。注意,尽管大多数经典的collection view限制在一个轴方向上滚动(正如UICollectionViewFlowLayout同样),但这不是必须的。
 
在咱们的日历示例中,咱们想要视图垂直的滚动。好比,若是咱们想要在垂直空间上一个小时占去100点,这样显示一成天的内容高度就是2400点。注意,咱们不可以水平滚动,这就意味这咱们collection view只能显示一周。为了可以在日历中的多个星期间分页,咱们能够在一个独立(分页)的scroll view(可使用UIPageViewController)中使用多个collection view(一周一个),或者坚持使用一个collection view而且返回足够大的内容宽度,这会使得用户感受在两个方向上滑动自由。
  1. - (CGSize)collectionViewContentSize 
  2.   
  3.   
  4. // Don't scroll horizontally 
  5.   
  6. CGFloat contentWidth = self.collectionView.bounds.size.width; 
  7.   
  8. // Scroll vertically to display a full day 
  9.   
  10. CGFloat contentHeight = DayHeaderHeight + (HeightPerHour * HoursPerDay); 
  11.   
  12. CGSize contentSize = CGSizeMake(contentWidth, contentHeight); 
  13.   
  14. return contentSize; 
  15.   
 
为了清楚起见,我选择布局在一个很是简单模型上:假定每周天数相同,天天时长相同,
 
也就是说天数用0-6表示。在一个真实的日历程序中,布局将会为本身的计算大量使用基于NSCalendar的日期。
 
layoutAttributesForElementsInRect:
这是任何布局类中最重要的方法了,同时可能也是最容易让人迷惑的方法。collection view调用这个方法并传递一个自身坐标系统中的矩形过去。这个矩形表明了这个视图的可见矩形区域(也就是它的bounds),你须要准备好处理传给你的任何矩形。
 
你的实现必须返回一个包含UICollectionViewLayoutAttributes对象的数组,为每个cell包含这样的一个对象,supplementary view或decoration view在矩形区域内是可见的。UICollectionViewLayoutAttributes类包含了collection view内item的全部相关布局属性。默认状况下,这个类包含frame,center,size,transform3D,alpha,zIndex属性(properties),和hidden特性(attributes)。若是你的布局想要控制其余视图的属性(好比,背景颜色),你能够建一个UICollectionViewLayoutAttributes的子类,而后加上你本身的属性。
 
布局属性对象经过indexPath属性和他们对应的cell,supplementary view或者decoration view关联在一块儿。collection view为全部items从布局对象中请求到布局属性后,它将会实例化全部视图,并将对应的属性应用到每一个视图上去。
 
注意!这个方法涉及到全部类型的视图,也就是cell,supplementary views和decoration views。一个幼稚的实现可能会选择忽略传入的矩形,而且为collection view中的全部视图返回布局属性。在原型设计和开发布局阶段,这是一个有效的方法。可是,这将对性能产生很是坏的影响,特别是可见cell远少于全部cell数量的时候,collection view和布局对象将会为那些不可见的视图作额外没必要要的工做。
 
你的实现须要作这几步:
 
1.建立一个空的mutable数组来存放全部的布局属性。
 
2.肯定index paths中哪些cells的frame彻底或部分位于矩形中。这个计算须要你从collection view的数据源中取出你须要显示的数据。而后在循环中调用你实现的layoutAttributesForItemAtIndexPath:方法为每一个index path建立并配置一个合适的布局属性对象,并将每一个对象添加到数组中。
 
3.若是你的布局包含supplementary views,计算矩形内可见supplementary view的index paths。在循环中调用你实现的layoutAttributesForSupplementaryViewOfKind:atIndexPath:,而且将这些对象加到数组中。经过为kind参数传递你选择的不一样字符,你能够区分出不一样种类的supplementary views(好比headers和footers)。当须要建立视图时,collection view会将kind字符传回到你的数据源。记住supplementary和decoration views的数量和种类彻底由布局控制。你不会受到headers和footers的限制。
 
4.若是布局包含decoration views,计算矩形内可见decoration views的index paths。在循环中调用你实现的layoutAttributesForDecorationViewOfKind:atIndexPath:,而且将这些对象加到数组中。
 
5.返回数组。
 
咱们自定义的布局没有使用decoration views,可是使用了两种supplementary views(column headers和row headers)
  1. - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 
  2.   
  3.   
  4. NSMutableArray *layoutAttributes = [NSMutableArray array]; 
  5.   
  6. // Cells 
  7.   
  8. // We call a custom helper method -indexPathsOfItemsInRect: here 
  9.   
  10. // which computes the index paths of the cells that should be included 
  11.   
  12. // in rect. 
  13.   
  14. NSArray *visibleIndexPaths = [self indexPathsOfItemsInRect:rect]; 
  15.   
  16. for (NSIndexPath *indexPath in visibleIndexPaths) { 
  17.   
  18. UICollectionViewLayoutAttributes *attributes = 
  19.   
  20. [self layoutAttributesForItemAtIndexPath:indexPath]; 
  21.   
  22. [layoutAttributes addObject:attributes]; 
  23.   
  24.   
  25. // Supplementary views 
  26.   
  27. NSArray *dayHeaderViewIndexPaths = [self indexPathsOfDayHeaderViewsInRect:rect]; 
  28.   
  29. for (NSIndexPath *indexPath in dayHeaderViewIndexPaths) { 
  30.   
  31. UICollectionViewLayoutAttributes *attributes = 
  32.   
  33. [self layoutAttributesForSupplementaryViewOfKind:@"DayHeaderView" 
  34.   
  35. atIndexPath:indexPath]; 
  36.   
  37. [layoutAttributes addObject:attributes]; 
  38.   
  39.   
  40. NSArray *hourHeaderViewIndexPaths = [self indexPathsOfHourHeaderViewsInRect:rect]; 
  41.   
  42. for (NSIndexPath *indexPath in hourHeaderViewIndexPaths) { 
  43.   
  44. UICollectionViewLayoutAttributes *attributes = 
  45.   
  46. [self layoutAttributesForSupplementaryViewOfKind:@"HourHeaderView" 
  47.   
  48. atIndexPath:indexPath]; 
  49.   
  50. [layoutAttributes addObject:attributes]; 
  51.   
  52.   
  53. return layoutAttributes; 
  54.   
 
layoutAttributesFor…IndexPath
有时,collection view会为某个特殊的cell,supplementary或者decoration view向布局对象请求布局属性,而非全部可见的对象。这就是当其余三个方法开始起做用时,你实现的layoutAttributesForItemAtIndexPath:须要建立并返回一个单独的布局属性对象,这样才能正确的格式化传给你的index path所对应的cell。
 
你能够经过调用 +[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:]这个方法,而后根据index path修改属性。为了获得须要显示在这个index path内的数据,你可能须要访问collection view的数据源。到目前为止,至少确保设置了frame属性,除非你全部的cell都位于彼此上方。
  1. - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath 
  2.   
  3.   
  4. CalendarDataSource *dataSource = self.collectionView.dataSource; 
  5.   
  6. id<CalendarEvent> event = [dataSource eventAtIndexPath:indexPath]; 
  7.   
  8. UICollectionViewLayoutAttributes *attributes = 
  9.   
  10. [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; 
  11.   
  12. attributes.frame = [self frameForEvent:event]; 
  13.   
  14. return attributes; 
  15.   
 
若是你正在使用自动布局,你可能会感到惊讶,咱们正在直接修改布局参数的frame属性,而不是和约束共事,但这正是UICollectionViewLayout的工做。尽管你可能使用自动布局来定义collection view的frame和它内部每一个cell的布局,但cells的frames仍是须要经过老式的方法计算出来。
 
相似的,layoutAttributesForSupplementaryViewOfKind:atIndexPath: 和 layoutAttributesForDecorationViewOfKind:atIndexPath:方法分别须要为supplementary和decoration views作相同的事。只有你的布局包含这样的视图你才须要实现这两个方法。UICollectionViewLayoutAttributes包含另外两个工厂方法,+layoutAttributesForSupplementaryViewOfKind:withIndexPath: 和 +layoutAttributesForDecorationViewOfKind:withIndexPath:,他们是用来建立正确的布局属性对象。
 
shouldInvalidateLayoutForBoundsChange:
最后,当collection view的bounds改变时,布局须要告诉collection view是否须要从新计算布局。个人猜测是:当collection view改变大小时,大多数布局会被做废,好比设备旋转的时候。所以,一个幼稚的实现可能只会简单的返回YES。虽然实现功能很重要,可是scroll view的bounds在滚动时也会改变,这意味着你的布局每秒会被丢弃屡次。根据计算的复杂性判断,这将会对性能产生很大的影响。
 
当collection view的宽度改变时,咱们自定义的布局必须被丢弃,但这滚动并不会影响到布局。幸运的是,collection view将它的新bounds传给shouldInvalidateLayoutForBoundsChange: method。这样咱们便能比较视图当前的bounds和新的bounds来肯定返回值:
  1. - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 
  2.   
  3.   
  4. CGRect oldBounds = self.collectionView.bounds; 
  5.   
  6. if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds)) { 
  7.   
  8. return YES; 
  9.   
  10.   
  11. return NO; 
  12.   
 
动画
插入和删除
UITableView中的cell自带了一套很是漂亮的插入和删除动画。可是当为UICollectionView增长和删除cell定义动画功能时,UIKit工程师遇到这样一个问题:若是collection view的布局是彻底可变的,那么预先定义好的动画就没办法和开发者自定义的布局很好的融合。他们提出了一个优雅的方法:当一个cell(或者supplementary或者decoration view)被插入到collection view中时,collection view不只向其布局请求cell正常状态下的布局属性,同时还请求其初始的布局属性,好比,须要在开始有插入动画的cell。collection view会简单的建立一个animation block,并在这个block中,将全部cell的属性从初始(initial)状态改变到常态(normal)。
 
经过提供不一样的初始布局属性,你能够彻底自定义插入动画。好比,设置初始的alpha为0将会产生一个淡入的动画。同时设置一个平移和缩放将会产生移动缩放的效果。
 
一样的原理应用到删除上,此次动画是从常态到一系列你设置的最终布局属性。这些都是你须要在布局类中为initial或final布局参数实现的方法.
 
initialLayoutAttributesForAppearingItemAtIndexPath:
 
initialLayoutAttributesForAppearingSupplementaryElementOfKind:atIndexPath:
 
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
 
finalLayoutAttributesForDisappearingItemAtIndexPath:
 
finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
 
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
 
布局间切换
能够经过相似的方式将一个collection view布局动态的切换到另一个布局。当发送一个setCollectionViewLayout:animated:消息时,collection view会为cells在新的布局中查询新的布局参数,而后动态的将每一个cell(经过index path在新旧布局中判断出相同的cell)从旧参数变换到新的布局参数。你不须要作任何事情。
 
结论
根据自定义collection view布局的复杂性,写一个一般很不容易。确切的说,本质上这和从头写一个完整的实现相同布局自定义视图类同样困难了。由于所涉及的计算须要肯定哪些子视图当前是可见的,以及他们的位置。尽管如此,使用UICollectionView仍是给你带来了一些很好的效果,好比cell重用,自动支持动画,更不要提整洁的独立布局,子视图管理,以及数据提供架构规定(data preparation its architecture prescribes.)。
 
自定义collection view布局也是向轻量级view controller迈出很好的一步,正如你的view controller不要包含任何布局代码。正如Chris的文章中解释的同样,将这一切和一个独立的datasource类结合在一块儿,collection view的视图控制器将很难再包含任何代码。
 
每当我使用UICollectionView的时候,我被其简洁的设计所折服。对于一个有经验的Apple工程师,为了想出如此灵活的类,极可能须要首先考虑NSTableView和UITableView。
相关文章
相关标签/搜索