想研究下collection view自定义布局,所以通读apple文档,顺手翻译记下来,供以后翻阅(水平有限,错误在所难免,请原谅我蹩脚的英文)
在你开始创建一个自定义layout的时候,先考虑一下是否真的需要。
UICollectionViewFlowLayout已经提供的特性,可以实现很多不同种类的布局。满足一下条件,可以考虑用自定义layout:
解释一下:
- 如果你需要的layout跟UICollectionViewFlowLayout样式差别过大
- 多方向滚动
- 修改UICollectionViewFlowLayout比创建一个自定义还麻烦
好消息是API很清晰,实现一个自定义layout并不困难,最难的部分是在布局中通过计算确定每个cell的位置,当你搞定这些信息,提供给collection view是很简单的。
对于自定义layout, 你需要继承UICollectionViewLayout,只有一少部分核心方法必须需要你实现的,其他方法按需实现,这些核心方法只要来完成这些重要的任务:
你可以只实现这些核心方法,但如果你实现一些可选方法会让你的layout看起来更加牛逼!
layout对象可以根据data source提供的信息创建出collection view 的layout。
你的layout通过调用collectionView 属性方法跟data source进行通信,这些属性在所有layout方法中都是可以访问的。
在layout过程中,你要明白,你的collection view知道什么,不知道什么,因为collection view不能追踪布局或者views的位置,甚至,layout对象不会限制你去调用任何collection view的方法,所以,别指望collection view帮你计算布局。
collection view布局工作都由自定义layout对象进行处理。当collection view需要布局信息的时候,它会向layout对象要求提供这些信息。
举个例子,collection view首次显示或者resize的时候,它会向layout要这些信息。
你也可以调用layout对象的invalidateLayout
方法通知collection view更新自己的布局。这个方法把存在的layout信息全部丢弃,然后layout对象会重新生成布局信息。
注意:不要把
invalidateLayout
方法跟collection view的reloadData
方法搞混了。
不恰当地调用invalidateLayout
将导致collection view 废弃掉已经存在的布局,和子视图
当然了,如果删除、移动或者添加cell,重新计算所有的布局是有必要的。
如果data source中得数据改变了,调用reloadData
更好
在整个布局过程中, collection view 调用layout对象的方法。
你可以在这些方法中计算cell的位置和给collection view 提供一些必要的信息,其他的方法也可能调用,但是以下几个方法在整个布局过程中调用最为频繁,且调用顺序如下:
prepareLayout
方法为布局计算做一些准备工作collectionViewContentSize
方法返回内容区域的sizelayoutAttributesForElementsInRect:
方法返回矩形区域内cells或者views的属性5-1 说明了你怎样使用上述方法产生布局信息
prepareLayout
方法里面做布局需要的所有cells和views位置相关的计算, 最少你也要在这个方法中计算出内容区域的size,以供第二步返回使用。
collection view 使用content size 配置自己scrollview,举个例子,当你计算的content size超过设备的屏幕大小,scrollview便能够同时横向和纵向移动了。 不像 UICollectionViewFlowLayout, 它不默认的调节布局使之只能一个方向滚动。
基于当前的滚动位置,collection view 会调用layoutAttributesForElementsInRect:
方法获取指定矩形区域内cells和views的属性,这个指定区域跟可见区域大小可能相同,也可能不相同,返回这些信息之后,核心布局过程已经完成了。
布局完成之后,你cells和views中的属性,会被保留,除非你或者collection view主动废弃了这些布局。
调用invalidateLayout
会导致布局过程重新开始,再次从prepareLayout
开始
collection view滚动的时候,也可能会自动废弃布局,当用户滚动它的内容的时候,collection view会调用layout 对象的shouldInvalidateLayoutForBoundsChange:
方法,如果该方法返回YES,便会废弃约束。
注意 记住调用
invalidateLayout
方法不会立刻开始更新布局很有用。当数据和布局不一致的时候,才需要调用这个方法,在下一个视图更新循环中,collection view会检查是否自己的约束需要更新,如果需要,就更新,事实上,你可以在一个很短的时间内多次调用invalidateLayout
方法,但并不会每次都出发布局更新
你layout生成的属性是UICollectionViewLayoutAttributes的实例变量,这些实例变量可以在你app不同的方法里创建。
当你的app不是处理成千上万条数据,你完全可以在prepareLayout
方法里面创建,因为你的布局会被缓存和引用。
但如果这样的成本高过所得到的效率的话,那在属性使用的时候创建也是很容易的。
不管怎样,当你新创建一个UICollectionViewLayoutAttributes
实例的时候,从以下几个方法中选一个吧:
layoutAttributesForCellWithIndexPath:
layoutAttributesForSupplementaryViewOfKind:withIndexPath:
layoutAttributesForDecorationViewOfKind:withIndexPath:
对于不同的view,你必须使用正确的类方法,因为collection view会使用这些信息取向data source对象请求view的类型,使用不正确的方法将导致collection view创建错误的视图,你想要的布局也不会出现。
创建每个属性对象之后,设置相应地属性到对应的view上,最少你要设置view的大小和位置,view之间有重叠的部分,你需要给zIndex
赋值,来保证这些view的层级关系。其他属性让你可以控制是否可见或者外观,是否可以按照要求改变,如果这些标准的属性类型不满足你的需求,你可以实现子类,扩展他们去存储其他属性。当你使用了子类属性对象,你必须实现isEqual:
方法,用来比较属性,因为collection view一些操作用到了这个方法。
在布局开始的时候,layout对象会先调用prepareLayout
方法,这个方法里面你可以计算一会儿你要用到的信息。 prepareLayout
方法并不是必须实现的,但是它给你一个机会去做一些必要地初始化计算。
这个方法调用后,你计算出来的信息必须能够计算出collection view的content size.
布局的最后一步,collection view会调用layoutAttributesForElementsInRect:
方法,这个方法的目的就是提供指定区域内cells,supplementary,或者decoration view需要的属性。
如果是一个很大的滚动区域,collection view可能只是需要可见区域的属性, 在图 5-2中, 需要就是6-20和第二个headerview的布局属性, 你必须准备好这些布局属性。这些属性可能用来做删除插入动画。
因为这个layoutAttributesForElementsInRect
方法在prepareLayout
之后调用,所以你应该已经有了绝大多数的信息取创建并返回需要的属性,实现layoutAttributesForElementsInRect
方法需要以下几步:
prepareLayout
生成的数据,决定是访问缓存还是创建一个新的。layoutAttributesForElementsInRect
给的矩形区域内(可交叉)UICollectionViewLayoutAttributes
对象到一个数组 取决于你怎样管理你的布局信息,你可能会在prepareLayout
方法,或者在layoutAttributesForElementsInRect
方法中创建UICollectionViewLayoutAttributes
对象。
不管使用哪种方式,谨记效率,重复计算一个新布局属性是非常昂贵的操作,这样对你app的体验是非常有害的。换个说法,当你collection view item数量巨大,你应该考虑在需要的时候才去创建这些属性,这是一个很简单的策略。
注意: layout对象也需要能够为一些item立刻提供属性,collection view可能会因为一些特殊原因,包括创建动画,去要求这些信息
collection view会定期向你的layout对象要求特殊的属性,举个例子,当你配置插入和删除动画的时候,collection view会要求这些信息,你的layout对象必须准备好为这些cell,supplementary,decoration提供支持布局属性,你可以复写一下方法取做这件事:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
layoutAttributesForDecorationViewOfKind:atIndexPath:
有时限这些方法应该取回cell或者view的布局属性,每个自定义布局对象都有必要实现layoutAttributesForItemAtIndexPath:
这个方法。如果你的布局不包含任何supplementary views,你不用实现layoutAttributesForSupplementaryViewOfKind:atIndexPath
这个方法,同样地,如果不包含decoration views, 你也不用实现layoutAttributesForDecorationViewOfKind:atIndexPath:
这个方法,当返回这些属性的时候,你不应该更新这些属性,如果你需要更改布局信息,废弃掉这个layout 对象,让它重新更新,重新开始一个布局过程。
这里有两个方法可以是使用你的自定义布局:纯代码,和通过storyboard,collection view会通过一个属性与你的自定义布局相关联--collectionViewLayout
.
self.collectionView.collectionViewLayout = [[MyCustomLayout alloc] init];
为每个cell提供布局属性是必要的,但是你的layout还有其他可以提升用户体验的特性,实现这些特性不是必须的,但非常建议。
略
插入和删除cells和views是一个非常有趣的问题, 插入一个cell会造成其他cell和view布局的改变。
因为layout对象知道怎样对已经存在的cell和view从当前位置移动到一个新位置做动画, 但是,它并不知道新cell会被插入的位置,无动画的插入一个新的cell,collection view为了做这个动画,会向layout对象要求提供一系列属性。当一个cell被删除的时候,过程也相似。
去理解这些初始化属性怎样工作,看一个例子是很有帮助的,图5-3展示了一个只有三个cell的collection view,当一个新的cell被插入的时候,layout对象会提供给collection view这个cell的初始属性。这样,layout对象会设置cell的位置到collection view的中间,并且把alpha值从0设置为1,在动画期间,这个新cell会渐渐地出现移动到collection view的中央,最后的位置在右下角。
5-2展示了相关代码
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
CGSize size = [self collectionView].frame.size;
attributes.center = CGPointMake(size.width / 2.0, size.height / 2.0);
return attributes;
}
注意:当,一个cell插入的时候,5-2 代码会将所有的cell都做动画,但第四个之前的cell已经展示完毕了,再做动画也不合适。只为刚插入的cell做动画,检查一下这个方法的index path是否跟
prepareForCollectionViewUpdates:
传入的index path一致, 如果一致,则做动画,否则就调用super的initialLayoutAttributesForAppearingItemAtIndexPath:
方法
删除的处理过程跟插入的完全一致,除了你需要指定最终属性,而不是初始实行,根据刚才的例子,如果你使用相同的属性删除一个cell,cell会慢慢消失同时移动到collection view的中间,在UICollectionViewLayout
中有六个方法可用--两个分离的方法(初始参数和最终参数)
你自定义layout对象会影响滚动的效果去创建一个更好地体验。当滚动相关的触摸事件结束后,scrollview会根据当前的速度和减速率决定最后静止的内容区域,当collection view知道了这个位置,它会调用layout对象的targetContentOffsetForProposedContentOffset:withScrollingVelocity:
方法,是否位置需要改变。