CollectionView自定义布局

作者: ParkinWu | 来源:发表于2015-11-06 13:50 被阅读12061次

    想研究下collection view自定义布局,所以通读apple文档,顺手翻译记下来,供以后翻阅(水平有限,错误在所难免,请原谅我蹩脚的英文)

    一、创建自定义layout

    在你开始创建一个自定义layout的时候,先考虑一下是否真的需要。
    UICollectionViewFlowLayout已经提供的特性,可以实现很多不同种类的布局。满足一下条件,可以考虑用自定义layout:

    • 你需要的layout一点也不像一个网格状的layout,或者line-based breaking layout(就是,当cell铺满一行后,接着再下一行铺,一直到所有cell展示完毕),或者需要多方向滚动的时候
    • 你想要经常改变cell的位置,而且修改flow layout比创建一个自定义layout还要麻烦的时候

    解释一下:

    1. 如果你需要的layout跟UICollectionViewFlowLayout样式差别过大
    2. 多方向滚动
    3. 修改UICollectionViewFlowLayout比创建一个自定义还麻烦

    好消息是API很清晰,实现一个自定义layout并不困难,最难的部分是在布局中通过计算确定每个cell的位置,当你搞定这些信息,提供给collection view是很简单的。

    二、继承UICollectionViewLayout

    对于自定义layout, 你需要继承UICollectionViewLayout,只有一少部分核心方法必须需要你实现的,其他方法按需实现,这些核心方法只要来完成这些重要的任务:

    • 指定能滚动的区域大小
    • 给cell提供属性对象, 使你的layout能够正确的摆放cell(也就是给每个cell定位)

    你可以只实现这些核心方法,但如果你实现一些可选方法会让你的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 提供一些必要的信息,其他的方法也可能调用,但是以下几个方法在整个布局过程中调用最为频繁,且调用顺序如下:

    1. 使用prepareLayout方法为布局计算做一些准备工作
    2. 使用 collectionViewContentSize方法返回内容区域的size
    3. 使用layoutAttributesForElementsInRect:方法返回矩形区域内cells或者views的属性

    5-1 说明了你怎样使用上述方法产生布局信息

    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一些操作用到了这个方法。

    五、 准备布局(Preparing the Layout)

    在布局开始的时候,layout对象会先调用prepareLayout方法,这个方法里面你可以计算一会儿你要用到的信息。 prepareLayout方法并不是必须实现的,但是它给你一个机会去做一些必要地初始化计算。
    这个方法调用后,你计算出来的信息必须能够计算出collection view的content size.

    六、提供布局属性

    布局的最后一步,collection view会调用layoutAttributesForElementsInRect:方法,这个方法的目的就是提供指定区域内cells,supplementary,或者decoration view需要的属性。
    如果是一个很大的滚动区域,collection view可能只是需要可见区域的属性, 在图 5-2中, 需要就是6-20和第二个headerview的布局属性, 你必须准备好这些布局属性。这些属性可能用来做删除插入动画。

    5-2

    因为这个layoutAttributesForElementsInRect方法在prepareLayout之后调用,所以你应该已经有了绝大多数的信息取创建并返回需要的属性,实现layoutAttributesForElementsInRect方法需要以下几步:

    1. 遍历所有prepareLayout生成的数据,决定是访问缓存还是创建一个新的。
    2. 检查每个item的frame,确保在layoutAttributesForElementsInRect给的矩形区域内(可交叉)
    3. 对于每个符合步骤2条件的item,添加对应的UICollectionViewLayoutAttributes对象到一个数组
    4. 返回数组给collection view

    取决于你怎样管理你的布局信息,你可能会在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-3

    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:方法,是否位置需要改变。

    相关文章

      网友评论

      • 大冲哥:有Demo吗?
      • 0271fb6f797c:如果使用自定义布局的话,我在滚动时怎么获取页数呢?用 self.pageControl.currentPage = (int)(scrollView.contentOffset.x / scrollView.frame.size.width + 0.5) 一直有问题,请问有什么好的解决办法吗?
      • 健健锅:我想让cell 按照一定的轨迹滑动,实现过程中发现如果cell的frame尺寸足够大 没问题,但是如果尺寸偏小,当collectionview 滑动到最后时 所有的cell 会突然消失,然后往回滑动有一闪出现,contentsize 是没有问题的
        ParkinWu:cell 位置计算出错了吧, 留意一下,当 offset 超出 contentSize 的所计算 cell的位置
      • abbb38f69998:理解万岁
      • c116923a494a:楼主写得已经很好了,如果附加demo就更好了 :blush:
      • HelloEverything:谢谢楼主,译的很好,谢谢
      • 李大戮:楼主可以提供个demo吗?直接上码理解更快点

      本文标题:CollectionView自定义布局

      本文链接:https://www.haomeiwen.com/subject/hycchttx.html