CollectionView自定义布局

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