创建自定义UICollectionView layout
在创建自定义的layout之前,你需要知道UICollectionViewFlowLayout提供的很多特性已经 经过优化以满足多种常用的layout。除非是如下情况,否则不建议自定义
1 你所想实现的外观并不是网格或者 line-based breaking 布局(items排成一行直到行满,再继续往下一行上去排,直到所有items都排列完成),或者必须要在多个方向上都可以滚动
2 需要频繁地改变所有 Cell的位置,以致于创建自定义layout比修改现有flow layout工作量更省
记住:自定义最难的部分是确定布局中各item位置所需要的计算
继承UICollectionViewLayout
继承UICollectionViewLayout之后只需要重载几个提供布局核心特性的方法,其他方法只需按情况重载即可,核心特性如下:
1 指定可滚动内容区域的size
2 为布局中的每个Cell及view提供属性对象
layout对象需要用到datasource以创建collection view的layout对象,其通过layout自身的collectionView属性访问此datasource。需要注意的是,知道 layout过程中哪些信息可以从collection view中访问到,哪些不可以 是非常重要的。因为layout过程中,collection view 是无法获知各View的布局以及位置的。所以尽量避免通过 collection view获取除layout之外的信息。
深入理解布局过程
collection view完全通过你自定义的layout对象管理整个布局过程,如 collection view 首次布局或者resize的时候,会向布局对象获取相关信息。你也可以手动调用invalidateLayout方法以更新布局对象,此方法会强制生成新layout。(需要注意invalidateLayout与reloadData的区别,在移动,添加或者删除item的时候,需要摒弃原有布局,重新生成新的布局,使用invalidateLayout,而如果只是datasource中的数据有更新,这时需要使用reloadData)
layout过程中,如下方法提供了layout的基本信息,其他方法也会被调用,但如下这些方法总是按如下顺序调用的:
1 prepareLayout方法调用来为即将进行的layout作前期的计算
2 collectionViewContentSize方法基于初始计算,返回整体内容区域的size
3 layoutAttributesForElementsInRect:方法返回指定区域中cells和views的属性
演示如上3个方法的调用过程prepareLayout方法是为确定布局中各cell和view位置做计算,需要在此方法中算出足够的信息以供后续方法计算内容区域的整体size,collection view使用content size 以正确地配置scroll view。比如 content size 长宽均超过屏幕的话,水平与竖直方向的滚动都会被enable。基于当前滚动位置,collection view会调用 layoutAttributesForElementsInRect:方法以请求特定rect(有可能是也可能不是可见rect)中cell和view的属性。到此,core layout process已经结束了。
layout结束之后,cells和views的属性在你或者collection view invalidate布局之前都不会变,collection view可以在滚动的过程中自动invalidate 布局:用户滚动内容过程中,collection view调用layout的shouldInvalidateLayoutForBoundsChange:方法,如果返回值为YES则invalidate 布局。(但需要知道的是,invalidateLayout并不会马上触发layout update process,而是在下一个view更新周期中,collection view发现layout已经dirty才会去更新)
创建布局属性
自定义layout需要返回UICollectionViewLayoutAttributes类的对象,这些对象可以在很多不同方法中创建,但创建时间可以根据具体情况具体决定。如果collectionview未有数千的item,则prepare layout时创建会比在用户滚动过程中用到时再计算更可取,因为创建的这些属性可以缓存起来。如果计算所有属性并缓存起来所带来的性能消耗比请求时获取的消耗更大,则可请求时再创建相关属性对象。
创建UICollectionViewLayoutAttributes类对象新实例时,可以使用这样几个方法:layoutAttributesForCellWithIndexPath:,layoutAttributesForSupplementaryViewOfKind:withIndexPath:,layoutAttributesForDecorationViewOfKind:withIndexPath:,基于展示的view类型的不同,必须使用正确的类方法,因为collection view使用这些信息向datasource对象请求适当类型的view。使用错误的方法会引起collection view在错误的地方创建错误的view,你所希望呈现的layout就不会出现。
创建每个属性对象之后,将相应View的相关属性都设置上。最少要在layout中设置view的size和position。如果在你的布局中有view重叠了,需要正确配置zIndex属性以维持重叠views的一致的有序状态。其他属性可以让你控制cell或者view的可见性或者外观表现。如果标准属性类无法满足你的需要,可以继承并对其进行扩充以存储每个View的其他信息。继承layout属性时,需要实现属性的isEqual:方法因为collectionview需要使用这个方法。
给定矩形中的items的布局属性
layout processs的最后,collection view会调用你的layout对象的layoutAttributesForElementsInRect:方法。对一个大的可滚动的内容区域,collectionview可能只会请求当前可见的那部分区域中的所有items的属性。当然,这个方法需要支持获取任意rect中items的信息,因为有可能在插入及删除时需要做动画效果。
可见区域中的itemslayoutAttributesForElementsInRect:方法的实现需要遵循如下步骤:
1 遍历prepareLayout方法产生的数据以访问缓存的属性或者创建新的属性
2 检查每个item的frame以确定是否与layoutAttributesForElementsInRect:方法中指定rectangle有重叠部分
3 对每个重叠的item,添加一个对应的UICollectionViewLayoutAttributes对象到一个数组中
4 返回布局属性的数组给collection view
不仅要记住缓存layout信息能够带来性能提升,也要记住不断重复为cells创建新layout属性的计算代价是十分昂贵的,足以影响到app的性能。当collection view管理的items量很大时,采用在请求时创建layout属性的方式是十分合理的。
按需提供布局属性对象
collection view会在正常的layout 过程之外周期性地让你提供单个items的layout对象。比如为某item配置插入和删除动画时。自定义的layout通过如下方法提供这些信息:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
layoutAttributesForDecorationViewOfKind:atIndexPath:
返回属性时,不能更新这些layout属性,如果需要改变layout信息,调用invalidateLayout,在接下来的layout周期中更新这些信息。上述方法中layoutAttributesForItemAtIndexPath:是所有自定义 layout都必须重载的方法,如果有supplementary view和decoration view可以分别重载下面两个方法。
可以通过self.collectionView.collectionViewLayout = [[MyCustomLayout alloc] init];方式也可以在storyboard文件中设置collection view 的class属性
让你的layout更优异
除了上述这些必须实现的方法,还有一些特性能够改善自定义layout的用户体验,实现这些属性是可选但推荐实现的。
通过 附加view 提供内容品质
supplementary views与Cells分离且有自己的layout属性,由Datasource提供,且其目的是为app主要内容增强信息。与cells一样,supplementary view也会经历重用的过程以最小化collection view使用的资源消耗。所以所有 supplementary view都需要继承UICollectionReusableView。
添加supplementary view到layout中的过程如下:
1 注册supplementary view到layout对象中,registerClass:forSupplementaryViewOfKind:withReuseIdentifier: or registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
2 在datasource中实现collectionView:viewForSupplementaryElementOfKind:atIndexPath:,由于这些view是可重用的,调用dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:来获取可用的view
3 但为Cells创建一样为supplementary Views创建layout 属性对象
4 layoutAttributesForElementsInRect:方法中返回的属性数组中包含supplementary view的layout属性对象
5 实现layoutAttributesForSupplementaryViewOfKind:atIndexPath:方法为特定supplementary View返回属性对象
处理supplementaryview布局属性的过程和cell属性的过程一样,但不同的是supplementary view可以有很多种但只能有一种Cell。这是因为 supplementary view与它们是分离开的,是为了烘托主旨,所以每个supplementary view方法都会指明其各类以方便正确计算其特有的属性。
在layout中添加Decoration Views
Decoration Views是layout UI特征的有效点缀,与cell和supplementary view不同的是,它只做外观呈现用,所以与datasource无关。可以用来提供自定义背影,在Cells缝隙之间填充,甚至可以掩盖cell,它完全由layout对象控制。
在layout中添加Decoration view步骤如下:
1 用registerClass:forDecorationViewOfKind: or registerNib:forDecorationViewOfKind: method方法注册自定义的decoration view,但记住是在layout对象中注册
2 layout对象中layoutAttributesForElementsInRect:方法中为decoration view创建属性
3 实现layoutAttributesForDecorationViewOfKind:atIndexPath:方法并在请求时返回decoration view的布局属性
4 选择性地实现initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath: 和 finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:方法以处理出现和消失的动画,可参考下面的插入和删除动画部分
由于decoration view与cell和supplementary view的创建过程不同,注册class或者 nib即可,最多需要调用 一个initWithFrame:方法。但任何decoration view也需要是UICollectionReusableView子类,因为 也对其启用了回收机制。
插入和删除动画
插入及删除cell时collection view会询问layout对象提供一组初始化属性用于动画,同样,删除元素时会询问一组终值属性。
初始化属性演示item插入时,layout对象提供正要被插入的item的初始化layout信息。在此例中,layout先将Cell的初始化位置设置到Collection view中间,并将其alpha通道设置为0,动画期间,此item会渐现并从中间移动到右下角。下面的代码描述了如何设置初始化信息及实现动画:
插入动画示例需要注意的是,上述代码会使得此item插入的时候对所有Cell都会添加此插入的动画,若只想对插入的item做插入动画,可以检查 index path是否与传入prepareForCollectionViewUpdates:方法的item的index path匹配,并只在有匹配到的时候才进行动画,否则只返回super initialLayoutAttributesForAppearingItemAtIndexPath:
delete动画与插入类似,提供正确的final 属性即可
提升layout的滚动体验
滚动的时候scrollview会根据当前的speed和减速状况决定最终会停在哪个偏移,当算出这个停留位置之后,其会调用 targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法是否要改变这个位置,由于其是在滚动过程中调用此方法,所以自定义layout可以改变滚动的仪停留位置。
下图展示了调整滚动特性的效果
调整content offset停到合适的位置假定collection view开始于(0,0),且用户向左侧滑,collection view计算出滚动原本会停下的位置,自定义layout可能会改变这个值以确保滚动停下的时候,某个item正好停留在可见区域正中间。这个新值会成为新的目标content offset,且会从targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法返回。
友情提示
1 items数较小(数百),或者items layout信息变化较小 时,可以在prepareLayout中创建并缓存layout信息
2 尽量不要继承UICollectionView
3 不要在layoutAttributesForElementsInRect:方法中调用uicollectionview的visiblecells方法,因为其实这个调用是转化成了向layout对象请求visible cells