文档翻译-Collection View - 1.5

创建自定义布局

在开始创建自定义布局之前,考虑清楚是否有这个必要。UICollectionViewFlowLayout 类提供了大量已经对效率进行优化了的行为,并且可以通过多种方式进行调整,来实现许多不同类型的标准布局。唯一需要考虑实现自定义布局是在以下情况下:

  • 你想要的布局看前来不像网格或基于线性中断布局(布局中items被放置到一行,直到这一行布满了,然后继续布到下一行,直到所有的items被放置),或者需要不止在一个方向上滚动。
  • 你想要频繁地改变所有单元格的位置,而修改已经存在的流式布局比创建自定义布局要做更多的工作。

好消息是,从API的角度来看,实现自定义布局并不难。最困难的部分是执行必要的计算来确定布局中items的位置。当你知道了这些items的位置,给集合视图提供那些信息是很简单的。

子类化UICollectionViewLayout

对于自定义布局,你想要子类化UICollectionViewLayout,这为你的设计提供了一个新的起点。只有少数方法为你的布局对象提供了核心的行为,并且在你的实现中是必须的。其余的方法可以根据需要来重写以调整布局行为。核心方法处理以下关键任务:

  • 指定可滚动内容区域的大小。
  • 为构成布局的单元格和视图提供属性对象,以便集合视图可以定位每个单元格和视图。

虽让你可以创建一个仅实现核心方法的功能性布局对象,但如果你也实现了几种可选方法,布局可能会更有吸引力。
布局对象使用它的数据源提供的信息来创建集合视图的布局。你的布局通过调用collectionView属性的方法来与数据源进行通信,该方法可以在所有布局的方法中访问。记住你的集合视图在布局过程中知道和不知道的内容。由于布局过程正在进行,集合视图无法跟踪视图的布局或位置。因此,即使布局对象不会限制你调用集合视图的任何方法,也不要依赖于集合视图来计算布局所需的数据以外的其他内容。

理解核心布局过程

集合视图直接与你的自定义布局对象一起工作来管理整个布局过程。当集合视图确定它需要布局信息时,它会让你的布局对象来提供。例如,集合视图会在首次显示或调整大小时询问布局信息。你也可以通过调用布局对象的invalidateLayout方法告诉集合视图来显式地更新它的布局。该方法扔掉已经存在的布局信息,并强制布局对象生成新的布局信息。

注意:不要将布局对象的invalidateLayout方法和集合视图的reloadData方法混淆。调用invalidateLayout方法不一定会导致集合视图抛出其现有的单元格和子视图。而是,它强制布局对象来重新计算其当移动和添加或删除items时所需的所有布局属性。如果数据源中的数据已更改,则reloadData方法是合适的。无论你如何启动布局更新,实际的布局过程是一样的。

在布局过程中,集合视图会调用你的布局对象的特定方法。这些方法是你计算items位置的机会,并为集合视图提供所需的主要信息。其他方法也可能被调用,但是在布局过程中总是按照以下顺序调用这些方法:

  1. 使用prepareLayout方法来执行提供布局信息所需的前期计算。
  2. 使用collectionViewContentSize方法来根据初始计算返回整个内容区域的总体大小。
  3. 使用layoutAttributesForElementsInRect:方法来返回指定矩形中的单元格和视图的属性。

图5-1 说明如何使用上述方法生成布局信息

图5-1

prepareLayout方法是你执行任何计算来确定在布局中单元格和视图的位置的机会。至少,你应该在这个方法中计算足够的信息,以便返回内容区域的整体大小,在步骤2中以返回给集合视图。
集合视图使用内容的大小来适当地配置它的滚动视图。例如,如果计算的内容在纵向和横向上超过当前设备屏幕的边界,则滚动视图将进行调整,以便同时在两个方向上滚动。不像UICollectionViewFlowLayout, 默认不调整布局的内容,只在一个方向上滚动。
基于当前滚动位置,然后集合视图调用你的layoutAttributesForElementsInRect:方法来询问在特定矩形内的单元格和视图的属性,这可能或不可能与可见矩形相同。返回那些信息之后,核心布局过程就有效地完成了。
在布局完成之后,你的单元格和视图的属性会保持不变,直到你或集合视图无效化布局。调用布局对象的nvalidateLayout方法会导致布局过程重新开始,才从prepareLayout方法的新调用开始。集合视图在滚动期间能够自动地无效你的布局。如果用户滚动其内容,集合视图会调用布局对象的shouldInvalidateLayoutForBoundsChange:方法,如果该方法返回YES,则会使布局无效。

注意:记住调用invalidateLayout方法不会立即开始布局更新过程是有用的。该方法仅将布局标记为与数据不一致并需要更新。在下一个视图更新周期中,集合视图会检查其布局是否不一致,如果是则更新它。事实上,你可以快速连续地调用invalidateLayout方法,而不会每次触发立即布局更新。

创建布局属性

你的布局负责的属性对象是UICollectionViewLayoutAttributes类的实例。在你的应用程序中这些实例可以被多种不同的方法来创建。当你的应用程序不处理数千个items时,在准备布局时才创建这些实例是有意义的,因为布局信息可以缓存和引用,而不是即时计算。如果计算所有属性的成本高于应用程序缓存的好处,则在请求时创建属性一样容易。
无论如何,创建UICollectionViewLayoutAttributes类的新实例时,请使用以下类方法之一:

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

你必须根据正在显示的视图类型使用正确的类方法,因为集合视图使用该信息从数据源对象请求恰当类型的视图。使用错误的方法会导致集合视图在错误的地方创建错误的视图,并且你的布局不会按预期显示。
创建每个属性对象之后,为相应视图设置相关属性。至少,设置视图在布局中的大小和位置。在布局视图重叠的情况下,为zIndex属性分配一个值,以确保重叠视图的顺序一致。其他属性让你控制单元格或视图的可见性或外观,并可以根据需要进行更改。如果标准的属性类不适合你的应用程序需要,你可以子类化并且扩展它以储存关于每个视图的其他信息。在子类化布局属性时,为了比较你的自定义属性,必须实现isEqual:方法,因为集合视图对其某些操作会使用此方法。
关于布局属性的更多信息,请看UICollectionViewLayoutAttributes Class Reference

准备布局

在布局周期开始时,布局对象会在布局过程开始前调用prepareLayout方法。该方法给你一个机会来计算稍后通知你的布局的信息。实现自定义布局不是必须使用prepareLayout方法,但是如果需要,可以作为进行初始计算的机会。在该方法调用之后,你的布局必须具有足够的信息来计算集合视图内容的大小,即布局过程的下一步。然而,该信息可以从这个最低要求到创建和存储布局将使用的所有布局属性对象。使用prepareLayout方法需要你的应用程序的基础架构,以及有意义于计算前面和计算要求。关于prepareLayout方法的示例,请看Preparing the Layout.

为给定矩形中的items提供布局属性

在布局过程的最后一步时,集合视图调用你的布局对象的layoutAttributesForElementsInRect:方法。该方法的目的是为与指定矩形相交的每个单元格和每个补充视图或装饰视图提供布局属性。对于一个大的滚动内容区域,集合视图可能只是询问当前可见的内容区域部分中的items的属性。在图5-2中,你的布局对象需要创建属性对象为当前可见内容是单元格6到20以及第二个标题视图。你必须准备为集合视图的内容区域的任何部分提供布局属性。这些属性可能用于促进插入或删除items的动画。

图5-2

因为layoutAttributesForElementsInRect:方法是在你的布局对象的prepareLayout方法之后调用, 你应该已经拥有大部分信息,以返回或创建必需的属性。你的layoutAttributesForElementsInRect:方法的执行遵循以下步骤:

  1. 迭代由prepareLayout方法生成的数据,以访问缓存的属性或创建新的属性。
  2. 检查每个itemframe, 看看它是否与传递给layoutAttributesForElementsInRect:方法的矩形相交。
  3. 对于每个相交的item,将相应的UICollectionViewLayoutAttributes对象添加到数组。
  4. 返回布局属性数组给集合视图

根据你如何管理布局信息,你可以在prepareLayout方法中创建UICollectionViewLayoutAttributes对象,或者在layoutAttributesForElementsInRect方法中执行此操作。在形成符合应用程序需求的实现时,请牢记缓存布局信息的好处。为单元格重复计算新的布局属性是一项昂贵的操作,可能会对应用程序的性能造成明显的不利影响。也就是说,当你的集合视图管理的items数目很大时,在请求时创建布局属性可能会更有意义(针对性能)。这只是一个问题,弄清楚哪个策略对你的应用程序来说是最有意义的。

注意:布局对象还需要能够根据个别items提供布局属性。集合视图可能出于一些原因请求常规布局过程之外的信息,包括创建适当的动画。有关按需提供布局属性的详细信息,请看Providing Layout Attributes On Demand

对于如何实现layoutAttributesForElementsInRect:的具体示例,请看Providing Layout Attributes.

按需提供布局属性

集合视图定期询问你的布局对象为正式布局过程之外的各个items提供属性。例如,集合视图当为一个item配置插入和删除动画时询问这些信息。你的布局对象必须准备为每个单元格、补充视图和其支持的装饰视图提供布局属性。你通过重写下列方法来做这些:

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

你的这些方法的实现应该为给定的单元格或视图获取当前的布局属性。每个自定义布局对象都被预期实现layoutAttributesForItemAtIndexPath:方法。如果你的布局不包含任何补充视图,你不需要重写layoutAttributesForSupplementaryViewOfKind:atIndexPath:方法。相同的,如果你的布局不包含装饰视图,不需要重写layoutAttributesForDecorationViewOfKind:atIndexPath:方法。当返回属性时,你不应该更新布局属性。如果你需要改变布局信息,无效化布局对象,并让它在随后的布局周期中更新数据。

连接自定义布局供使用

有两种方式来关联自定义布局到集合视图:以编程方式或通过故事板。集合视图通过一个可写的属性collectionViewLayout来关联它的布局。要将布局设置为自定义实现,请将集合视图的布局属性设置为自定义布局对象的实例。清单5-1显示所需的代码行。
清单5-1 关联自定义布局

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

否则,从你的故事板,打开“文档大纲”面板并选择你的集合视图(它列在控制器的下拉菜单中)。选择集合视图后,在“实用程序”窗口打开“属性”检查器,并在标签有集合视部分的下方将“布局”选项从“流式”改成“自定义”。它下面的选项从滚动方向更改为类,现在可以选择自定义布局类。

使你的自定义布局更具吸引力

在布局过程中为每个单元格和视图提供布局属性是必需的,但是用你的自定义布局有其他的行为可以提高用户体验。实现这些行为是可选的但推荐使用。

通过补充视图提高内容

补充视图是与集合视图的单元格分离开来的,并有它自己的布局属性集合。和单元格一样,这些视图被数据源对象提供,但是它们的目的是来增强应用程序的主要内容。例如,UICollectionViewFlowLayout为分区头和分区尾来使用补充视图。另一个应用程序可以使用补充视图来给每个单元格自定的文本标签来显示关于该单元格的信息。和集合视图单元格一样,补充视图有回收机制,以优化集合视图所使用的资源数量。因此,在你的应用程序中使用的所有补充视图,都应该继承自UICollectionReusableView类。
将补充视图添加到布局的步骤如下:

  1. 使用registerClass:forSupplementaryViewOfKind:withReuseIdentifier:registerNib:forSupplementaryViewOfKind:withReuseIdentifier:方法注册补充视图到集合视图的布局对象
  2. 在你的数据源中,实现collectionView:viewForSupplementaryElementOfKind:atIndexPath:方法。因为这些视图是重用的,调用dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:来获取,或者创建一个新的重用视图,并在返回前设置必要的数据。
  3. 为你的补充视图创建布局属性,就像为单元格做这些一样。
  4. 通过layoutAttributesForElementsInRect:方法返回一个数组,该数组包含这些布局属性对象。
  5. 实现layoutAttributesForSupplementaryViewOfKind:atIndexPath:方法为特定补充视图每当查询时返回属性对象。

在自定义布局中为补充视图创建属性对象的过程是几乎与单元格的过程相同,不同之处在于自定义布局可以有多种类型的补充视图,而单元格的类型限制为一种。这是因为补充视图旨在增强主要内容,因此与它分离。有许多方法可以补充应用程序的内容,因此每个补充视图的方法都会指定要处理哪种视图以区分其他视图,并允许布局根据其类型正确计算其属性。当注册一个补充视图来使用时,你提供使用的字符串通过布局对象来区分来自于其他的视图。有关将补充视图合并到自定义布局中的示例,请看Incorporating Supplementary Views

在自定义布局中包含装饰视图

装饰视图是增强集合视图布局外观的视觉装饰。不像单元格和补充视图,装饰视图仅提供可视内容,因此独立于数据源。你可以使用它们来提供自定义背景,填充单元格之间的空间,或者甚至模糊你想要的单元格。装饰视图仅由布局对象定义和管理,不与集合视图的数据源对象交互。
要将装饰视图添加到布局中,请执行以下操作:

  1. 使用registerClass:forDecorationViewOfKind:registerNib:forDecorationViewOfKind:方法使用布局对象注册你的装饰视图。虽然这个过程有点像注册单元格和补充视图,但记住注册装饰视图是发生在布局对象上,而与数据源无关。
  2. 在你的布局对象的layoutAttributesForElementsInRect:方法中,为装饰视图创建属性,就像为单元格和补充视图那样做一样。
  3. 在你的布局对象中实现layoutAttributesForDecorationViewOfKind:atIndexPath:方法,并当询问时返回装饰视图的属性。
  4. 可选的,实现initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:方法来处理动画为装饰视图的出现和消失。关于更多信息,请看Making Insertion and Deletion Animations More Interesting

创建装饰视图的过程与单元格和补充视图的过程不同。注册类或nib文件是需要做的,以确保在需要的时候创建装饰视图。因为它们纯粹是视觉的,装饰视图不需要超出在提供的nib文件或对象的initWithFrame:方法中已经完成的任何配置。因此,当需要装饰视图时,集合视图为你创建并应用布局对象提供的属性。任何装饰视图都应该继承自UICollectionReusableView,因为布局对象使用其装饰视图的回收机制。

注意:当为你的装饰视图创建属性时,不要忘记考虑zIndex属性。你可以使用zIndex属性将显示的单元格和补充视图背后(或者,如果你愿意的话)层叠你的装饰视图。

使插入和删除动画更有趣

插入和删除单元格和视图在布局过程中构成了一个有趣的挑战。插入单元格可能会导致其他单元格和视图的布局更改。即使布局对象知道如何将现有单元格和视图从当前位置设置为新位置,但它没有插入单元格的当前位置。代替插入一个新的单元格没有动画效果,集合视图会询问布局对象来提供一组用于动画的初始属性。相似的,当单元格被删除时,集合视图会询问布局对象来提供用于任何动画的端点的一组最终属性。
为了理解初始属性的工作原理,可以看一个示例。起始布局(图5-3)显示了最初只包含三个单元格的集合视图。当一个新的单元格被插入时,集合视图会询问布局对象来提供初始属性为被插入的单元格。在这种情况下,布局对象会将单元格的起始位置设置在集合视图的中心,并设置它的透明度值为0来隐藏它。在动画期间,这个新单元格会淡出并从集合视图的中心移动到右下角的最终位置。

图5-3

清单5-2显示了如图5-3所示可以用来指定插入单元格的初始属性的代码。此方法将单元格的位置设置为集合视图的中心,并使其透明。然后,布局对象将作为常规布局过程的一部分,为单元格提供最终位置和alpha值。
清单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;
}

注意:列表5-2将在插入一个单元格时对所有单元格进行动画处理,因此在插入之前已经存在的三个单元格也将从集合视图的中心弹出。要仅动画所插入的单元格,请检查item的索引路径是否与传递给prepareForCollectionViewUpdates:方法的item的索引路径匹配,并且仅在找到匹配项时才执行动画。否则,返回通过调用initialLayoutAttributesForAppearingItemAtIndexPathsuper方法返回的属性。

处理删除的过程与插入过程相同,只是您指定最终属性而不是初始属性。从上一个示例中,如果您使用与插入单元格时使用的相同的属性,则删除单元格将导致它在移动到集合视图的中心时淡出。在UICollectionViewLayout类中有六种方法可用于item,补充视图和装饰视图的两个独立方法(初始和最终属性)。

改善布局的滚动体验

自定义布局对象可以影响集合视图的滚动行为,以创建更好的用户体验。当滚动相关触摸事件结束时,滚动视图根据当前的实际速度和减速率确定滚动内容的最终静止位置。当集合视图知道该位置时,如果位置应该被改变它会询问布局对象通过调用它的targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法。因为它在基础内容仍然移动时调用此方法,自定义布局可能会影响滚动内容的最终位置。
图5-4演示了如何使用布局对象来更改集合视图的滚动行为。假设集合视图偏移从(0,0)开始,用户向左滑动。集合视图计算滚动自然停止的位置,并将该值提供为“建议”内容偏移值。您的布局对象可能会更改建议值,以确保在滚动停止时,item将精确集中在集合视图的可见边界。这个新值将成为目标内容偏移,并且时从targetContentOffsetForProposedContentOffset:withScrollingVelocity:返回。

图5-4

实现自定义布局的提示

以下是实现自定义布局对象的一些提示和建议:

  1. 考虑使用prepareLayout方法来创建和存储以后需要的UICollectionViewLayoutAttributes对象。集合视图将在某个时候询问布局属性对象,因此在某些情况下,可以在前面创建和存储它们。如果您的items数量相对较少(几百个)或这些items的实际布局属性不会频繁更改,则尤其如此。但是,如果您的布局需要管理数千个items,则需要权衡缓存和重新计算的优势。对于其布局不经常更改的可变大小items,缓存通常不需要定期重新计算复杂的布局信息。对于大量固定大小的items,可以根据需要计算属性更简单。而对于属性频繁更改的items,您可能会重新计算所有时间,因此缓存可能只占用内存中的额外空间。
  2. 避免子类化UICollectionView。集合视图有很少或没有自己的外观。相反,它从数据源对象和布局对象的所有布局相关信息中提取其所有视图。如果您尝试在三维中布置items,正确的方法是实现一个自定义布局,以便设置每个单元格的3D变换并正确查看。
  3. 不要从您的自定义布局对象的layoutAttributesForElementsInRect:方法调用UICollectionViewvisibleCells方法。集合视图对于items的位置一无所知,除非布局对象告诉它。所以询问可见单元格只是将请求转发到你的布局对象上。您的布局对象应始终知道内容区域中items的位置,并且可以随时返回这些items的属性。在大多数情况下,它应该自己做。在有限的情况下,布局对象可能依赖于数据源中的信息来定位items。例如,在地图上显示items的布局可能会从数据源中检索每个item的地图位置。

官方文档地址
Creating Custom Layouts

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容