iOS集合视图编程指南

翻译自“Collection View Programming Guide for iOS”

0 关于iOS集合视图

集合视图是用灵活的和可变的布局来显示有序的数据项集合的一种方式。最常用来显示类似网格布局的项,但在iOS中不仅仅只能用来显示行和列。在集合视图中,通过继承可以定义可视元素的精确布局,并能动态改变。因此可以实现网格,堆栈,循环布局,动态变化的布局,以及任何能想象的布局。

集合视图严格隔离显示的数据和用来显示数据的可视化元素。绝大部分情况下,应用仅仅负责管理数据和提供用来显示数据的视图对象。然后集合视图获得这些视图,并在屏幕上放置它们。集合视图与布局对象配合完成该项工作,布局对象为视图指定位置和可视化的属性,并可以通过继承实现应用的额外需求。因此,你提供数据,布局对象提供位置信息,然后集合视图结合这两个部分完成最终的显示。

0.1 概述

标准的iOS集合视图类提供了实现简单网格的所有行为。可以通过扩展标准类来支持自定义布局,以及与这些布局的特定交互。

0.1.1 集合视图管理数据驱动视图的可视化显示

集合视图可以方便的显示应用提供的数据驱动视图。集合视图只关心获得视图,并按指定方式布局它们。集合视图只关注视图的显示和排列,并不关注它们的内容。理解集合视图,数据源,布局对象和自定义对象之间的交互是使用集合视图的关键,尤其对巧妙的,高效的使用集合视图很关键。

0.1.2 流布局支持网格和其它线性显示

流布局对象是UIKit提供的具体布局对象。通常用来实现网格(即项的行和列),但也支持其它的线性流。它不仅仅支持网格,通过继承或者不继承,流布局可以创建有趣的,灵活的排列。流布局不使用继承就能支持不同尺寸的项,可变的项空间,自定义页眉和页脚,以及自定义页边。而使用继承可以更一步定制流布局类的行为。

0.1.3 单元格和布局操作可以使用手势识别

跟其它所有视图一样,集合视图可以附加手势识别来操作视图的内容。因为集合视图涉及许多视图的协作,因此在集合视图中加入手势,可以帮助理解一些基本技术。可以使用手势识别定制集合视图中的布局属性和项。

0.1.4 自定义布局不仅仅可以实现网格

通过继承基础的布局对象可以实现自定义布局。尽管设计自定义布局通常不需要太多代码,但是越深入理解布局如何工作,越能设计出更高效的布局对象。该指南的最后一章是一个完全自定义布局的例子。

1 集合视图基础

集合视图协调需要不同的对象来显示它的内容。有些对象需要自定义,并由应用提供。例如,应用必须提供数据源对象,告诉集合视图需要显示多少项。其它基础部分的对象由UIKit提供。

跟表格一样,集合视图是面向数据的对象,它的实现涉及应用中对象的协作。要想理解如果编写代码,需要知道集合视图的实现原理。

1.1 集合视图是对象协作的结果

集合视图分隔数据和数据显示的方式。尽管应用完全负责管理显示的数据,但可视化显示由许多不同的对象负责。表格1-1列出了UIKit中集合视图的类,通过它们在集合视图中扮演的不同角色排序。绝大部分类不需要继承,因此通常只需要很少的代码就能实现集合视图。当需要额外的行为时,再使用继承。

表格1-1 实现集合视图的类和协议

目标 类/协议 描述
顶层包含和管理 UICollectionView
UICollectionViewController
UICollectionView对象定义了集合视图内容的可见区域。该类从UIScrollView继承,可以根据需要包含一个很大的滚动区域。该类从布局对象接收布局信息,并基于布局信息帮助显示数据。
UICollectionViewController对象为集合视图提供控制器层次的视图管理。是否使用该对象是可选的。
内容管理 UICollectionDataSource协议
UICollectionDelegate协议
数据源对象是集合视图最重要的对象,也是必须提供的对象之一。数据源管理集合视图的内容,并创建需要显示内容的视图。通过创建一个遵循UICollectionDataSource协议的对象实现数据源对象。
集合视图代理对象可以拦截感兴趣的集合视图消息,并自定义视图的行为。例如,使用代理对象追踪项的选择和高亮。代理对象是可选的。
显示 UICollectionReusableView
UICollectionViewCell
集合视图显示的所有视图必须是UICollectionReusableView类的实例。该类使用时支持回收利用机制。回收再利用视图(而不是创建新视图)通常可以提高性能,尤其在滚动时。
UICollectionViewCell对象是可复用视图的具体类型,用做主要的数据项。
布局 UICollectionViewLayout
UICollectionViewLayoutAttributes
UICollectionViewUpdateItem
UICollectionViewLayout的子类作为布局对象,负责定义集合视图中单元格和可复用对象的位置,大小和可见的属性。
布局过程中,布局对象创建布局属性对象(UICollectionViewLayoutAttributes类的实例),告诉集合视图单元格和可复用视图的位置,以及如何显示。
当数据项插入,删除或移动时,布局对象收到UICollectionViewUpdateItem类的实例。永远不要自己创建该类的实例。
流布局 UICollectionViewFlowLayout
UICollectionViewDelegateFlowLayout协议
UICollectionFlowLayout类是具体的布局对象,用来实现网格或其它线性布局。可以用该类,或者与流代理对象组合,动态的实现自定义布局信息。

图1-1显示了集合视图核心对象之间的关系。集合视图从数据源获得显示单元格的信息。数据源和代理是自定义对象,用来管理内容,包括单元格的选择和高亮。布局对象决定单元格位置,并在一个或多个布局属性对象中发送该消息给集合视图。然后集合视图结合布局信息和真正的单元格(或其它视图)来创建最终的可视化显示。

图1-1 结合内容和布局来创建最终的显示

创建集合视图界面时,首先添加一个UICollectionView对象到storyboard或nib文件。把集合视图看做中心枢纽,其它所有对象从其中发射出去。添加该对象后,开始配置相关对象,例如数据源和代理。所有配置围绕集合视图本身展开。例如,没有创建集合视图对象时,永远不要创建布局对象。

1.2 重用视图来提高性能

集合视图用一个视图回收重用程序提高性能。当视图从屏幕中移除后,视图被移除,并放到重利用队列,而不是删除。当新内容滚动到屏幕,视图从队列移除,并设置新内容。为实现回收和重用,集合视图的所有视图必须从UICollectionReusableView类继承。

集合视图支持三种重利用视图类型,每种有不同的用途:

  • 单元格显示集合视图的主要内容。单元格用来显示数据源对象的单个数据项。每个单元格必须是UICollectionViewCell类的实例,可以根据显示的内容从该类继承。单元格对象为选择和高亮状态提供了内置支持。需要写一些自定义代码来实现真正的的单元格高亮。

  • 辅助视图(supplementary view)显示关于节的信息。跟单元格一样,辅助视图是数据驱动的。与单元格不同,辅助视图是可选的,它们的使用和位置由布局对象控制。例如,流布局支持页眉和页脚作为辅助视图。

  • 装饰视图(decoration view)是可见的装饰,完全由布局对象拥有,并且不绑定数据源对象的任何数据。例如,布局对象用装饰视图显示一个自定义的背景。

与表格视图不同,集合视图不对数据源提供的单元格和辅助视图强加任何类型。相反的,基础的可复用视图类是由你来修改的空白画布。例如,可以使用它们构建小的视图层次来显示图片,甚至动态的绘制内容。

数据源对象负责提供单元格和辅助视图。然而数据源永远不会直接创建视图。当需要视图时,数据源使用集合视图的方法出列一个期望类型的视图。不管从可复用队列接收,还是使用类、nib文件、storyboard文件创建一个新视图,出列过程总是返回一个有效的视图。

1.3 布局对象控制可视化显示

布局对象唯一的职责是决定集合视图中项的位置和样式。尽管数据源对象提供了视图和内容,但布局对象决定这些视图的大小、位置和其它显示相关的属性。这种职责的分离可以动态的改变布局,而不用改变任何数据对象。

集合视图的布局过程与其它视图的布局过程相关,但又不同。换句话说,不要混淆布局对象所做的事,与用来重定位子视图在父视图中位置的layoutSubviews方法。布局对象永远不会直接接触它管理的视图,因为它不是真的拥有这些视图。相反的,它创建描述单元格、辅助视图和装饰视图的位置,大小,以及可见显示的属性。然后集合视图应用这些属性到真正的视图对象。

集合视图中布局对象对视图的影响没有限制。布局对象可以移动部分视图,而其它的不动。可以只移动视图一点点,也可以在屏幕闪随机移动。甚至可以移动到与周围视图没有任何关系的地方。例如,布局对象可以叠加视图。真正的唯一限制是应用期望的显示方式。

图1-2是一个单元格和辅助视图垂直滚动的流布局。在垂直滚动的流布局中,内容区域的宽度是固定的,高度增加到可以容纳内容。为了计算区域,布局对象每次放置一个视图和单元格,并为每一个选择最合适的位置。在流布局中,单元格和辅助视图的大小由布局对象或者使用代理的属性指定。计算布局就是用这些属性确定每个视图的位置。

图1-2 布局对象提供布局度量

布局对象不只控制视图的大小和位置。布局对象可以指定其它与视图相关的属性,例如透明度,在3D空间的转换,在其它视图上面或下面是否可见(如果有的话)。这些属性可以创建更有趣的布局。例如,通过放置视图在另一个视图上面来创建单元格栈,并改变z序,或者使用转换让它们绕任意轴旋转。

1.4 集合视图自动初始化动画

集合视图内置最基本的动画支持。当插入(或删除)项或节时,集合视图自动对影响的视图产生动画。例如,插入项时,插入点之后的项通常会让出位置给新项。集合视图可以创建这些动画,是因为它检测到项的当前位置,并计算出插入后它们的最终位置。因此,它可以动画的把每一项从初始位置移动到最终位置。

除了动画的插入,删除和移动操作,还可以在任何时候让布局失效,并强制重新计算布局属性。让布局失效不会直接让项产生动画;当让布局失效时,集合视图在新计算出的位置显示项,并且不会
产生动画。相反的,在自定义布局中,可以使用这些属性定期的定位单元格,并产生动画效果。

2 设计数据源和代理

每个集合视图必须有一个数据源对象。数据源对象是应用要显示的内容。可以是应用的数据模型的一个对象,或者是管理集合视图的视图控制器。数据源的唯一要求是提供集合视图需要的信息,例如有多少项,使用哪个视图来显示这些项。

代理对象是可选的(但是推荐使用),负责显示内容,以及与内容交互相关的方面。尽管代理的主要工作是管理单元格的高亮和选择,但也可以通过扩展来提供额外的信息。例如,流布局扩展基本的代理行为来自定义布局度量,比如单元格的尺寸和它们之间的距离。

2.1 数据源管理内容

数据源对象负责管理集合视图显示的内容。数据源对象必须遵循UICollectionViewDataSource协议,该协议定义了必须支持的基本行为和方法。数据源的工作是为集合视图提供以下问题的答案:

  • 集合视图包含多少节?
  • 给定的节包含多少项?
  • 给定的节或项用什么视图来显示相应的内容?

节和项是集合视图内容的基本组成单位。一个集合视图通常最少包括一个节,也可以包括多个。每一节依次包括0个或多个项。项表示要显示的主要内容,节组织这些项在一个逻辑组里。例如,一个照片应用使用节表示一个相册,或者同一天拍摄的照片集。

集合视图用NSIndexPath对象指向它包括的数据。集合视图用布局对象提供的索引路径信息定位项。对于项来说,索引路径包括节号和项号。对于辅助视图和装饰视图来说,索引路径包括布局对象提供的任何一个值。尽管第一个索引代表数据源中的指定节,但辅助视图和装饰视图关联的索引路径的意义依赖于应用。视图的索引路径更多是身份证明,而不是当前可见的哪种类型的哪个视图。因此,如果在流布局中,辅助视图为节创建了页眉和页脚,索引路径提供的相关信息就是节的引用。

提示:尽管标准的索引路径支持多级,但集合视图的单元格只支持两级——节和项,与UITableView类的索引路径很像。如果需要,辅助视图和装饰视图可以有更复杂的索引路径。索引路径大于1的元素解释为相当于路径中第一个索引指定的节。通常情况下,只有第二个索引是必须的,但是辅助和装饰视图不限制为两个。设计数据源时牢记这个规则。

不管在数据对象中如何排列节和项,这些节和项的可视化显示仍然由布局对象决定。不同的布局对象显示节和项有很大的不同,如图2-1所示。该图中,流布局对象垂直排列每一节。自定义布局不是线性排列这些节,再次证明布局和真正数据之间的隔离。

**图2-1 ** 根据布局对象排列节

2.1.1 设计数据对象

高效的数据源使用节和项来帮助组织后台数据对象。把数据组织进节和项可以简化数据源方法的实现。因为数据源方法频繁被调用,所以需要确保这些方法的实现能尽可能快的检索数据。

一个简单的方案(当然不是唯一的方案)是使用嵌套数组,如图2-2所示。在该配置中,顶层数组包含一个或多个表示数据源的节的数组。每一个节的数组包含该节的数据项。查找某节的某一项就是检索节数组,然后该数组中检索项。这种排列类型很适合中等规模的项集合,以及根据需要检索单个数据项。

图2-2 使用嵌套数组排列数据对象

设计数据结构时,从简单的数组集开始,然后根据需要移入更复杂的结构中。通常情况下,数据对象绝不是性能瓶颈。集合视图通常只在计算一共有多少对象和获得当前屏幕上元素视图时才会访问数据源。如果布局对象只依赖数据对象中的数据,当数据源包括成千上万个的对象时,性能会受到严重的影响。

2.1.2 把内容告诉集合视图

在集合视图询问数据源的问题中,有数据源包括多少节,和每一节包括多少项。当下面情况发生时,集合视图要求数据源提供这些信息:

  • 集合视图第一次显示。
  • 为集合视图分配另外一个数据源。
  • 显示调用集合视图的reloadData方法。
  • 集合视图代理用PerformBatchUpdates:completion:执行一个闭包,或者任何的移动,插入,删除方法。

numberOfSectionsInCollectionView:方法提供节的数量,用collectionView:numberOfItemsInSection:方法提供每一节项的数量。必须实现collectionView:numberOfItemsInSection:方法,如果集合视图只有一节,numberOfSectionsInCollectionView:方法是可选的。这两个方法都返回整数。

如果按图2-2实现数据源,数据源方法的实现就跟列表2-1这么简单。这段代码中,_data是一个自定义变量,存储了节的顶层数组。该数组的数目就是节的数量。子数组的数目就是该节中项的数目。(当然,代码中应该实现错误检查,确保返回值是有效的。)

列表2-1 提供节和项的数目

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView*)collectionView {
    // _data is a class member variable that contains one array per section.
    return [_data count];
}
 
- (NSInteger)collectionView:(UICollectionView*)collectionView numberOfItemsInSection:(NSInteger)section {
    NSArray* sectionArray = [_data objectAtIndex:section];
    return [sectionArray count];
}

2.2 配置单元格和辅助视图

数据源的另一个重要任务是提供显示内容的视图。集合视图不保存内容。它简单的获得视图,然后把当前的布局信息应用到集合视图。因此,视图显示的任何东西都由你负责。

数据源提供节和项的数目后,集合视图让布局对象提供集合视图内容的布局属性。在某一时刻,集合视图让布局对象在指定的矩形中提供一系列元素(通常是可见的矩形)。集合视图用这个列表向数据源请求相应的单元格和辅助视图。要提供这些单元格和辅助视图,代码应该完成以下工作:

  1. 在storyboard文件中嵌入单元格和视图。(或者为每种支持的单元格和视图类型注册类或nib文件。)
  2. 被请求时,在数据源中出列和配置响应的单元格和视图。

为确保单元格和辅助视图以最高效的方式使用,集合视图承担了创建这些对象的职责。每个集合视图维护一个当前未使用的单元格和辅助视图的内部队列。向集合视图请求需要的视图,而不是自己创建对象。如果重用队列中有一个等待对象,集合视图迅速准备并返回它。如果不存在等待对象,集合视图用注册的类或nib文件创建一个新的,并返回。因此,每次出列一个单元格或视图,总能获得一个可用的对象。

使用复用标识符(reuse identifier)可以注册多种类型的单元格和辅助视图。复用标识符是区分单元格和视图类型的一个字符串。字符串的内容只跟数据源对象有关。请求视图或单元格时,使用提供的索引路径决定视图或单元格的类型,然后传递响应的复用标识符到出列方法。

2.2.1 注册单元格和辅助视图

可以通过代码或者storyboard文件配置集合视图的单元格和视图。

在storyboard中配置单元格和视图。在storyboard中配置单元格和视图时,拖拽项到集合视图,并在其中完成配置。这样就在集合视图和响应的单元格或视图之间创建了一宗关系。

  • 对于单元格,从对象库中拖拽一个集合视图单元格(Collection View Cell)到集合视图。设置自定义类和集合可复用视图的复用标识符。

  • 对于辅助视图,从对象库中拖拽一个集合可复用视图(Collection Reusable View)到集合视图。设置自定义类和集合可复用视图的复用标识符。

代码配置单元格。使用registerClass:forCell:withReuseIdentifier:方法或registerNib:forCellWithReuseIdentifier:方法关联单元格和复用标识符。可以在父视图控制器的初始化过程中调用这些方法。

代码配置辅助视图。registerClass:forSupplementaryViewOfKind:withReuseIdentifier:registerNib:forSupplementaryViewOfKind:withReuseIdentifier方法关联每种视图类型和复用标识符。可以在父视图控制器的初始化过程中调用这些方法。

尽管只是用复用标识符注册单元格,但辅助视图需要一个额外的标识符:类型字符串(kind string)。每个布局对象辅助定义它支持的辅助视图类型。例如,UICollectionViewFlowLayout类支持两种视图类型:节页眉视图和节页脚视图。它定义了两个常量字符串:UICollectionElementKindSectionHeaderUICollectionElementKindSectionFooter来区分这两个视图。在布局过程中,布局对象包括该视图类型的类型字符串和其它的布局属性。然后集合视图传递这些信息到数据源。接着数据源使用类型字符串和复用标识符来决定出列和返回哪一个视图对象。

提示:如果实现自定义布局,需要定义布局支持的辅助视图类型。一个布局可能支持任意数量的辅助视图,每一个有自己的类型字符串。

注册是一次性的事件,必须在出列任何单元格或视图之前。注册后,根据需要出列单元格或视图,不需要重复注册。不推荐出列一个或多个项后,改变注册信息。只注册一次单元格和视图,并用它完成所有工作。

2.2.2 出列和配置单元格和辅助视图

集合视图向数据源对象请求单元格和辅助视图。UICollectionViewDataSource协议包括两个方法来实现:collectionView:cellForItemAtIndexPath:collectionView:viewForSupplementaryElementOfKind:atIndexPath:。因为单元格是集合视图必须的元素,数据源必须实现第一个方法。第二个方法是可选的,由使用的布局类型决定。两种情况下,这些方法的实现遵守一个简单的模式:

  1. dequeueReusableCellWithReuseIdentifier:forIndexPath:dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:方法出列一个适当类型的单元格或视图。
  2. 用指定索引路径的数据配置视图。
  3. 返回视图。

出列过程的设计使得不用自己创建单元格或视图。只要之前注册了单元格或视图,出列方法保证永远不会返回nil。如果复用队列中没有给定类型的单元格或视图,出列方法使用注册过的storyboard、类或nib文件简单的创建一个。

从出列过程返回的单元格处于初始化状态,可以配置新的数据。对于必须创建的单元格或视图,出列过程使用正常过程创建和初始化——也就是,通过从storyboard或nib文件加载,或者通过创建一个新实例,并使用initWithFrame:方法初始化。相比之下,一个不是重新创建的项,而是从复用队列取回的项可能包括上一次的数据。这种情况下,出列方法调用项的prepareForReuse方法,从而有机会让它回到初始化状态。实现自定义单元格或视图类时,可以覆写该方法,重置属性为默认值,并执行清除工作。

数据源出列视图后,用新数据配置该视图。用传递给数据源方法的索引路径来定位相应的数据对象,然后把该数据对象应用到视图上。配置完视图后,从方法中返回该视图,工作就完成了。列表2-2是一个配置单元格的简单例子。出列单元格后,方法用单元格的位置信息设置自定义标签,然后返回单元格。

列表2-2 配置自定义单元格

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
                  cellForItemAtIndexPath:(NSIndexPath *)indexPath {
   MyCustomCell* newCell = [self.collectionView dequeueReusableCellWithReuseIdentifier:MyCellID
                                                                          forIndexPath:indexPath];
 
   newCell.cellLabel.text = [NSString stringWithFormat:@"Section:%d, Item:%d", indexPath.section, indexPath.item];
   return newCell;
}

提示:从数据源返回视图时,总是需要返回一个有效的视图。被请求的视图因为一些原因不应该被显示而返回nil,会触发一个断言,并且导致应用终止,因为布局对象希望这些方法返回有效的视图。

2.3 插入、删除、移动节和项

按下面的步骤插入,删除或移动一节或一项:

  1. 更新数据源对象的数据。
  2. 调用集合视图响应的方法插入或删除节或项。

通知集合视图任何变化之前,需要先更新数据源对象。集合视图方法假定数据源包含当前正确的数据。否则,集合视图可能从数据源接收到错误的项的集合,或者请求不存在的项而导致应用崩溃。

通过代码添加,删除或移动一个单独项时,集合视图自动创建该变化的动画。如果要多次变化一起产生动画,需要在闭包中执行所有的插入,删除或者移动,然后把闭包传递给performBatchUpdates:completion:方法。接着批量更新过程同时动画的完成所有变化。可以在同一个闭包中混合调用插入,删除或移动项。

列表2-3的例子中执行一个批量更新操作来删除当前选中的项。传递给performBatchUpdates:completion:方法的闭包首先调用一个自定义方法来更新数据源。然后告诉集合视图删除项。更新闭包和完成闭包都是同步执行的。

列表2-3 删除选中的项

[self.collectionView performBatchUpdates:^{
   NSArray* itemPaths = [self.collectionView indexPathsForSelectedItems];
 
   // Delete the items from the data source.
   [self deleteItemsFromDataSourceAtIndexPaths:itemPaths];
 
   // Now delete the items from the collection view.
   [self.collectionView deleteItemsAtIndexPaths:itemPaths];
} completion:nil];

2.4 管理选择和高亮的可见状态

集合视图默认支持单项选择,可以配置为多项选择或者禁用选择。集合视图检测到点击,然后高亮或者选中对应的单元格。大多数情况下,集合视图只修改单元格的属性来表明被选中或高亮;它不会改变单元格的可视化显示,只有一种例外情况。如果单元格的selectedBackgroundView属性包含一个有效的视图,当单元格被选中或高亮时,集合视图会显示该视图。

列表2-4的代码可以包含进自定义集合视图单元格的实现中来显示高亮和选中状态的变化。单元格第一次载入时,并且不是高亮或选中状态时,单元格的backgroundView属性总是默认视图。当单元格被高亮或选中时,selectedBackgroundView属性替换默认的背景视图。这个例子中,单元格被选中或高亮时,背景颜色从红色变为白色。

列表2-4 设置背景视图指明状态变化

UIView* backgroundView = [[UIView alloc] initWithFrame:self.bounds];
backgroundView.backgroundColor = [UIColor redColor];
self.backgroundView = backgroundView;
 
UIView* selectedBGView = [[UIView alloc] initWithFrame:self.bounds];
selectedBGView.backgroundColor = [UIColor whiteColor];
self.selectedBackgroundView = selectedBGView;

集合视图的代理提供以下方法表明高亮和选中:

  • collectionView:shouldSelectItemAtIndexPath:
  • collectionView:shouldDeselectItemAtIndexPath:
  • collectionView:didSelectItemAtIndexPath:
  • collectionView:didDeselectItemAtIndexPath:
  • collectionView:shouldHighlightItemAtIndexPath:
  • collectionView:didHighlightItemAtIndexPath:
  • collectionView:didUnhighlightItemAtIndexPath:

这些方法提供了许多机会来对集合视图的高亮和选中行为做确切希望的说明。

例如,如果想要自己绘制单元格的选中状态,可以设置selectedBackgroundView属性为nil,然后通过代理对象对单元格使用任何的可视化变化。在collectionView:didSelectItemAtIndexPath:方法中使用可视化变化,并在collectionView:didDeselectItemAtIndexPath:方法中移除它们。

如果想要自己绘制高亮状态,可以覆写collectionView:didHighlightItemAtIndexPath:collectionView:didUnhighlightItemAtIndexPath:代理方法,并用它们使用自定义的高亮。如果还对selectedBackgroundView属性指定了视图,应该用单元格内容视图的变化确保自定义变化可见。列表2-5使用内容视图的背景色改变高亮。

列表2-5 对单元格使用临时高亮

- (void)collectionView:(UICollectionView *)colView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell* cell = [colView cellForItemAtIndexPath:indexPath];
    cell.contentView.backgroundColor = [UIColor blueColor];
}
 
- (void)collectionView:(UICollectionView *)colView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell* cell = [colView cellForItemAtIndexPath:indexPath];
    cell.contentView.backgroundColor = nil;
}

单元格的高亮和选中状态有一个细微,但是重要的区别。高亮状态是一个过渡状态,用户的手指一直触摸设备时,用来响应单元格的可见。只在当集合视图追踪单元格的触摸事件时,该状态被设置为true。当触摸事件结束,高亮状态返回false。相比之下,选中状态在一系列触摸事件结束后发生改变——具体来说,当这些触摸事件表明用户试图选择单元格时。

图2-3描述当用户点击一个为选中的单元格时发生的一系列步骤。初始的按下事件导致单元格的高亮状态变为true,尽管这不会自动改变单元格的外观。如果最终的触摸抬起事件在单元格中发生,高亮状态变为false,同时集合视图把选中状态变为true。当用户改变选中状态时,集合视图显示单元格视图的selectedBackgroundView属性,这只是集合视图的单元格发生的可视化变化。其它任何的可视化变化由代理对象完成。

图2-3 在单元格中追踪触摸

用户在选中或未选中单元格时,单元格的选中状态总是最后发生变化。点击单元格首先改变高亮状态。只有在点击序列结束,并且该过程中的所有高亮都移除后,单元格的选中状态才发生变化。设计单元格时,需要确保高亮和选中状态的外观不会发生意外的冲突。

2.5 显示单元格的编辑菜单

用户在单元格上执行长按手势时,集合视图视图为单元格显示一个编辑菜单。编辑菜单可以在集合视图剪切,复制和粘贴单元格。编辑菜单显示之前需要满足几个条件:

  • 代理必须实现下面三个动作处理相关的方法:
    collectionView:shouldShowMenuForItemAtIndexPath:
    collectionView:canPerformAction:forItemAtIndexPath:withSender:
    collectionView:performAction:forItemAtIndexPath:withSender:

  • 对指定的单元格,collectionView:shouldShowMenuForItemAtIndexPath:方法应该返回true。

  • collectionView:canPerformAction:forItemAtIndexPath:withSender:方法至少对一个希望的动作返回true。集合视图支持以下动作:剪切,复制和粘贴。

如果三个条件都满足,并且用户从菜单中选择一个动作后,集合视图在指定的项上调用代理的collectionView:performAction:forItemAtIndexPath:withSender:方法。

列表2-6描述如何阻止一个菜单项的显示。例子中,collectionView:canPerformAction:forItemAtIndexPath:withSender:方法阻止编辑菜单显示剪切项。允许复制和粘贴项,用户可以插入新的内容。

列表2-6 选择性禁用编辑菜单的动作

- (BOOL)collectionView:(UICollectionView *)collectionView
        canPerformAction:(SEL)action
        forItemAtIndexPath:(NSIndexPath *)indexPath
        withSender:(id)sender {
   // Support only copying and pasting of cells.
   if ([NSStringFromSelector(action) isEqualToString:@"copy:"]
      || [NSStringFromSelector(action) isEqualToString:@"paste:"])
      return YES;
 
   // Prevent all other actions.
   return NO;
}

2.6 布局之间的转换

实现布局之间转换最简单的方式是使用setCollectionViewLayout:animated:方法。然而,如果想要控制转换,或希望它能互动,需要使用UICollectionViewTransitionLayout对象。

UICollectionViewTransitionLayout类是布局的具体类型,该类在转换到新布局时作为集合视图的布局对象。使用转换布局对象,可以不按线性路径布局对象,而使用时间算法,或根据到达的触摸事件移动。标准类提供一个线性转换到新布局,跟UICollectionViewLayout类一样,可以通过继承UICollectionViewTransitionLayout类来创建一个想要的效果。这么做的话,需要实现跟创建自定义布局一样的方法,并让实现响应用户的输入(通常是通过手势识别器)。

UICollectionViewLayout类提供了几个方法来追踪布局之间的转换。UICollectionViewTransitionLayout对象通过transitionProgress属性来追踪转换是否完成。转换发生后,代码定期更新该属性来指示转换完成百分比。例如,用UICollectionViewTransitionLayout类结合手势识别器之类的对象(可用来在布局间转换)可以创建可交互的转换。同样的,如果显示了自定义转换布局对象,UICollectionViewTransitionLayout类提供了两个追踪布局相关值的方法:updateValue:forAnimatedKey:valueForAnimatedKey:。这些方法追踪特定的浮点数值,可以在转换中设置和改变这些值,用来与布局的重要信息交互。例如,用pinch手势在布局间转换,可以用这些方法告诉转换布局对象,视图的偏移量多少时开始转换。

在应用中使用UICollectionViewTransitionLayout对象的步骤如下:

  1. initWithCurrentLayout:nextLayout:方法创建标准或自定义类。
  2. 通过定期修改transitionProgress属性与转换过程交互。改变转换进度之后,不用忘记使用集合视图的invalidateLayout方法让布局失效。
  3. 在集合视图的代理是实现collectionView:transitionLayoutForOldLayout:newLayout:方法,并返回自己的转化布局对象。
  4. 可选的用updateValue:forAnimatedKey:方法需改布局的值,来表明布局对象相关的变化值。这种情况下固定值为0.

3 使用流布局

可以在集合视图使用具体的布局对象——UICollectionViewFlowLayout类来排列项。流布局实现了打破的线性布局,意味着布局对象在线性路径上放置单元格,并沿着线放置尽可能多的单元格。布局对象用完当前排后,会创建新排,并继续布局过程。图3-1显示了一个垂直滚动的流布局样式。这个例子中,排横向排列,每一个新排在上一个排的下面。一个节的单元格可选的被节头和节尾视图包围。

图3-1 用流布局排列节和单元格

用流布局不仅仅能实现网格,还能实现更多样式。线性布局的思路可以应用到许多不同的设计。例如,适应空间在滚动方向上创建项的单一排,而不是项的网格。项可以有不同的尺寸,产生不对称性,而不是传统的网格,但仍然是线性流。有许多的可能性。

可以在代码或Interface Builder中配置流布局。配置流布局的步骤如下:

  1. 创建刘布局对象,并分配给集合视图。
  2. 配置单元格宽度和高度。
  3. 如果需要,可选的为排和项设置空间。
  4. 如果有节头和节尾,指定它们的尺寸。
  5. 设置布局的滚动方向。

重要:最低限度必须指定单元格的宽度和高度。如果不指定,项的宽度和高度设置为0,永远不可见。

3.1 自定义布局属性

流布局对象暴露几个属性来配置内容的外观。设置时,这些属性同等的应用到数据中所有项。例如,用流布局对象的itemSize属性设置单元格的尺寸导致所有单元格有同样的尺寸。

通过UICollectionViewDelegateFlowLayout协议的方法来动态改变项的空间或尺寸。在指定给集合视图本身的同一个代理对象中实现这些方法。如果给定的方法存在,流布局对象调用该方法代替固定的值。这些方法的实现必须为集合视图中所有项返回合适的值。

3.1.1 指定流布局中项的尺寸

如果集合视图中所有项的尺寸一样,指定流布局对象的itemSize属性为合适的高度和宽度值。(总是用点指定项的尺寸。)这是为尺寸不变的内容配置布局对象最快的方式。

如果想要为单元格指定不同的尺寸,必须在集合视图代理中实现collectionView:layout:sizeForItemAtIndexPath:方法。用提供的索引路径信息返回相应项的尺寸。布局中,流布局在同一行垂直居中项,如图3-2所示。排的整体高度或宽度由该方向最大的项决定。

图3-2 流布局中不同尺寸的项

提示:为单元格指定不同尺寸时,每一排的项的数量可能变化。

3.1.2 指定项和行之间的空间

用流布局可以指定同一排项之间的最小空间,以及连续排之间的最小空间。记住提供的空间只是最小空间。根据如果布局内容,流布局对象可能会增加项之间的空间,该值可能比指定的值更大。当排列的项的尺寸不同时,布局对象可能简单的增加实际排空间的值。

布局过程中,布局对象在当前排增加项,直到没有足够的空间放下整个项。如果排刚好没有额外空间的放下整数的项,项之间的空间会等于最小的空间。如果排节尾有额外的空间,布局对象增加项之间的空间,知道项均匀的在排边界中放置,如图3-3。增加空间可以整体提供项的外观,并组织每排节尾的间隙。

图3-3 项之间的实际空间可能比最小值大

流布局对象在排之间的空间和项之间的空间使用同样的技术。如果所有项的尺寸一样,流布局完全按照最小排空间值,并且一排中所有项的空间与下一排完全一样。如果项有不同的尺寸,每个项之间的实际空间可能会变化。

图3-4描述了当项有不同尺寸时,最小排空间发生的变化。对于不同尺寸的项,布局对象在滚动方向的每一排选择最大尺寸的项。例如,在垂直滚动布局中,寻找每一排中高度最大的项。然后设置这些项之间的空间为最小值。如果这些项在每一排的不同位置,实际的排空间会比最小值更大,如图所示。

图3-4 如果项尺寸不同,排空间大小也不同

对于流布局的其它属性,可以使用固定的空间值或者动态变化的值。排和项的空间逐节处理。因此在同一节中,排和项的空间是一样,而不同节之间可能不一样。用布局对象的minimumLineSpacingminimumInteritemSpacing属性静态的设置空间,或使用集合视图代理的collectionView:layout:minimumLineSpacingForSectionAtIndex:collectionView:layout:minimumInteritemSpacingForSectionAtIndex:方法设置。

3.1.3 用节的嵌入物来微调内容的间距

节的嵌入物是调整布局单元格的可用空间的一种方式。用嵌入物在节头视图之后和节尾视图之前插入空间。也可以用嵌入物在内容边界周围插入空间。图3-5显示了嵌入物如何影响垂直滚动的流布局的内容。

图3-5 节的嵌入物改变布局单元格的可用空间

因为嵌入物减少了布局单元格的可用空间,因此可用用来限制给定排的单元格数量。在不能滚动的方向上指定嵌入物,是限制每排空间的一种方式。用该信息组合适当的单元格尺寸,可用控制每一排单元格的数量。

3.2 何时从流布局继承

尽管不用继承就能有效的使用流布局,但有时仍然需要通过继承来实现需要的行为。表格3-1列出了一些需要继承UICollectionViewFlowLayout来实现需要的效果的一些场景。

表格3-1 继承UICollectionViewFlowLayout的场景

场景 继承小窍门
想添加新的辅助视图或装饰视图到布局中 标准的流布局类只支持节头和节尾视图,没有装饰视图。要支持额外的辅助和装饰视图,最少需要覆写以下方法:
* layoutAttributesForElementsInRect:(必须)
* layoutAttributesForItemAtIndexPath:(必须)
* layoutAttributesForSupplementaryViewOfKind:atIndexPath:(支持新的辅助视图)
* layoutAttributesForDecorationViewOfKind:atIndexPath:(支持新的装饰视图)
在自定义的layoutAttributesForElementsInRect:方法,可以调用super获得单元格的布局属性,然后在指定的矩形中为辅助或装饰视图添加新的属性。按需要用其它方法提供属性。
想要调整流布局返回的布局属性 覆写layoutAttributesForElementsInRect:方法和其它返回布局属性的方法。自定义方法的实现需要调用super,修改父类提供的属性,并返回它们。
要想为单元格和视图添加新的布局属性 创建自定义的UICollectionViewLayoutAttributes子类,添加所有想要显示的自定义布局信息的属性。
继承UICollectionViewFlowLayout类,并覆写layoutAttributesClass方法。在该方法中,返回自定义的子类。还需要覆写layoutAttributesForElementsInRect:方法,layoutAttributesForItemAtIndexPath:方法,以及其它任何返回布局属性的方法。在自定义的实现中,设置任何自定义属性的值。
想要指定插入或删除项的初始位置或最终位置 项的插入和删除的默认动画懂淡入淡出动画。要创建自定义动画,需要覆写下面部分或全部方法:
* initialLayoutAttributesForAppearingItemAtIndexPath:
* initialLayoutAttributesForAppearingSupplementaryElementOfKind:atIndexPath:
* initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
* finalLayoutAttributesForDisappearingItemAtIndexPath:
* finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
* finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
在这些方法的实现中,指定每个视图插入之前或删除之后的属性。流布局对象使用提供的这些属性创建插入和删除动画。
如果覆写这些方法,推荐同时覆写prepareForCollectionViewUpdates:finalizeCollectionViewUpdates方法。使用这些方法来追踪当前循环中哪些项被插入或删除。

还有些情况需要从头创建自定义布局。做这个决定之前,认真考虑是否真的需要这么做。流布局已经提供了许多可自定义的行为,这些行为适用于许多不同类型的布局,并且很容易使用,同时也很高效。而然这并不是说永远不要创建自定义布局,有些情况下自定义布局非常有意义。流布局只能在一个方向滚动,如果布局包括的内容在两个方向上都超过了屏幕的边界,此时需要自定义布局。如果布局既不是网格,也不是打破的线性布局(像上面描述一样),或者布局中的项频繁的移动,导致从流布局继承比创建自定义布局更负责,此时使用自定义布局就是一个正确的决定。

4 加入手势支持

使用手势识别器让集合视图更有交互性。添加手势识别器到集合视图,手势发生时,用它来触发动作。对于集合视图,有两种类型的动作:

  • 想要触发集合视图布局信息的变化。
  • 想要直接操作单元格和视图。

总是把手势识别器关联到集合视图本身,而不是指定的单元格或视图。UICollectionView类从UIScrollView类继承,因此关联到集合视图的手势识别器不太可能涉及其它必须追踪的手势。另外,因为集合视图必须访问数据源和布局对象,所以还必须访问所有操作单元格和视图的信息。

4.1 使用手势识别器修改布局信息

手势识别器提供了一种很容易动态修改布局参数的方式。例如,使用pinch手势识别器来改变自定义布局中之间的空间。配置这样一个手势识别器相对来说很简单:

  1. 创建手势识别器。
  2. 关联手势识别器到集合视图。
  3. 用手势识别器的处理方法来更新布局参数,并设置布局对象为无效。

跟其它所有对象对象一样,用alloc/init来创建手势识别器。初始化时,指定目标对象和手势触发时调用的动作方法。然后调用集合视图的addGestureRecognizer:方法关联到视图。绝大部分真正的工作发生在初始化时指定的动作方法中。

列表4-1是动作方法的一个例子,由关联到集合视图的pinch手势识别器调用。在该例子中,pinch数据用来改变自定义布局中单元格之间的距离。布局对象实现了自定义的updateSpreadDistance方法,其中设置了新的距离值,并保存下来再下次布局过程中使用。然后动作方法设置布局为无效,并强制更新项的位置为新值。

列表4-1 用手势识别器改变布局的值

- (void)handlePinchGesture:(UIPinchGestureRecognizer *)sender {
    if ([sender numberOfTouches] != 2)
        return;
 
   // Get the pinch points.
   CGPoint p1 = [sender locationOfTouch:0 inView:[self collectionView]];
   CGPoint p2 = [sender locationOfTouch:1 inView:[self collectionView]];
 
   // Compute the new spread distance.
    CGFloat xd = p1.x - p2.x;
    CGFloat yd = p1.y - p2.y;
    CGFloat distance = sqrt(xd*xd + yd*yd);
 
   // Update the custom layout parameter and invalidate.
   MyCustomLayout* myLayout = (MyCustomLayout*)[[self collectionView] collectionViewLayout];
   [myLayout updateSpreadDistance:distance];
   [myLayout invalidateLayout];
}

4.2 使用默认手势行为

UICollectionView类监听单个的点击来初始化高亮和选中的代理方法。如果集合视图要添加自定义点击或长按手势,需要配置为另外一个手势识别器。例如,配置一个点击(tap)手势识别器来响应双击。

列表4-2描述如何让集合视图响应自定义的手势,而不是监听单元格的选中或高亮。因为集合视图不使用手势识别器来初始化它的代理方法,所以自定义手势识别器通过设置delaysTouchesBegan属性为true延迟注册其它触摸事件,或者设置cancelsTouchesInView属性为true取消触摸事件,能获得比默认选中监听器更高的优先级。注册点击事件后,首先检测自定义手势识别器是否有优先权。如果该输入对自定义手势识别器无效,然后代理方法跟正常一样调用。

列表4-2 手势识别器优先

UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
tapGesture.delaysTouchesBegan = YES;
tapGesture.numberOfTapsRequired = 2;
[self.collectionView addGestureRecognizer:tapGesture];

4.3 操作单元格和视图

如何使用手势识别器操作单元格和视图取决于计划实现的操作类型。简单的插入和删除可以在标准手势识别器的动作方法中执行。更复杂的操作需要自定义手势识别器来追踪触摸事件。

一个需要自定义手势识别器的操作类型是在集合视图中移动单元格。移动单元格最直接的方式是在集合视图中临时的删除该单元格,用手势识别器拖拽一个可见的单元格,触摸事件结束后再新位置插入该单元格。所有这一切需要自己管理触摸事件,结合布局对象决定新的插入位置,操作数据源的变化,然后在新位置插入项。

5 创建自定义布局

创建自定义布局之前,认真考虑是否真的需要这么做。UICollectionViewFlowLayout类提供了很多有意义的行为,并且已经进行了效率上的优化,可以实现许多不同类型的标准布局。只在下面的情况下考虑实现自定义布局:

  • 想要的布局看起来不像一个网格或打破的线性布局(在行中排列项,知道一行排满,然后在下一行接着排列),或者需要在两个方向滚动。

  • 频繁改变所有单元的位置导致修改现有的流布局比创建自定义布局需要做更多的工作。

好消息是从API的视角来看,实现自定义布局并不困难。最难的部分是计算布局中项的位置。知道这些项的位置后,直接把这些信息传递给集合视图就行了。

5.1 继承UICollectionViewLayout

对于自定义布局,可以从UICollectionViewLayout继承,该类提供了设计的入口。只需要实现少数的方法,就能提供布局对象的核心行为。通过覆写剩下的方法来调整布局行为。核心方法处理以下重要的任务:

  • 指定可滚动内容区域的尺寸。
  • 为组成布局的单元格和视图提供属性对象,集合视图根据这些属性确定每个单元格和视图的位置。

尽管可以创建一个只实现核心方法的使用布局对象,如果实现一些可选的方法,可以让布局更迷人。

布局对象使用数据源提供的信息创建集合视图的布局。布局通过调用collectionView属性的方法与数据源交互,该属性在所有布局方法中都能访问。记住集合视图在布局过程中知道什么以及不知道什么。因为布局过程在后台发生,所以集合视图不能追踪视图的布局或定位。尽管布局对象不限制调用集合视图的任何方法,但要避免依赖除了必要的计算布局之外的任何东西。

5.1.1 理解核心的布局过程

集合视图与自定义布局对象直接结合来管理整个布局过程。当集合视图需要布局信息时,它向布局对象请求信息。例如,集合视图第一次显示或者重新计算大小时会请求布局信息。也可以调用布局对象的invalidateLayout方法显示的更新布局。该方法丢弃现存的布局信息,并强制布局对象产生新的布局信息。

提示:不要混淆布局对象的invalidateLayout方法和集合视图的reloadData方法。调用invalidateLayout方法不会必然的导致集合视图丢弃现存的单元格子视图。如果需要,例如移动和添加或删除项时,它会强制布局对象重新计算所有布局属性。如果数据源的数据改变,对应的是reloadData方法。不管布局更新是如何发起的,真正的布局过程都是一样的。

布局过程中,集合视图调用布局对象的具体方法。这些方法是计算项的位置,以及向集合视图提供需要的主要信息的机会。其它方法也可能被调用,但这些方法总是按下面的顺序调用:

  1. prepareLayout方法执行需要提供布局信息的预先计算。
  2. 通过初始计算,用collectionViewContentSize方法返回全部内容区域的尺寸。
  3. layoutAttributesForElementsInRect方法返回指定矩形区域的单元格和视图的属性。

图5-1描述了如何使用上面的方法生成布局信息。

图5-1 布局自定义内容

prepareLayout方法执行所有决定布局中单元格和视图位置的计算。最小限度的,应该在该方法中计算足够返回整体内容区域尺寸的信息,即在第二步中返回给集合视图的尺寸。

集合视图使用内容尺寸配置滚动视图的合适尺寸。例如,如果计算出的内容尺寸在垂直和水平方向都超过了当前设置的屏幕,滚动视图允许同时在两个方向滚动。不像UICollectionViewFlowLayout,默认它不会只在一个方向调整内容布局来滚动。

然后集合视图基于当前滚动位置,调用layoutAttributesForElementsInRect方法请求指定矩形区域内单元格和视图的属性,该矩形区域可能与可见矩形区域相同,也可能于可见矩形区域不同。返回该信息后,有效的完成布局过程。

布局完成后,单元格和视图的属性保持相同,知道程序或集合视图让布局失效。调用布局对象的invalidateLayout方法导致重新开始布局过程,从重新调用prepareLayout方法开始。滚动时集合视图可以自动让布局失效。如果用户滚动内容,集合视图调用布局对象的shouldInvalidateLayoutForBoundsChange方法,该方法返回true会让布局失效。

提示:调用invalidateLayout方法并不会立即开始更新布局。该方法仅仅标记布局与数据不一致,需要更新。下一次视图更新周期,集合视图检测布局是否混杂的,如果是就更新。实际上,可以接二连三的调用invalidateLayout方法多次,每次不会立即触发布局更新。

5.1.2 创建布局属性

布局负责的属性对象是UICollectionViewLayoutAttributes类的实例。这些实例可以在各种不同方法中创建。当应用不处理成千上万的项时,可以在准备布局时创建这些实例,因为布局信息可以被缓存和引用,而不是快速的计算。如果预先计算所有属性的成本超过在应用中缓存的好处,就仅在请求时才创建属性。

不管怎么样,用下面类方法之一创建UICollectionViewLayoutAttributes类的实例:

  • layoutAttributesForCellWithIndexPath:
  • layoutAttributesForSupplementaryViewOfKind:withIndexPath:
  • layoutAttributesForDecorationViewOfKind:withIndexPath:

根据显示的视图类型使用正确的类方法,因为集合视图用该信息从数据源对象请求视图的显示类型。使用不正确的方法会导致集合视图在错误的位置创建错误的视图,布局不会像希望的那样显示。

创建每一个属性对象之后,为相应的视图设置相关的属性。最起码,设置布局中每个视图的尺寸和位置。布局中视图有重叠的情况下,指定zIndex属性的值,确保重叠视图的顺序一致。其它属性可以控制单元格或视图的可见性或外观,可以按需要修改。如果标准的属性类不符合应用的需求,可以通过继承和扩展来存储每个视图的其它信息。继承布局属性时,必须实现isEqual方法来比较自定义属性,因为集合视图用该方法完成一些操作。

5.1.3 布局准备

在布局周期开始时,布局对象在布局过程开始前调用prepareLayout。该方法是计算之后通知布局的信息的机会。该方法不是实现自定义布局必须的,但它提供了按需完成初始计算的机会。调用该方法之后,布局过程的下一步中,布局必须有足够的信息来计算集合视图的内容尺寸。然而,该信息可以从需要的最小值到创建和存储布局将会使用的缩影布局属性对象。prepareLayout方法的使用受制于应用的基本构架,以及预先计算和按请求计算哪一个更有意义。

5.1.4 在给定矩形中为项提供布局属性

在布局过程的最后一步中,集合视图调用布局对象的layoutAttributesForElementsInRect:方法。该方法的目的是为贯穿指定矩形的每一个单元格和每一个辅助视图或装饰视图提供布局属性。对于巨大的可滚动内容区域,集合视图可能仅仅请求当前可见的内容区域的项的属性。在图5-2中,布局对象需要创建属性对象的当前可见内容是单元格6到20,包括第二个头部视图。必须准备好为集合视图内容区域的任何一部分提供布局属性。这些属性一定会为插入或删除项创建动画。

图5-2 只布局可见视图

因为layoutAttributesForElementsInRect:方法在布局对象的prepareLayout方法之后调用,所以应该有需要的绝大部分信息来返回或创建必需的属性。按一下步骤实现自定义的layoutAttributesForElementsInRect:方法:

  1. 迭代prepareLayout方法返回的数据来访问缓存属性或创建新的。
  2. 检测每一项的边界是否贯穿layoutAttributesForElementsInRect:方法中传进来的矩形中。
  3. 返回每一个贯穿的项,添加一个相应的UICollectionViewLayoutAttributes对象到一个数组。
  4. 返回布局属性数组到集合视图。

根据如何管理布局信息,可以在prepareLayout方法中创建UICollectionViewLayoutAttributes对象,或者等到在layoutAttributesForElementsInRect:方法中创建。根据程序的需求来完成该实现,牢记缓存布局信息的好处。重复计算单元格的新布局信息是一个昂贵的操作,会明显的影响应用的性能。也就是说,当集合视图管理巨大的单元格数量时,请求时创建布局信息对于性能有更大的意义。判断哪种策略对应用更有大的意义是一件很简单的事情。

提示:对于单个的项,布局对象也要能够按需提供布局属性。有几个原因会导致集合对象可能在正常布局过程之外请求该信息,包括创建合适的动画。

5.1.5 按需提供布局属性

在正式布局过程之外,集合视图会定期向布局对象请求单个项的属性。例如,配置项的插入和删除动画时,集合视图会请求该信息。布局对象必须准备好提供每个单元格,辅助视图和装饰视图的布局信息。通过覆写下面的方法完成该项工作:

  • layoutAttributesForItemAtIndexPath:
  • layoutAttributesForSupplementaryViewOfKind:atIndexPath:
  • layoutAttributesForDecorationViewOfKind:atIndexPath:

这些方法的实现中,需要为给定的单元格或视图检索布局信息。每个自定义布局对象都应该实现layoutAttributesForItemAtIndexPath:方法。如果布局中不包括任何的辅助视图,就不需要覆写layoutAttributesForSupplementaryViewOfKind:atIndexPath:方法。类似的,不包括装饰视图,就不需要覆写layoutAttributesForDecorationViewOfKind:atIndexPath:方法。返回属性时,不应该更新布局属性。如果需要改变布局信息,让布局对象无效,并在下一次布局周期中更新数据。

5.1.6 连接自定义布局

有两种方法连接自定义布局到集合视图:代码或者storyboard。集合视图通过可写属性collectionViewLayout链接到它的布局。通过设置集合视图的布局属性为自定义布局对象来设置布局为自定义的实现。列表5-1是需要的代码。

列表5-1 链接自定义布局

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

另外,从storyboard链接时,打开Document Outline面板,选择集合视图(在控制器的下拉列表中)。选中之后,打开Utilties面板中的Attributes检查器,在Collection View节标签下,改变Layout选项为Custom。下面的选项从Scroll Direction变为Class,然后选择自定义布局类。

5.2 让自定义布局更迷人

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

5.2.1 通过辅助视图提升内容

辅助视图跟集合视图的单元格是分开的,有自己的布局属性集。跟单元格一样,这些视图由数据源对象提供,目的是为了增强应用的主内容。例如,UICollectionViewFlowLayout为节头和结尾使用辅助视图。其它应用可以用辅助视图为每一个单元格增加一个自己的文本标签,用来显示该单元格的信息。跟集合视图的单元格一样,辅助视图通过循环利用来优化集合视图使用的资源。因此,应用中的所有辅助视图应该从UICollectionReusableView类继承。

为布局添加辅助视图的步骤如下:

  1. registerClass:forSupplementaryViewOfKind:withReuseIdentifier:registerNib:forSupplementaryViewOfKind:withReuseIdentifier:方法注册辅助视图到集合视图的布局对象。
  2. 在数据源中实现collectionView:viewForSupplementaryElementOfKind:atIndexPath:方法。因为这些视图是可复用的,所有调用dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:方法出列或创建新的可复用视图,并在返回前设置所有数据。
  3. 跟单元格一样,为辅助视图创建布局属性对象。
  4. 通过layoutAttributesForElementsInRect:方法返回包括这些布局属性对象的属性数组。
  5. 当请求时,通过layoutAttributesForSupplementaryViewOfKind:atIndexPath:方法为指定的辅助视图返回属性对象。

在自定义布局中为辅助视图创建属性对象的过程几乎与单元格一样。差别是在自定义布局中,可以有多个类型的辅助视图,但是只能有一种类型的单元格。这是因为辅助视图是为了增强主内容,并从主内容中分隔开。有许多种增强主内容的方法,因此每种辅助视图的方法指定了处理的是哪种视图,跟其它类型区分开来,并允许布局根据类型正确的计算属性。注册辅助视图时,提供给布局对象的字符串用来区分不同的视图。

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

装饰视图用来增强集合视图布局的外观。跟单元格和辅助视图不同,装饰视图只提供可见的内容,因此不依赖与数据源。可以用它们提供自定义背景,填充单元格周围的空间,甚至可以模糊单元格。装饰视图仅仅由布局对象定义和管理,不与数据源对象交互。

根据以下步骤为布局添加装饰视图:

  1. 使用registerClass:forDecorationViewOfKind:或者registerNib:forDecorationViewOfKind:方法为布局对象注册装饰视图。尽管该方法与注册单元格和辅助视图类似,但请记住注册装饰视图发生在布局对象中,而不是数据源。
  2. 在布局对象的layoutAttributesForElementsInRect:方法中,跟创建单元格和辅助视图一样,为装饰视图创建属性。
  3. 在布局对象中实现layoutAttributesForDecorationViewOfKind:atIndexPath:方法,请求装饰视图时返回属性。
  4. 可选的实现initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:方法,来处理装饰视图显示和消失时的动画。

创建装饰视图的过程与创建单元格和辅助视图的过程不同。所要做的所有事就是注册类或nib文件,来保证请求时能创建装饰视图。因为它们只是单纯的可见,所以除了在提供的nib文件,或对象的initWithFrame:方法,装饰视图不需要任何的配置。基于这个原因,需要时,集合视图创建装饰视图,并设置布局对象提供的属性。所有装饰视图必须是UICollectionReusableView类的子类,因为布局对象为装饰视图提供了可复用机制。

提示:创建装饰视图的属性时,不要忘记考虑zIndex属性。通过zIndex属性可以在显示的单元格和辅助视图之后(如果喜欢的话,也可以在之前)放置装饰视图。

5.2.3 让插入和删除动画更有趣

在布局中插入和删除是一个很有趣的挑战。插入单元格导致布局改变其它的单元格和视图。尽管布局对象没有被插入的单元格的当前位置,它还是知道如何从当前位置动画的移动现有单元格到新位置。比起没有动画的插入新的单元格,集合视图会向布局对象请求一系列动画需要的初始属性。类似的,单元格被删除时,集合视图向布局对象请求一系列结束时需要的最终属性。

通过例子来理解初始属性如何工作。开始的布局(图5-3)是一个初始只包括三个单元格的集合视图。当新单元格被插入时,集合视图请求布局对象提供被插入的单元格的初始属性。在该例子中,布局对象设置单元格的位置为集合视图的中间,并设置alpha为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中的代码会让所有单元格产生动画,所以之前已存在的三个单元格会从集合视图中间弹出。要只让插入的单元格产生动画,需要检查项的索引路径是否与传递到prepareForCollectionViewUpdates:方法的项的索引路径匹配,只在匹配时才执行动画。否则,返回调用initialLayoutAttributesForAppearingItemAtIndexPath:父类方法返回的值。

处理删除的过程与插入完全一样,除了用指定最终属性代替初始属性。在之前的例子中,如果用插入单元格时一样的属性,删除单元格会让单元格在移动到集合视图中间时淡出。UICollectionViewLayout类有六个可用的方法——项,辅助视图和装饰视图各两个(初始和最终属性)。

5.2.4 提高布局的滚动体验

自定义布局对象影响集合视图的滚动行为,从而影响用户体验。滚动相关触摸事件结束后,滚动视图根据实际的当前速度和减速度决定滚动内容的最终位置。当集合视图知道该位置后,调用targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法向布局对象请求位置是否应该修改。因为调用该方法时下面的内容仍然在移动,所以自定义布局会影响滚动内容的最终位置。

图5-4阐述了如何使用布局对象改变集合视图的滚动行为。假设集合视图的偏移量在(0, 0),用户向左滑动。集合视图计算滚动在哪自然地停止,并把该值作为建议的内容偏移量。布局对象可能会改变该建议的值,保证滚动停止时,一个项正好在集合视图可见边界的中间。该新的值成为目标的内容偏移量,也就是从targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法返回的值。

图5-4 改变推荐的内容偏移量为更合适的值

5.3 实现自定义布局的小窍门

下面是实现自定义布局对象的窍门和建议:

  • 考虑使用prepareLayout方法创建和存储之后需要的UICollectionViewLayoutAttributes对象。集合视图会在将来某些时间点请求布局属性对象,因此有些情况下预先创建和存储它们很有意义。尤其在项的数量(几百)相对较少时,或者实际的布局属性很少发生变化时。

如果布局需要管理成千上万个项时,需要权衡缓存和重计算的利益。对于布局很少变化的可变尺寸的项,缓存通常可以消除定期重计算复杂的布局信息。对于大量固定尺寸的项,按需计算属性会更简单。对于属性经常变化的项,始终需要重新计算,因此缓存可能只会浪费额外的内存空间。

  • 避免继承UICollectionView。集合视图本身几乎没有外观。相反的,它从数据源对象获得所有视图,从布局对象获得所有布局相关的信息。如果视图用三种尺寸布局项,合适的方法是实现自定义布局,并适当的设置每个单元格和视图的3D转换。

  • 永远不要在自定义布局对象中调用UICollectionViewvisibleCells方法。集合视图不知道项的位置,而是布局对象告诉它的。因此应该向布局对象请求可见单元格。

布局对象应该总是知道内容区域的项的位置,并且能在任何时候返回这些项的属性。绝大部分情况下,应该自己完成该项工作。在有效的情况下,布局对象可能依赖数据源中的信息来放置项。例如,在地图上显示项的布局,可能从数据源接收每一项在地图上的位置。

6 自定义布局:一个可以工作的例子

对于简单的需求,创建一个自定义的集合视图布局很容易,但是实现细节的过程不一样。布局必须为集合视图包括的每一个视图产生布局属性对象。这些属性的创建顺序取决于应用的性质。对于有成千上万个项的集合视图,预先计算和缓存布局属性是一个耗时的过程,因此只在请求时创建指定项的属性变得很有意义。对于项的数量比较少的应用,计算并缓存一次布局信息,然后在请求时引用属性可以节省很多不必要的重复计算。本章的例子属于第二种类型。

牢记例子中的代码绝不是创建自定义布局的最终方式。开始创建自定义布局之前,花些时间设置实现架构对于应用获得最佳的性能有决定性的意义。

因为本章阐述了在特定顺序实现自定义布局,所以记住该例子从头到尾遵循了一个特定的实现目标。本章关注创建自定义布局,而不是实现一个完整的应用。因此没有提供创建最终产品的视图和控制器的实现。布局使用自定义集合视图单元格作为单元格,自定义视图用来创建链接不同单元格的线条。

6.1 概念

这个可以工作的例子的目标是实现一个自定义布局,用来显示继承关系的信息树,如图6-1所示。连同当前的自定义过程节点,解释代码之后,提供了一些代码块。集合视图的每一节包括树中的一个深度层次:第0节只包含NSObject单元格。第1节包含NSObject的所有子单元格。第2节包含包含这些子单元格的所有子单元格,以此类推。每一个单元格都是一个自定义单元格,包括一个关联类名的标签,单元格之间的联系作为辅助视图。因为视图连接器类必须决定需要绘制多少连接,所以需要访问数据源的数据。因此用辅助视图实现这些连接,而不是装饰视图。

图6-1 类的继承关系

6.2 初始化

创建自定义布局的第一步是继承UICollectionViewLayout类。这么做提供了构建自定义布局的必要的基础。

该例子中,需要一个自定义协议来报告某些项之间的空间。如果特定项的属性需要从数据源获得额外的信息,最好为自定义布局实现一个协议,而不是初始化一个直接的关联到数据源。布局会更健壮和可复用;它不会关联到特定的数据源,而是响应任何实现这个协议的对象。

列表6-1是自定义布局的头文件的必要代码。现在,任何实现MyCustomProtocol协议的类可以利用自定义布局,布局可以在该类中查询需要的信息。

列表6-1 连接到自定义协议

@interface MyCustomLayout : UICollectionViewLayout
@property (nonatomic, weak) id<MyCustomProtocol> customDataSource;
@end

接下来,自定义布局使用缓存系统存储布局属性,这些属性在准备布局时产生,然后在集合视图请求时检索这些存储的值。这么做的原因是集合视图管理的项的数量相对较少。列表6-2是需要布局维护的三个私有属性和初始化方法。其中layoutInformation字典存储集合视图中所有视图类型的布局属性,maxNumRows属性存储多少行,用来构成树的最高的列,insets对象控制单元格之间的空间,以及设置视图的边框或内容尺寸。前两个属性在准备布局是设置,而insets对象用init方法设置。这种情况下,INSET_TOPINSET_LEFTINSET_BOTTOMINSET_RIGHT适用于为每个参数定义的常量。

列表6-2 初始化变量

@interface MyCustomLayout()
 
@property (nonatomic) NSDictionary *layoutInformation;
@property (nonatomic) NSInteger maxNumRows;
@property (nonatomic) UIEdgeInsets insets;
 
@end
 
-(id)init {
    if(self = [super init]) {
        self.insets = UIEdgeInsetsMake(INSET_TOP, INSET_LEFT, INSET_BOTTOM, INSET_RIGHT);
    }
    return self;
}

自定义布局的最后一步是创建自定义布局属性。尽管这一步不总是必须的,但在这个例子中,单元格被放置后,代码需要访问当前单元格的子节点的索引路径,从而能够调整子单元格的边界来匹配父单元格。因此从UICollectionViewLayoutAttributes继承,用来存储一个数组,该数组存储提供该信息的单元格的子节点。从UICollectionViewLayoutAttributes继承,添加下面的代码到头文件中:

@property (nonatomic) NSArray *children;

在iOS 7及后续版本中,继承布局属性需要覆写isEqual:方法。这个例子中,isEqual:方法的实现很简单,因为只有一个字段需要比较——子数组的内容。如果数组中两个布局属性对象匹配,它们就必须相等,因为子节点只能属于一个类。列表6-3是isEqual:方法的实现。

列表6-3 实现继承布局属性的要求

-(BOOL)isEqual:(id)object {
    MyCustomAttributes *otherAttributes = (MyCustomAttributes *)object;
    if ([self.children isEqualToArray:otherAttributes.children]) {
        return [super isEqual:object];
    }
    return NO;
}

记得在自定义布局文件中为自定义布局属性引入头文件。

此时,已经准备好开始实现自定义布局的主要部分。

6.3 布局准备

此时,所有必须的组件已经完成初始化,可以开始准备布局。集合视图首先调用布局过程中的prepareLayout方法。在该例子中,prepareLayout方法用来实例化集合视图中每个视图的所有布局属性对象,然后在layoutInformation字典中缓存这些属性,之后会用到这些属性。

6.3.1 创建布局属性

例子中prepareLayout方法的实现分为两部分。图6-2是该方法第一部分的目的。代码迭代每一个单元格,如果该单元格有子节点,就关联这些子节点到父单元格。如图所示,每一个单元格完成该过程,包括其它父单元格的子单元格。

图6-2 关联父和子索引路径

列表6-4是prepareLayout方法的第一部分实现。在代码开始初始化的两个可变字典组成了基本的缓存机制。首先,layoutInformation是一个局部变量,与layoutInformation属性相等。创建一个局部可变副本允许实例变量成为不可变的,因为布局属性在prepareLayout方法执行后不应该再改变。然后代码递增迭代每一节,为每个单元格的每一节中的每一项创建属性。自定义的attributesWithChildrenForIndexPath:方法返回一个自定义布局属性的实例,子属性由当前索引路径项的子节点的索引路径填充。接着,局部变量cellInformation字典用索引路径作为键存储属性对象。设置项的边界之前,所有项的初始路径允许代码为每一项设置子节点。

列表6-4 创建布局属性

- (void)prepareLayout {
    NSMutableDictionary *layoutInformation = [NSMutableDictionary dictionary];
    NSMutableDictionary *cellInformation = [NSMutableDictionary dictionary];
    NSIndexPath *indexPath;
    NSInteger numSections = [self.collectionView numberOfSections;]
    for(NSInteger section = 0; section < numSections; section++){
        NSInteger numItems = [self.collectionView numberOfItemsInSection:section];
        for(NSInteger item = 0; item < numItems; item++){
            indexPath = [NSIndexPath indexPathForItem:item inSection:section];
            MyCustomAttributes *attributes =
            [self attributesWithChildrenAtIndexPath:indexPath];
            [cellInformation setObject:attributes forKey:indexPath];
        }
    }
    //end of first section

6.3.2 存储布局属性

图6-4描述了发生在prepareLayout方法第二部分的过程,该过程中,树的层次结构从最后一行到第一行开始构建。刚开始看起来该方法有点奇怪,但这是一种很聪明的方法,可以消除调整子单元格边界的复杂性。因为子单元格的边界需要匹配父单元格的边界,以及每一行单元格之间的空间大小依赖于一个单元格由多少子节点(包括每一个子单元格有多少子节点,以此类推),所以在设置父节点的边界之前,先设置子节点的边界。这种方式中,子单元格和它的所有子单元格可以调整来匹配整个父单元格。

第一步中,最后一列的单元格已经按顺序排列。第二步中,布局正在决定第二列的边界。这一列中,单元格按顺序布局,直到没有单元格有多余一个的子节点。但是绿色的单元格的边界必须调整来匹配它的父单元格,所以它向下移动一个空间。最后一步中,正在排列第一列中的单元格。第二列的前三个单元格是第一列第一个单元格的子节点,所以第一列剩下的单元格向下移动。这个例子中,不需要真的这么做,因为第一个单元格之后的两个单元格没有子节点,但是布局对象不知道这些。如果一个有子节点的单元格之后的单元格本身有子节点,宁可总是调整空间。同样的,两个绿色单元格都需要下移匹配它们的父节点。

图6-4 确定边界的过程

列表6-5是prepareLayout方法的第二部分,其中设置每一项的边界。有些代码之后的数字注释与之后解释代码的序号对应。

列表6-5 存储布局属性

    //continuation of prepareLayout implementation
    for(NSInteger section = numSections - 1; section >= 0; section—-){
        NSInteger numItems = [self.collectionView numberOfItemsInSection:section];
        NSInteger totalHeight = 0;
        for(NSInteger item = 0; item < numItems; item++){
            indexPath = [NSIndexPath indexPathForItem:item inSection:section];
            MyCustomAttributes *attributes = [cellInfo objectForKey:indexPath]; // 1
            attributes.frame = [self frameForCellAtIndexPath:indexPath
                                withHeight:totalHeight];
            [self adjustFramesOfChildrenAndConnectorsForClassAtIndexPath:indexPath]; // 2
            cellInfo[indexPath] = attributes;
            totalHeight += [self.customDataSource
                            numRowsForClassAndChildrenAtIndexPath:indexPath]; // 3
        }
        if(section == 0){
            self.maxNumRows = totalHeight; // 4
        }
    }
    [layoutInformation setObject:cellInformation forKey:@"MyCellKind"]; // 5
    self.layoutInformation = layoutInformation
}

列表6-5中,代码按降序遍历节,从后向前构建树。totalHeight变量存储当前项之后需要多少行。该实现没有很聪明的追踪空间,只是简单的在有子节点的单元格后留下空白空间,这样两个单元格的子节点就永远不会重叠,用totalHeight变量帮助完成这个功能。代码按下面的顺序完成这个功能:

  1. 设置单元格的编辑之前,从局部字典中取回第一部分创建的对象属性。
  2. 自定义的adjustFramesOfChildrenAndConnectorsForClassAtIndexPath:递归的调整所有单元格的子节点,孙子节点等等,来匹配单元格的边框。
  3. 把调整后的属性放回字典后,totalHeight变量指向下一项的边框。这里用到了自定义的协议。任何实现该协议的对象都需要实现numRowsForClassAndChildrenAtIndexPath:方法,该方法根据每个类有多少个子类,返回每个类占据的行数。
  4. maxNumRows属性(之后用来设置内容尺寸)设置为第0节的总高度。因为该实现没有智能的调整空间,所以最高的那一列总是第0节,该节的高度为树中所有子节点调整后的高度。
  5. 在该方法最后,用唯一的字符串标识符作为键,插入所有单元格属性到局部layoutInformation字典。

最后一步中用来插入字典的字符串标识符贯穿剩下的自定义布局过程,用来检索单元格的正确属性。当例子中用到辅助视图后,它会变得更重要。

6.4 提供内容尺寸

在准备布局时,代码设置maxNumRows的值为布局中最大节的行数。该信息可以在布局过程的下一步中用来设置合适的内容尺寸。列表6-6是collectionViewContentSize的实现。它依赖ITEM_WIDTHITEM_HEIGHT常量,假定它们为应用的全局常量(例如,在自定义单元格的实现中,用来正确的确定单元格标签的尺寸)。

列表6-6 确定内容区域的尺寸

- (CGSize)collectionViewContentSize {
    CGFloat width = self.collectionView.numberOfSections * (ITEM_WIDTH + self.insets.left + self.insets.right);
    CGFloat height = self.maxNumRows * (ITEM_HEIGHT + _insets.top + _insets.bottom);
    return CGSizeMake(width, height);
}

6.5 提供布局属性

当所有布局属性对象初始化和缓存之后,代码已经完全准备好,可以提供在layoutAttributesForElementsInRect:方法中请求的所有布局信息。该方法是布局过程的第二步,与prepareLayout方法不同,该方法是必须的。该方法提供一个矩形,返回一个该矩形中所有视图的布局属性对象的数组。某些情况下,有成千上万个项的集合视图可能直到该方法被调用才只初始化矩形中包括的元素的布局属性对象,但是这个例子中的实现依赖缓存。因此,layoutAttributesForElementsInRect:方法需要循环所有存储的属性,并返回一个收集属性的数组给调用者。

列表6-7是layoutAttributesForElementsInRect:方法的实现。代码遍历所有包含主字典_layoutInformation中指定视图类型的布局属性对象的子字典。如果在子字典中检测到的属性在给定的矩形中,就把它们添加到数组中,该数组存储给定矩形的所有属性,当存储的所有属性检测完后,返回该数组。

列表6-7 收集和处理存储的属性

- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
    NSMutableArray *myAttributes [NSMutableArray arrayWithCapacity:self.layoutInformation.count];
    for(NSString *key in self.layoutInformation){
        NSDictionary *attributesDict = [self.layoutInformation objectForKey:key];
        for(NSIndexPath *key in attributesDict){
            UICollectionViewLayoutAttributes *attributes =
            [attributesDict objectForKey:key];
            if(CGRectIntersectsRect(rect, attributes.frame)){
                [attributes addObject:attributes];
            }
        }
    }
    return myAttributes;
}

提示:实现layoutAttributesForElementsInRect:方法时,永远不要涉及给定属性的视图是否可见。该方法提供的矩形不是必须会成为可见矩形,不管如何实现,永远不要建设它是为可见视图返回属性。

6.6 请求时提供单个属性

布局过程完成后,布局对象必须准备好在任何时候,为集合视图的任何视图类型的单个项返回布局属性。有三种类型的视图(单元格,辅助视图和装饰视图)方法,但是这个应用只用到了单元格,所以只有layoutAttributesForItemAtIndexPath:方法是必须实现的。

列表6-8是该方法的实现。它进入存储为单元格存储的字典,并在子字典中,返回用指定索引路径为键存储的属性对象。

列表6-8 为指定项提供属性

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    return self.layoutInfo[@"MyCellKind"][indexPath];
}

图6-4是此时代码实现的布局的样子。索引单元格都正确的放置和调整为匹配它们的父节点,但还没有绘制它们之间的连接线。

图6-4 目前为止的布局

6.7 加入辅助视图

当前状态中,应用在继承结构场景中正确的显示了所有单元格,但是父节点和子节点之间还没有连接线,所以还很难理解类图。程序用自定义视图作为辅助视图添加到布局中,来绘制类单元格和它们子节点之间的连接线。

列表6-9中的代码可以加入到prepareLayout的实现中来添加辅助视图。创建单元格的属性对象和辅助视图的属性对象有一个小区别,辅助视图的方法需要一个字符串标识符,用来区分属性对象属于哪种类型的辅助视图。这是因为一个自定义布局可以有多个不同类型的辅助视图,而只能有一种类型的单元格。

列表6-9 为辅助视图创建属性对象

// create another dictionary to specifically house the attributes for the supplementary view
NSMutableDictionary *supplementaryInfo = [NSMutableDictionary dictionary];
…
// within the initial pass over the data, create a set of attributes for the supplementary views as well
UICollectionViewLayoutAttributes *supplementaryAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:@"ConnectionViewKind" withIndexPath:indexPath];
[supplementaryInfo setObject: supplementaryAttributes forKey:indexPath];
…
// in the second pass over the data, set the frame for the supplementary views just as you did for the cells
UICollectionViewLayoutAttributes *supplementaryAttributes = [supplementaryInfo objectForKey:indexPath];
supplementaryAttributes.frame = [self frameForSupplementaryViewOfKind:@"ConnectionViewKind" AtIndexPath:indexPath];
[supplementaryInfo setObject:supplementaryAttributes ForKey:indexPath];
...
// before setting the instance version of _layoutInformation, insert the local supplementaryInfo dictionary into the local layoutInformation dictionary
[layoutInformation setObject:supplementaryInfo forKey:@"ConnectionViewKind"];

因为辅助视图的代码跟单元格的代码很相似,所以很容易把这段代码添加到prepareLayout方法中。这段代码中辅助视图的缓存机制与单元格的一样,为ConnectionViewKind辅助视图使用另一个字典。如果要想添加更多类型的辅助视图,可以创建另一个字典,并为这种类型的视图添加这些代码。这个例子中,布局只需要一种类型的辅助视图。对比代码中初始化单元格的布局属性,这个实现中用自定义的frameForSupplementaryViewOfKind:AtIndexPath:方法根据视图的类型决定辅助视图的边界。记住,在prepareLayout方法中的自定义方法adjustFramesOfChildrenAndConnectorsForClassAtIndexPath:需要添加所有类继承布局相关的辅助视图的调整。

这个例子中,不需要修改layoutAttributesForElementsInRect:方法,因为它会循环所有存储在主字典中的属性。一旦辅助视图的属性添加到主字典后,layoutAttributesForElementsInRect:方法就能按期望工作。

最后,与单元格的情况一样,集合视图可能在任何时候为指定视图请求辅助视图属性。因此需要实现layoutAttributesForSupplementaryElementOfKind:atIndexPath:方法。

列表6-10是该方法的实现,几乎与layoutAttributesForItemAtIndexPath:一样。唯一的不同是,使用提供的字符串,而不是在返回值中硬编码视图类型,这样可以在自定义布局中使用多种辅助视图。

列表6-10 按需要提供辅助视图属性

- (UICollectionViewLayoutAttributes *) layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
    return self.layoutInfo[kind][indexPath];
}

6.8 总结

通过添加辅助视图,现在的布局对象足够重现类继承图。在最终的实现中,可能想要在自定义布局中节约空间。这个例子探讨了一个真实的自定义布局的基本实现应该是什么样。集合视图非常健壮,能提供比这更多的功能。在应用中添加移动,插入或删除单元格时高亮和选中(甚至动画)单元格可以很容易的增强功能。

推荐阅读更多精彩内容