UICollectionView详解
本篇文章主要是书《UICOllectionView The Complete Guide》的读书笔记,对比iCrousel源码,并用UICollectionView来实现iCrousel提供的部分的卡片效果。前段时间做了一个左右滑动界面的需求,用的是三个UICollectionView相互嵌套来完成了,在做的过程中同事说可以使用iCrousel来实现,但是对iCrousel不太熟悉,最终我还是使用的是UICollectionView,先完成任务之后开始研究iCrousel。结合着这个过程中遇到的问题,自己把iCrousel的源码看了一遍,为了看的懂源码,中间有看了一本《Core Animation Advanced Techniques》。因为这本书已经有大神翻译好了,有时候原版看着费劲的时候就对着翻译看,但是在看《UICollectionView The Complete Guide》的时候就没有这么幸运了,在网上一直没有找到译本,只能看原版。还记得需求是8月初的时候做的,当我学完相关知识的时候已经到了十一月底了,当然中间还有些公司的任务要开发,中途还看了一些其他的书籍,想着如果文笔好的话还能写个鸡汤类的读书笔记啥的,但是写了点东西才知道,自己能把一个技术点写清楚已经很费劲了,还是要继续学习啊~
趁着刚刚看完,做一个读书笔记,之后就不用再去翻书找知识点了。
概述
看了下UICollectionView,觉的这个是一个有点像RN的组件,内容和布局完全分离的设计,UICollectionView负责界面部分,UICollectionViewlayout负责UIcollectionView的布局,具体的每个元素的布局就交给UICollectionViewLayoutAttributes,另外attributes也是可以进行扩展的,比如需要加入maskView或者改变layer的属性,都可以在attributes里面进行自己的定义。
iCrousel的主要的实现方式是将view加载到视图的中间,然后根据设定好的几种展示风格设计好transform3D。比较意外的是没有使用UIScrollview来实现视图的滚动,而是使用逐帧动画的原理,根据ios设备屏幕的刷新速度来计算每一帧的所有view的位置。ios设备的刷新频率是60hz,也就是说一秒钟60次,结合动画一秒24帧就不会感觉卡顿,难怪动画会这么流畅。
因为UICollectionView是UIScrollView的子类,而iCrousel是计算每个item的偏移,所以实现方式上也是有很大的差异,iCrousel需要手动计算,而UICollectionView可以直接使用UIScrollview带来的便利。
重用问题
在UICollectionView中重用的控件有三种,分别是UICollectionViewCell、SupplementatyView和DecorationView,其中UICollectionViewCell就不用多说了,SupplementaryView指的是header和footer。DecorationView指的是装饰图,既然是装饰图,那么应该是独立于view的,所以是在layout里面进行设置的。这两个view是继承UICollectionReusableView的,所以UICollectionView帮我们都做好了重用。
从iCrousel中可以大概猜测到重用的机制是怎么实现的。在iCrousel里面维护了两个队列,一个是屏幕上可见items的队列,另一个是不可见的items的队列,也就是重用队列。实时的更新两个队列,每次先检查重用队列里面有没有可以重复使用的item,如果有的话就任取一个拿来使用,如果没有话的再新建一个。
类比到UICollectionView里面的话,自我感觉应该是UICollectionReusableView或者UICollectionViewCell被加入到重用队列的时候会触发一个prepareForReuse的钩子方法,在这个方法里面我们就可以做一些view被重用的准备。
UICollectionView
UICollectionView的用法与UITableView的用法类似,如果是创建一个简单的UICollectionView界面的话,那么完全可以像使用UITableView一样来使用。其方法也是实现类似的dataSource和Delegate。但距离UITableView框架问世已经过去了好多年了,随着每一代ios系统的升级,UITableView也带来了很多的变化,毕竟有一些局限性。所有就有了UICollectionView。
UICollectionViewCell
首先看一下UICollectionViewCell的层次结构:
UICollectionCell层次
从这张图上我们可以看出来,UICollectionViewCell之上还有还有三个View,分别是backgroundView和selectedBackgroundView,最外层的才是contentView,从字面上理解就是背景图层,选中的背景图层,最后才是我们编辑的内容图层。所以我们自定义的view应该是加上contentView上面才对。另外我们使用xib文件或者是storyboard文件创建的CollectionViewCell的时候不会看到有其他的图层,甚至看不到像tableview一样的contentView,是因为我们创建的view就是默认添加到contentView上面的。所以用代码来构建页面的时候要注意添加到contentView上面来,这样才不会破坏UICollectionViewCell的层级结构,当我们使用到选中态的时候才不会发生不必要的麻烦(现在终于知道了,其实自己bug多的原因真的是对知识的一知半解导致的)。另外selectedBackgroundView和backgroundView都是option的,当有的时候才会加入到view的层级当中。
性能优化
在之前core_animation笔记里面有提到tableView加载很多图片时的优化问题。后台加载图片,后台解压图片(png格式比jpeg块),避免频繁的离屏渲染等方面来加载图片。另外这本书的前半部分还有使用Instruments来分析哪个方法造成了界面的卡顿,界面的卡顿也就说掉帧的问题,因为耗时任务造成没有达到60Hz的渲染,就会有卡顿的现象出现。通过Instruments的Core Animation工具就能分析出哪个方法耗时较长,从而定位到问题。
离屏渲染是将渲染好的纹理缓存在GPU,不需要每次重新渲染,对高频率使用的图层优化较大,比如把UICollectionViewCell的shouldRasterize设置成YES来触发离屏渲染,这样在做动画的的时候不用每一帧都重复渲染,减少GPU的压力。直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。
UICollectionViewLayout
UICollectionViewLayout是一个没有实现具体方法的类(是否可以叫做抽象类,因为确实很多方法需要重载才有有效),只有继承并实现相关的方法之后才能够使用。苹果的SDK已经帮我们实现了一个流式布局的类,就是UICollectionViewFlowLayout,一般的流式布局都是可以通过继承这个类来实现。我们可以在flowLayout里面设置itemSize,sectionInset,item间距,item行距等基本的Cell的设置。设置好cell的基本属性之后,剩下的flowlayout就会帮我们把布局根据流布局的规则计算出来了。一般我们使用flowLayout的之类已经足够了。layout管理着UICollectionView的布局,也就是说它管理着界面上能看到的一切,无非就是UICollectionViewCell,SupplementatyView和DecorationView。每个元素对应了一个UICollectionViewLayoutAttributes,通过这个管理attributes属性就能够改变UICollectionView的布局,主要是要实现以下几个方法:
- (NSArray *)layoutAttributesForElements:(Rect)rect;
这个方法会返回在这个rect里面所有的attributes数组,可以通过复写这个方法来编辑SupplementatyView和DecorationView的attributes属性,判断attributes是否为cell的方法就是判断attributes的representedElementKind属性,当前为nil的时候就是cell。具体区分的话就是要使用representedElementCategory属性了。
这个方法之后还有针对每个indexPath的修改attributes的方法,也是属于同一种方法类型,具体可以查阅文档(个人感觉文档已经很清楚了,不过很多知识还是需要看一些例子才能将这些零碎的知识点整合起来)。
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;
这个方法是用来判断是否CollectionView的bounds发生变化的时候就更新attributes,所以当我们需要实时的更新attributes的时候,这个方法就要返回YES。因为UICollectionView是UIScrollview的子类类,而UIScrollview的实现方式就是UIView改变自身的bounds从而改变可见的范围,所以这个方法就是当scrollview发生偏移的时候(bounds发生变化)就将layout无效,重新计算layout。这里还需要注意一个问题,就是屏幕发生旋转的时候bounds也会发生改变,并且在旋转的时候默认使用的动画是移除所有items和添加所有items所用动画,所以如果自定义了item出现和消失动画的话就会在旋转屏幕的时候对每一个item重复做动画。解决方案在网上有查到一个方法,详见:UICollectionView动画
initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingSupplementaryElementOfKind:atIndexPath:
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
这些方法就是上面所说的定义item消失和添加时候的动画,当然也可以对Supplementary view和Decoration view的动画。
还有两个方法也很重要,用来识别bounds改变的动画和update的动画,同时用这两个方法可以避免上面所说的屏幕旋转动画的异常。
- prepareForCollectionViewUpdates:
- prepareForAnimatedBoundsChange:
第一个方法是在view更新的时候触发的方法,第二个方法是在bounds发生改变的时候触发的方法,在这两个方法里面可以分别申请两个数组来区分两种动画。
UICollectionViewLayoutAttributess
这个名字比较长的类似用来存储UICollectionView元素布局的属性,可以改变frame,bounds,center,size,transform,transform3D,alpha,zIndex,hidden等属性,其中zIndex反映的是view的层级关系,默认的zIndex值是0,大的在层级的上面,小的在下面。transform3D可以给每个元素添加transform3D属性,这样的话就可以仿照iCrousel来实现相似的效果了。
自定义UICollectionViewLayoutAttributess属性
对于继承于UICollectionViewLayoutAttributess的子类,我们可以自定义一些items需要表现的属性,比如是否光栅化,自定义maskView的透明度之类的我们自定义items或者Supplementary和Decoration的表现。比如说我要根据偏移来计算不同的maskView 透明度的变化,那么就要给UICollectionViewLayoutAttributess子类加上属性alpha,同时要复写copy和equal方法。对于自定义的属性,我们在实现itemCell的时候需要复写方法 -applyLayoutAttributes: ,在这个方法里面可以接受作用于这个itemCell的attributes,对于我们自定义的属性,需要在这个方法里面让itemCell根据属性做一些相应的改变。
关于光栅化会触发离屏渲染,上面也有所离屏渲染会带来很多开销,但是对于cell图层比较多,合成起来比较费时,并且频繁重用的view来说,进行离屏渲染是很有帮助的。
小结
关于UICollectionView的知识点大约就是这么多,但是具体要实现哪些动画还是需要我们自己来实现相应的算法。相比于iCrousel,UICollectionView已经实现好了重用机制,并且将layout的代码独立出来,也是使用transform3D属性来改变item的位置,所以用UICollectionView实现iCrousel的布局方式只需要将其对每个item的transform3D算法转移到item对应的attributes里面就可以了。