布局对象 (Layout Objects)
我们可以使用 flow layout 实现一个标准的 grid view,这可能是在 collection view 中最常见的使用案例了。尽管大多数人都这么想,但是 Apple 很聪明,没有明确的命名这个类为UICollectionViewGridLayout,而使用了更为通用的术语 flow layout,更好的描述了该类的功能:它通过一个接一个的放置 cell 来建立自己的布局,当需要的时候,插入横排或竖排的分栏符。通过自定义滚动方向,大小和 cell 之间的间距,flow layout 也可以在单行或单列中布局 cell。实际上,UITableView的布局可以想象成 flow layout 的一种特殊情况。
Cells 和其他 Views
为了适应任意布局,collection view 建立一个了类似、但比 table view 更灵活的视图层级(view hierarchy)。像往常一样,你的主要内容显示在 cell 中,cell 可以被任意分组到 section 中。Collection view 的 cell 必须是UICollectionViewCell的子类。除了 cell,collection view 额外管理着两种视图:supplementary views 和 decoration views。
collection view 中的Supplementary views相当于 table view 的 section header 和 footer views。像 cells 一样,他们的内容都由数据源对象驱动。然而和 table view 中用法不一样,supplementary view 并不一定会作为 header 或 footer view;他们的数量和放置的位置完全由布局控制。
Decoration views纯粹为一个装饰品。他们完全属于布局对象,并被布局对象管理,他们并不从 data source 获取的 contents。当布局对象指定需要一个 decoration view 的时候,collection view 会自动创建,并将布局对象提供的布局参数应用到上面去。并不需要为自定义视图准备任何内容。
自定义布局
作为一个非常有意义的自定义 collection view 布局的例子,我们不妨设想一个典型的日历应用程序中的周 (week) 视图。日历一次显示一周,星期中的每一天显示在列中。每一个日历事件将会在我们的 collection view 中以一个 cell 显示,位置和大小代表事件起始日期时间和持续时间。
一般有两种类型的 collection view 布局:
1.独立于内容的布局计算。这正是你所知道的像 UITableView 和 UICollectionViewFlowLayout 这些情况。每个 cell 的位置和外观不是基于其显示的内容,但所有 cell 的显示顺序是基于内容的顺序。可以把默认的 flow layout 做为例子。每个 cell 都基于前一个 cell 放置(或者如果没有足够的空间,则从下一行开始)。布局对象不必访问实际数据来计算布局。
2.基于内容的布局计算。我们的日历视图正是这样类型的例子。为了计算显示事件的起始和结束时间,布局对象需要直接访问 collection view 的数据源。在很多情况下,布局对象不仅需要取出当前可见 cell 的数据,还需要从所有记录中取出一些决定当前哪些 cell 可见的数据。
在我们的日历示例中,布局对象如果访问某一个矩形内 cells 的属性,那就必须迭代数据源提供的所有事件来决定哪些位于要求的时间窗口中。 与一些相对简单,数据源独立计算的 flow layout 比起来,这足够计算出 cell 在一个矩形内的 index paths 了(假设网格中所有cells的大小都一样)。
如果有一个依赖内容的布局,那就是暗示你需要写自定义的布局类了,同时不能使用自定义的UICollectionViewFlowLayout,所以这正是我们需要做的事情。
collectionViewContentSize
由于 collection view 对它的 content 并不知情,所以布局首先要提供的信息就是滚动区域大小,这样 collection view 才能正确的管理滚动。布局对象必须在此时计算它内容的总大小,包括 supplementary views 和 decoration views。注意,尽管大多数经典的 collection view 限制在一个轴方向上滚动(正如UICollectionViewFlowLayout一样),但这不是必须的。
为了清楚起见,我选择布局在一个非常简单的模型上:假定每周天数相同,每天时长相同,也就是说天数用 0-6 表示。在一个真实的日历程序中,布局将会为自己的计算大量使用基于NSCalendaar的日期。
layoutAttributesForElementsInRect:
这是任何布局类中最重要的方法了,同时可能也是最容易让人迷惑的方法。collection view 调用这个方法并传递一个自身坐标系统中的矩形过去。这个矩形代表了这个视图的可见矩形区域(也就是它的 bounds ),你需要准备好处理传给你的任何矩形。
布局属性对象 (layout attributes objects) 通过indexPath属性和他们对应的 cell,supplementary view 或者 decoration view 关联在一起。collection view 为所有 items 从布局对象中请求到布局属性后,它将会实例化所有视图,并将对应的属性应用到每个视图上去。
注意!这个方法涉及到所有类型的视图,也就是 cell,supplementary views 和 decoration views。一个幼稚的实现可能会选择忽略传入的矩形,并且为 collection view 中的所有视图返回布局属性。在原型设计和开发布局阶段,这是一个有效的方法。但是,这将对性能产生非常坏的影响,特别是可见 cell 远少于所有 cell 数量的时候,collection view 和布局对象将会为那些不可见的视图做额外不必要的工作。
你的实现需要做这几步:
1, 创建一个空的可变数组来存放所有的布局属性。
2, 确定 index paths 中哪些 cells 的 frame 完全或部分位于矩形中。这个计算需要你从 collection view 的数据源中取出你需要显示的数据。然后在循环中调用你实现的layoutAttributesForItemAtIndexPath:方法为每个 index path 创建并配置一个合适的布局属性对象,并将每个对象添加到数组中。
3, 如果你的布局包含 supplementary views,计算矩形内可见 supplementary view 的 index paths。在循环中调用你实现的layoutAttributesForSupplementaryViewOfKind:atIndexPath:,并且将这些对象加到数组中。通过为 kind 参数传递你选择的不同字符,你可以区分出不同种类的supplementary views(比如headers和footers)。当需要创建视图时,collection view 会将 kind 字符传回到你的数据源。记住 supplementary 和 decoration views 的数量和种类完全由布局控制。你不会受到 headers 和 footers 的限制。
4, 如果布局包含 decoration views,计算矩形内可见 decoration views 的 index paths。在循环中调用你实现的layoutAttributesForDecorationViewOfKind:atIndexPath:,并且将这些对象加到数组中。
5, 返回数组。
我们自定义的布局没有使用 decoration views,但是使用了两种 supplementary views(column headers和row headers):
layoutAttributesFor…IndexPath
如果你正在使用自动布局,你可能会感到惊讶,我们正在直接修改布局参数的 frame 属性,而不是和约束共事,但这正是 UICollectionViewLayout 的工作。尽管你可能使用自动布局来定义collection view 的 frame 和它内部每个 cell 的布局,但 cells 的 frames 还是需要通过老式的方法计算出来。
shouldInvalidateLayoutForBoundsChange:
最后,当 collection view 的 bounds 改变时,布局需要告诉 collection view 是否需要重新计算布局。我的猜想是:当 collection view 改变大小时,大多数布局会被作废,比如设备旋转的时候。因此,一个幼稚的实现可能只会简单的返回 YES。虽然实现功能很重要,但是 scroll view 的 bounds 在滚动时也会改变,这意味着你的布局每秒会被丢弃多次。根据计算的复杂性判断,这将会对性能产生很大的影响。
当 collection view 的宽度改变时,我们自定义的布局必须被丢弃,但这滚动并不会影响到布局。幸运的是,collection view 将它的新 bounds 传给shouldInvalidateLayoutForBoundsChange:方法。这样我们便能比较视图当前的bounds 和新的 bounds 来确定返回值:
动画
插入和删除
UITableView 中的 cell 自带了一套非常漂亮的插入和删除动画。但是当为 UICollectionView 增加和删除 cell 定义动画功能时,UIKit 工程师遇到这样一个问题:如果 collection view 的布局是完全可变的,那么预先定义好的动画就没办法和开发者自定义的布局很好的融合。他们提出了一个优雅的方法:当一个 cell (或者supplementary或者decoration view)被插入到 collection view 中时,collection view 不仅向其布局请求 cell 正常状态下的布局属性,同时还请求其初始的布局属性,比如,需要在开始有插入动画的 cell。collection view 会简单的创建一个 animation block,并在这个 block 中,将所有 cell 的属性从初始(initial)状态改变到常态(normal)。
通过提供不同的初始布局属性,你可以完全自定义插入动画。比如,设置初始的 alpha 为 0 将会产生一个淡入的动画。同时设置一个平移和缩放将会产生移动缩放的效果。
同样的原理应用到删除上,这次动画是从常态到一系列你设置的最终布局属性。这些都是你需要在布局类中为initial或final布局参数实现的方法.
initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingSupplementaryElementOfKind:atIndexPath:
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
布局间切换
可以通过类似的方式将一个 collection view 布局动态的切换到另外一个布局。当发送一个setCollectionViewLayout:animated:消息时,collection view 会为 cells 在新的布局中查询新的布局参数,然后动态的将每个 cell(通过index path在新旧布局中判断出相同的cell)从旧参数变换到新的布局参数。你不需要做任何事情。
结论
根据自定义 collection view 布局的复杂性,写一个通常很不容易。确切的说,本质上这和从头写一个完整的实现相同布局自定义视图类一样困难了。因为所涉及的计算需要确定哪些子视图当前是可见的,以及它们的位置。尽管如此,使用UICollectionView还是给你带来了一些很好的效果,比如 cell 重用,自动支持动画,更不要提整洁的独立布局,子视图管理,以及数据提供架构规定(data preparation its architecture prescribes.)。
每当我使用UICollectionView的时候,我被其简洁的设计所折服。对于一个有经验的 Apple 工程师,为了想出如此灵活的类,很可能需要首先考虑NSTableView和UITableView。
扩展阅读