自定义UICollectionViewLayout(二)Creating Custom Layouts翻译

官方文档:Creating Custom Layouts

在自定义布局之前,请先考虑你是否真的需要这样做。 UICollectionViewFlowLayout已经为你提供了大量、效率优化过的行为,您可以通过不同的方式来实现多类典型布局。只有如下情况,你才需要自定义布局:

  • 非网格或者线性布局——基础的断裂性布局 based breaking layout(各item排列成一行,直到这一行排满,会另起一行,如此重复直到所有item全部排列完毕)或者必须在不同方向上的滚动的布局。
  • 你需要频繁的改变所有cells的位置,且修改已有布局类的工作量大于自定义布局的工作量。

好消息是,基于API的观点,实现自定义布局并不困难。最难的部分在于计算布局中items的位置。一旦你直到了这些items的位置,那么为collection view提供布局信息将会轻而易举。

Subclassing UICollectionViewLayout (创建UICollectionViewLayout的子类)

UICollectionViewLayout为你的布局设计提供了入口,自定义布局需要继承它。UICollectionViewLayout要求您必须实现少量的几个方法,如此便可支撑布局类的核心行为。至于其它的方法,您可以根据需要进行重写来对布局行为稍作调整。这些核心方法处理了以下几个至关重要的任务:

  • 具体说明滚动区域的size。
  • 向cells和views提供组成布局的attribute objects,以便collection view可以把cells和views放置在正确的位置。

虽然你的自定义布局可以只实现核心方法,但实现几个可选的方法会使你的布局更有吸引力。

布局对象使用data source提供的信息来创建布局。你的布局对象如果需要与data source通信,可以调用属性 collectionView 中的相关方法,属性 collectionView 在任何布局方法中都是很容易获得的。在自定义布局的过程中,需要牢记哪些是collectionView可以知晓的,哪些是collectionView不知道的,原因在于布局进程启动后,collection view无法追踪布局或者视图的位置。因此,尽管布局对象没有限制你调用collectionView的任何方法,切莫依赖collectionView(除了必要的数据)来计算布局。

Understanding the Core Layout Process (理解核心布局过程)

collection view直接与你的自定义布局对象配合,从而管理整个布局过程。当collection view检查到自己需要布局信息时,会向你的布局对象发起请求。比如,当collection view第一次显示或者大小发生改变时,就会向布局对象发起请求。你也可以调用布局对象的 invalidateLayout 方法,明确地告知collection view去更新布局。invalidateLayout 方法会放弃原有的布局信息,并强制布局对象生成新的布局信息。

注:请注意不要混淆布局对象的 invalidateLayout 方法与collection view的 reloadData 方法。调用 invalidateLayout 方法不一定导致collection view放弃现有的cells或子视图,而是当collection view移动、添加或删除item时,如果有必要,强制布局对象重新计算所有的布局属性。如果数据源中的数据发生改变,那么调用 reloadData 方法是最合适的。invalidateLayout也好,reloadData也罢,不管你如何启动布局更新,实际的布局过程都是一样的。

在布局过程中,collection view会调用你的布局对象中的特定方法,这些特定方法为你提供了计算items位置的机会,并提供给collection view它所需要的主要信息。另外还有一些方法(preceding methods前置方法)也会被调用,但这些方法在布局过程中总是按照如下顺序被调用:

1
2
3
prepareLayout
collectionViewContentSize
layoutAttributesForElementsInRect:
  1. 使用prepareLayout方法执行提供布局信息之前所需要的计算
  2. 使用collectionViewContentSize方法返回初始计算的整个内容区域的总体尺寸
  3. 使用layoutAttributesForElementsInRect:方法返回指定的矩形内的cells或其它视图的属性

下图描述了你如何使用preceding methods来生成布局信息:

Figure 5-1  Laying out your custom content

你的布局对象为了确定cells或其它视图的位置所需要的任何计算都可以在 prepareLayout方法中执行。至少,你应该计算出内容区域的整体尺寸,以便在第二步返回这个尺寸。

collection view根据这个尺寸配置其滚动视图,举例来说,如果你计算出的内容区域尺寸在横向和纵向上都超出了屏幕边界,那么滚动视图就会调整其滚动策略,使之横向纵向都可以滚动。不像UICollectionViewFlowLayout那样,默认情况下不会调整自己内容区域的布局,而只能在一个方向上滚动。

在当前滚动位置的基础上,collection view会调用layoutAttributesForElementsInRect:方法获取指定的矩形内的cells或其它视图的属性。这个矩形和可见区域相同也可能不同。当返回该信息之后,核心布局过程到此结束。

核心布局过程结束之后,cells或其它视图的属性一直保持不变,直到collection view使其布局失效。调用布局对象的invalidateLayout方法,将会使布局过程再次执行,将会重新开始调用prepareLayout方法。collection view在滚动过程中也会自动使布局失效。当用户滚动内容时,collection view会调用shouldInvalidateLayoutForBoundsChange:方法,当该方法返回YES时,collection view会使布局失效。

注: 请牢记:调用invalidateLayout方法不会立即触发布局更新过程,事实上该方法仅仅标记出“布局与数据不一致,需要更新”,在下一次的视图更新周期中(view update cycle),collection view 会检查到其布局对象是否已经成为脏数据,如果是则更新布局对象。事实上,你可以连续调用该方法多次,但并不是每次都会触发布局立即更新。

Creating Layout Attributes(创建布局属性)

布局属性对象(attributes objects)是UICollectionViewLayoutAttributes类的实例。这些实例可以用多种方式创建。当你的应用程序并不需要处理上千个items时,在准备布局时,创建这些实例是有意义的,因为布局信息可以被缓存和引用,而不是持续在计算。如果计算所有属性的开销远大于缓存的开销,那么就创建属性吧。 (这段翻译的很不好,还是看原文吧: When your app is not dealing with thousands of items, it makes sense to create these instances while preparing the layout, because the layout information can be cached and referenced rather than computed on the fly.If the costs of computing all the attributes up front outweighs the benefits of caching in your app, it is just as easy to create attributes in the moment when they are requested)

创建UICollectionViewLayoutAttributes类的实例,可以用如下类方法:

1
2
3
layoutAttributesForCellWithIndexPath:
layoutAttributesForSupplementaryViewOfKind:withIndexPath:
layoutAttributesForDecorationViewOfKind:withIndexPath:

你需要根据不同的视图类型选择相应的类方法, 因为collection view会根据不同的属性实例去请求数据源中对应的视图信息。使用不正确的类方法会导致collection view在错误的地方创建错误的视图,从而和你期望的布局不符。

创建完属性对象之后,设置相应视图的相关属性。至少,需要设置视图的位置和尺寸。倘若你的布局中,视图有重叠的部分,那么你可以指定一个值给zIndex属性,以确保这些视图按照一致的顺序重叠。其它一些属性,例如可以让你控制cells或views的可见性和外观,可以根据需要进行设置。如果标准的attributes class不能满足你的需求,你可以创建其子类,扩展其存储内容。创建layout attributes子类时,必须实现 isEqual: 方法来比较两个layout attributes子类是否相同, 因为collection view 将会用到该方法。

更多内容请参考UICollectionViewLayoutAttributes Class Reference.

Preparing the Layout (准备布局)

在布局周期的开始阶段,布局对象在启动布局过程之前,会先调用prepareLayout方法。该方法为你提供了预先计算的机会,在该方法中不需要实现自定义布局,而是根据需要仅做初始信息的计算。该方法调用之后,你的布局对象必须要有足够的信息可以计算出内容区域的整体尺寸,以便下一过程使用。初始信息,至少是可以计算出内容区域的整体尺寸,至多则是存储所有的layout attributes。使用prepareLayout方法,是为你的应用程序提供基础信息,是为了进行有意义的前置计算,如果想看到prepareLayout方法是什么样子的,请参考see Preparing the Layout.

Providing Layout Attributes for Items in a Given Rectangle (提供指定矩形区域内的布局属性)

在布局周期的最后阶段,collection view会调用布局对象的 layoutAttributesForElementsInRect:方法,该方法的目的是为所有在指定区域内的cells、supplementary views、decoration views 提供layout attributes。对于一个比较大的可滚动区域来说,collection view只会请求可见部分的cells的属性。如下图所示:collection view只会要求布局对象提供6-20的cell及header2的layout attributes。你必须能够提供随意一段内容区域的layout attributes,这些属性可能被插入或删除items的动画使用。

Figure 5-2  Laying out only the visible views

由于layoutAttributesForElementsInRect:方法是在prepareLayout方法之后才会调用,因此你应该拥有了足够多的信息来返回或创建所需属性。实现layoutAttributesForElementsInRect:方法遵从以下步骤:

  1. 遍历prepareLayout方法生成的数据,根据这些数据生成属性,这些属性要么来自缓存,要么创建新的。
  2. 检查每个item的frame是否与layoutAttributesForElementsInRect:方法传递的rect相交。
  3. 对每个item,添加相应的UICollectionViewLayoutAttributes对象数组。
  4. 返回这个layout attributes数组给collection view。

你可以在prepareLayout方法中创建UICollectionViewLayoutAttributes对象,也可以在之后的layoutAttributesForElementsInRect:方法中创建,这取决于你如何管理你的布局信息。在实现你的程序时,要牢记缓存布局信息的好处。重复计算cell的layout attributes是一个相当耗费性能的操作,会极大的影响应用程序的性能。也就是说,当你的collection view管理的item数量庞大时,请求布局对象时将会耗费更多的性能来创建这些layout attributes。

布局对象还需要有能力提供layout attributes 应对单个item的特殊需求。collection view 可能在常规布局过程之外,包括创建动画,都需要这些特殊信息。更多信息请参考Providing Layout Attributes On Demand

如果你希望看到实现layoutAttributesForElementsInRect:方法具体的例子,请参考Providing Layout Attributes

Providing Layout Attributes On Demand (根据具体需求提供布局属性)

collection view 在常规布局过程之外,会定期请求你的布局对象, 要求布局对象提供个别item的属性。举例来说,当为某个item配置插入或删除动画时,collection view会请求这个item的属性。你的布局对象必须准备好可以提供每一个cell、supplementary view、decoration view的layout attributes,你可以通过重写下面几个方法来实现:

1
2
3
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
layoutAttributesForDecorationViewOfKind:atIndexPath:

在你的方法实现中,必须取回当前的layout attributes。每一个自定义布局对象都应该实现layoutattributesforitematindexpath:方法。如果你的布局对象不包含supplementary views,那么无需实现layoutAttributesForSupplementaryViewOfKind:atIndexPath:方法。同样,如果你的布局对象不包含decoration views,那么无需实现layoutAttributesForDecorationViewOfKind:atIndexPath:方法。在返回属性的过程中,你不能更新layout attributes。如果你需要更新布局信息,使布局对象失效,并在下一个布局周期中更新布局对象的数据。

Connecting Your Custom Layout for Use (使用你的自定义布局)

有两种方式让你的自定义布局和collection view相关联:纯代码的方式和storyboards的方式。collection view 链接布局可以通过它的一个可写属性 - collectionViewLayout。如果要把collection view的布局设置成你的自定义布局,只需创建自定义布局示例,赋值给collection view的collectionViewLayout属性即可。

1
self.collectionView.collectionViewLayout = [[MyCustomLayout alloc] init];

对于storyboards,打开‘Document Outline’面板,选择你的collection view(它在您的控制器的下拉菜单中)。选中collection view之后,在‘Utilities’面板中,打开‘Attributes inspector’(译者注:即右边面板顶部的第4个按钮),在‘Collection View’标签下面,找到‘Layout’,把它的选项由‘Flow’改为‘Custom’,这时你会发现下面的‘Scroll Direction’ 变成了 ‘Class’,这时你可以选择自定义布局Class了。

Making Your Custom Layouts More Engaging (使你的自定义布局更有吸引力)

在布局过程中,为每一个cell或者view提供layout attributes是硬性要求,但还有一些其它的行为可以增加用户体验。实现这些行为是可选的,但是还是推荐您实现。

Elevating Content Through Supplementary Views (通过补充视图来提升内容)

supplementary views是和cells相分离的,拥有它们自己的一套layout attributes。和cells类似,supplementary views由data source object提供,但它的目的是为了丰富应用程序的内容。例如,UICollectionViewFlowLayout使用supplementary views作为section的headers和footers。而另一个应用程序可能使用supplementary views给每个cell创建自己的文本标签,以显示该cell的信息。和cells类似,supplementary views也用了重用机制来优化性能。因此,你在应用程序中用到的所有supplementary views都应该是UICollectionReusableView的子类。

在你的布局中添加supplementary view的步骤如下:

  1. 使用 registerClass:forSupplementaryViewOfKind:withReuseIdentifier: 或者 registerNib:forSupplementaryViewOfKind:withReuseIdentifier: 方法在你collection view的布局对象中注册supplementary view。
  2. 在你的data source中,实现collectionView:viewForSupplementaryElementOfKind:atIndexPath:方法。因为supplementary views都是可以复用的,因此应该使用dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:来获取可复用队列中的supplementary views,当然,你也可以创建新的可复用supplementary view,在使用之前,为它必要的数据赋值。
  3. 为你的supplementary views创建layout attributes,创建方式类似上述cells的layout attributes创建方式。
  4. 把这些layout attributes添加进数组,并在layoutAttributesForElementsInRect:方法中返回。
  5. 实现layoutAttributesForSupplementaryViewOfKind:atIndexPath:方法,返回特定supplementary view的layout attributes。

为supplementary views创建layout attributes的过程和为cells创建layout attributes的过程很相似,但也有所不同:自定义布局中可以有多种类型的supplementary views,却只能有一种类型的cells(译者注:???)。这是因为supplementary views的目的在于丰富主应用的内容,所以它和主应用不是一体的。有多重途径可以丰富主应用的内容,因此每一个supplementary view的方法中都指明了具体要处理哪个视图以免混淆,同时允许布局对象通过类型计算相应视图的属性(There are many ways in which an app’s content can be supplemented, and so each of the supplementary view’s methods specifies which kind of view is being addressed to distinguish it from the others and allow your layout to compute its attributes correctly based on its type)。当你注册了一个supplementary view之后,你提供的那个字符串就是布局对象用于区分不同视图的凭证。如果想把supplementary views引入你的工程,可以参考Incorporating Supplementary Views.

Including Decoration Views in Your Custom Layouts (在你的布局中添加装饰视图)

Decoration views是用来丰富主应用的装饰性视图。与cells和supplementary views不同,decoration views并不依赖data source。你可以使用它来自定义背景,填充cells四周区域,甚至若果你愿意,可以让cells显得模糊不清。decoration views在布局对象中被单独定义,单独管理,完全不与data source交互。

在你的布局中添加decoration views,请做到以下几点:

  1. 使用registerClass:forDecorationViewOfKind:registerNib:forDecorationViewOfKind:方法,在布局对象中注册decoration view。尽管和注册cells或supplementary views的方式很相似,但请记住注册decoration view的方法出现在布局对象中,而注册cells 或supplementary views的方法出现在data source中。
  2. 在你布局对象中的layoutAttributesForElementsInRect: 方法里,创建supplementary views的属性,创建方式类似于cells 和supplementary views。
  3. 在你布局对象中实现 layoutAttributesForDecorationViewOfKind:atIndexPath:方法,返回指定supplementary view的属性。
  4. 选择性的实现initiallayoutattributesforappearingdecorationelementofkind:atindexpath:finallayoutattributesfordisappearingdecorationelementofkind:atindexpath:方法来处理你的decoration views出现和消失动画。

创建decoration view的过程不同于cells或supplementary views:创建过程中你唯一需要做的事就是注册Class或nib,这样就可以确保在使用decoration view时它们已经被创建了。由于decoration view纯粹是为了视觉需要,因此在decoration view的nib文件或者initWithFrame:方法之外,不允许配置任何其他内容。为此,当需要一个decoration view时,是由collection view使用布局对象提供的属性为您创建的。任何decoration views仍旧应该是UICollectionReusableView的子类,因为布局对象对decoration views也运用了重用机制。

注:在decoration views创建属性的时候,别忘记考虑zIndex属性,你可以使用zIndex属性把decoration views放在视图层级的最后面,也可以放在最前面遮挡住cells 和supplementary views。

Making Insertion and Deletion Animations More Interesting (使你的添加删除动画更加有趣)

插入、删除cells和views给布局提出了一个有趣的挑战。插入一个cell会使其它cells和views的布局发生改变。尽管布局对象知道如何把当前cells从现有位置移动到新位置,但它并不知道插入那个cell的当前位置。collection view会请求布局对象提供一组初始属性用于动画,而不是无动画的插入一个新的cell。类似地,当一个cell被删除时,collection view会请求布局对象提供一组用于表示动画终点的属性。

为了便于理解初始属性是如何工作的,请看下图:’starting layout’显示了一个collection view最开始只有3个cells,当有新的cell插入后,collection view请求布局对象提供新cell的初始属性。在这个示例中,布局对象把这个新cell的初始位置设置为collection view的中心点,并将cell的alpha值设置为0来隐藏它。在动画过程中,这个cell逐渐显示并从中心位置逐渐移动到右下角。

Figure 5-3  Specifying the initial attributes for an item appearing onscreen

‘Listing 5-2’列出了如何给这个新cell添加初始属性。该方法中,设置了cell的初始位置为collection view的中心点,并将cell置为透明。布局对象在接下来应该提供cell终止位置和透明度,以便应用于普通的布局过程。

Listing 5-2 Specifying the initial attributes for an inserted cell

1
2
3
4
5
6
7
8
- (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;
}

注:‘Listing 5-2’将会导致有cell插入时,全部cell都会移动。这样一来之前的3个cell也会从中心位置弹出来。如果只想移动新插入的cell,请检查插入的cell的index path,是否与prepareForCollectionViewUpdates:参数中的某个item的index path相等。如果相等,则会只移动相匹配的item;如果没有相等的,则调用initialLayoutAttributesForAppearingItemAtIndexPath:super方法,并返回。(To animate only the cell being inserted, check to see if the index path of the item matches the index path of an item passed to the prepareForCollectionViewUpdates: method and only perform the animation if a match is found.Otherwise, return the attributes returned by calling the super method of initialLayoutAttributesForAppearingItemAtIndexPath:.)。

删除操作的处理方式和插入操作是相同的,只不过把初始属性换成了最终属性。那上面的例子来说,如果你删除操作和插入操作使用相同的属性,则删除cell会导致cell从右下角移动到中心位置,并逐渐隐藏。UICollectionViewLayout提供了6个方法,items、supplementary views、decoration views各持有两个(针对初始属性和终止属性)。

Improving the Scrolling Experience of Your Layout (改善您的布局的滚动体验)

您的自定义布局对象会影响collection view的滚动行为,好的滚动行为将会增加用户体验。当滚动相关的touch事件结束,scroll view根据当前的速度和减速率决定了滚动内容的最终停止位置。collection view知道这个最终停止位置之后,通过调用布局对象的targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法,来询问布局对象是否应该修改这个位置。由于调用该方法时,内容仍在滚动中,因此你的布局对象可以决定内容最终停止位置。

下图示范了如何使用自定义布局来改变collection view的滚动行为。假设 collection view的offse为(0, 0) ,用户往左划动,collection view能够计算出内容滚动到何处可以自然的停止,并把这个值作为内容区域的“提议”offset value。你的自定义布局对象可以修改这个“提议”值,当滚动停止时,某个你想重点呈现的item可以刚好处于collection view可见位置的中心位置。同时,这个值将会作为内容区域的offset value,并在targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法中返回。

Figure 5-4  Changing the proposed content offset to a more appropriate value

Tips for Implementing Your Custom Layouts (实现自定义布局的小提示)

有几点提示和建议如下:

  1. prepareLayout方法中创建和存储UICollectionViewLayoutAttributes对象,collection view会在特定时间询问布局对象,所以在某些情况下,事先创建和存储它们是有意义的。尤其是你有较少的item(几百条),或者是item并不经常改变的情况下,这么做很有必要。

    但是,如果你要管理几千个item,那么你需要权衡缓存和重新计算的利弊。对于那些尺寸可变的item,假若他们的布局不常改变,缓存通常不需要定期重复计算它们复杂的布局信息。对于那些大量的固定大小的item,在有需要时它们可能仅仅只做是简单的计算即可。而对于那些属性变换频繁的item,你可能总要重复计算,缓存可是占用内存的额外空间而已,没有意义。

  2. 避免使用UICollectionView的子类。collection view没有自己的外观,所有视图均来自data source ,所有与布局相关的信息均来自布局对象。如果你想要把一个item放入三维空间,正确的做法是实现一个自定义布局,设置每cell和视图的3D transform。
  3. 在你的自定义布局类的layoutAttributesForElementsInRect:方法中,千万不要调用UICollectionViewvisibleCells方法。collection view并不知道items的位置,这些信息都是布局对象告知它的。因此visible cells的请求会再次转发到你的布局对象中。

    你的布局对象,必须始终知晓所有items的位置,并可以随时返回这些item的属性。大多数情况下,布局对象应该独立完整这些任务。但少数情况下,布局对象可能需要data source中的信息来确定items的位置。举例来说,在地图上显示item的布局,可能会从data source中检索每个item的地图位置。