Custom Layouts: A Worked Example

Creating a custom collection view layout is simple with straightforward requirements, but the implementation details of the process may vary. Your layout must produce layout attributes objects for every view your collection view contains. The order in which these attributes are created depend on the nature of your application. With a collection view that houses thousands of items, computing and caching layout attributes upfront is a time-consuming process, so it makes more sense to create attributes only when requested for a specific item. For an application with fewer items, computing layout information once and caching it to reference whenever attributes are requested can save your application a lot of unneeded recalculation. The worked example in this chapter belongs in the second category.

就创建一个自定义集合视图布局这要求来说简单明了,但是该过程的实现细节可能有所不同。 你的布局必须为集合视图包含的每个视图生成各种布局属性对象。 这些属性生成的顺序取决于应用程序的性质。 对于拥有成千上万个数据项的集合视图,提前计算并缓存各种布局属性是一个很耗时的过程,因此只在一个特定数据项请求时创建各种属性更有优势。 对于有少量数据项的应用程序来说,一次性计算布局信息并缓存它以便请求各种属性时引用可以为你节省很多不必要的重复计算。 本章的样例属于第二类。

Keep in mind that the provided sample code is by no means the definitive way to create a custom layout. Before you begin creating your custom layout, take time to devise an implementation structure that makes the most sense for your app to get the best performance. For a conceptual overview of the layout customizing process, see “Creating Custom Layouts.”

请注意,提供的样例代码并不是创建一个自定义布局的最终(definitive)方法。在你开始创建你的自定义布局之前,花时间设计一个实现结构,它可以最大程度让你的应用程序实现最佳性能。关于布局自定过程的概念性概述,请看Collection View Programming Guide for iOS---(六)---Creating Custom Layouts

Because this chapter presents the custom layout implementation in a specific order, follow along the example from top to bottom with a specific implementation goal in mind. This chapter focuses on creating a custom layout, not on implementing a complete app. Therefore, implementations of the views and controllers used to create the final product are not provided. The layout uses custom collection view cells as its cells and a custom view for creating the lines connecting cells to one another. Creating custom cells and views for collection views, as well as the requirements for using a collection view, are covered in previous chapters. To review this information, see “Collection View Basics” and “Designing Your Data Source and Delegate.”

因为本章以一个特定的顺序呈现自定义布局实现,因此带着一个特定的实现目标从头到尾跟随示例。 本章主要讲述创建一个自定义布局,而不是实现一整个应用程序。因此,并不提供用来创建最终产品的各种视图和控制器的实现。 布局使用自定义集合视图单元格作为它的单元格(cells),并使用一个自定义视图来创建一行接着一行的数据行。 为集合视图创建自定义单元格和视图,以及使用一个集合视图的各种要求,已经在前面章节讲述。 要想复习该信息,请看Collection View Programming Guide for iOS---(二)----Collection View Basics 和 Collection View Programming Guide for iOS---(三)---Designing Your Data Source and Delegate

The Concept

一、概念

The purpose of this worked example is to implement a custom layout for displaying a hierarchichal tree of information such as the diagram seen in Figure 6-1. The example provides snippets of code followed by an explanation of the code, along with the point in the customization process you have reached. Each section of the collection view constitutes one level of depth into the tree: Section 0 contains only the NSObject cell. Section 1 contains all of the children cell’s of NSObject. Section 2 contains all of the children cells of those children, and so on. Each of the cells is a custom cell, with a label for the associated class name, and the connections between cells are supplementary views. Because the connector view class must determine how many connections to draw, it needs access to the data in our data source. It therefore makes sense to implement these connections as supplementary views and not decoration views.

本示例的目标是实现一个自定义布局来显示一个信息的层次树,就像图6-1中所示。 该示例提供了一些代码片段(snippets),并跟着一个代码解释,以及你已经实现的自定义过程的重点。 集合视图的每一块(section)构成了树的一层:Section 0 只包含了NSObject 单元格。 Section 1 包含了所有NSObject的子单元格。 Section 2 包含了所有子单元格的子单元格,以此类推。 每个单元格都是一个自定义单元格,都有一个标签用来显示相关的类名,以及连接单元格的各种连接(connections),它们被称为辅助视图。 因为接口视图(connector view)必须决定绘制多少个连接(connections),它需要访问数据源中的数据。 因此它作为辅助视图来实现这些连接更有意义而不是作为装饰视图。

Figure 6-1  Class hierarchy

图 6-1  类层次结构

Initialization

二、 初始化

The first step in creating a custom layout is to subclass the UICollectionViewLayout class. Doing so provides you with the foundations necessary to build a custom layout.

创建一个自定义布局的第一步是子类化UICollectionViewLayout类。 这样就可以给你提供各种构建一个自定义布局的必要基础

For this example, a custom protocol is necessary to inform the layout’s spacing between certain items. If the attributes of specific items require extra information from the data source, it’s best to implement a protocol for a custom layout rather than to initiate a direct connection to the data source. Your resulting layout is more robust and reusable; it won’t be attached to a specific data source but will instead respond to any object that implements its protocol.

对于该例子,一个自定义协议很必要,它用来通知特定数据项之间的布局间隔。 如果一些特定数据项的各种属性要求从数据源获取额外的信息,它最好是为自定义布局实现一个协议而不是初始化一个到数据源的直接连接。你得到的布局更加健壮,可重用性更好;它不会被连接到一个特定的数据源但是它会响应任何实现该协议的对象。

Listing 6-1 shows the necessary code for the custom layout’s header file. Now, any class that implements the MyCustomProtocol protocol can utilize the custom layout, and the layout can query that class for the information it needs.

列表6-1 显示了实现自定义布局对象头文件的必要代码。 现在,任何实现了MyCustomProtocol 协议的类都可以使用自定义布局, 该布局可以向类请求它需要的各种信息。

Listing 6-1  Connecting to the custom protocol

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

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

Next, because the number of items the collection view will manage is relatively low, the custom layout makes use of a caching system to store the layout attributes it generates when preparing the layout and then retrieves these stored values whenever the collection view asks for them. Listing 6-2 shows the three private properties our layout will need to maintain and the init method. The layoutInformation dictionary houses all of the layout attributes for all types of views within our collection view, and the maxNumRows property keeps track of how many rows are needed to populate the tallest column of our tree. The insets object controls spacing between cells and is used in setting the frames for views and the content size. The values for the first two properties are set while preparing the layout, but the insets object should be set using the init method. In this case, INSET_TOPINSET_LEFTINSET_BOTTOM, and INSET_RIGHT refer to constants you define for each parameter.

 接下来,因为集合视图将管理的数据项数量相对较少,自定义布局使用一个缓存系统来存储生成布局时产生的各种布局属性,并在集合视图请求它们时取回这些被存储的值, 列表6-2 显示了我们的布局将申明的三个私有特性以及init方法。 layoutInformation字典包含了我们集合视图中视图的所有类型的所有布局属性。maxNumRows特性保持跟踪需要多少行来填充我们树的最高列。 insets对象控制单元格之间的空余,它用于设置视图的框架以及内容尺寸。 前两个特性的值在准备布局的时候设置,但是insets对象应该通过init方法来设置。 在本例中,INSET_TOPINSET_LEFTINSET_BOTTOM, 和 INSET_RIGHT指你为每个参数定义的常量

Listing 6-2  Initializing variables

列表 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;
}

The last step for this custom layout is to create custom layout attributes. Although this step is not always necessary, in this case, as cells are being placed, the code needs access to the index paths of the current cell’s children so that it can adjust the children cell’s frames to match that of their parent. So subclassingUICollectionViewLayoutAttributes to store an array of the cell’s children provides that information. You subclass UICollectionViewLayoutAttributes, and in the header file add the following code:

自定义布局的最后一步是创建自定义布局属性。尽管有时候不需要这步,这种情况下,当布局单元格时,代码需要访问当前单元格的子视图的索引路径,这样它就可以调整子视图的框架以符合它们父视图的框架。 因此需要子类化UICollectionViewLayoutAttributes 方法来存储单元格子视图的一个数组以提供该信息。 子类化UICollectionViewLayoutAttributes方法,在头文件中加入以下代码:

@property (nonatomic) NSArray *children;

As explained in the UICollectionViewLayoutAttributes class reference, subclassing layout attributes requires that you override the inherited isEqual:method in iOS 7 and later. For more information on why this is, see UICollectionViewLayoutAttributes Class Reference.

正如在UICollectionViewLayoutAttributes 类参考文档中所述,子类化布局属性需要在iOS 7 以及以后版本中重写isEqual: 方法。 更多信息,请看iOS---UICollectionView Class Reference---UICollectionView 类参考文档

The implementation for the isEqual: method in this case is simple because there’s only one field to compare—the contents of the children array. If the arrays of both layout attributes objects match, then they must be equal because children can belong to only one class. Listing 6-3 shows the implementation for theisEqual: method.

本例中的isEqual:方法实现很简单,因为它只需要比较一个字段---子视图数组(array)的内容。 如果两个布局属性的数组都需要匹配(match),那么它们必须相等(equal),因为子视图只能属于一个类。列表6-3 显示了isEqual: 方法的实现。

Listing 6-3  Fulfilling requirements for subclassing layout attributes

列表6-3 实现子类化布局属性的要求

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

Remember to include the header file for the custom layout attributes in the custom layout file.

记住在自定义布局文件中包含自定义布局属性的头文件。

At this point in the process, you are ready to start implementing the main part of the custom layout with the foundations you have laid.

此时,你已经开始在你布置的基础上实现自定义布局的主要部分。

Preparing the Layout

三、准备布局

Now that all of the necessary components have been initialized, you can prepare the layout. The collection view first calls the prepareLayout method during the layout process. In this example, the prepareLayout method is used to instantiate all of the layout attributes objects for every view in the collection view and then cache those attributes in our layoutInformation dictionary for later use. For more information on the prepareLayout method, see “Preparing the Layout.”

现在所有必要的组件都已经被初始化,你已经可以准备布局。 集合视图在布局过程中首先调用prepareLayout 方法。 在本例中,prepareLayout方法用于在集合视图中为每个视图初始化所有的布局属性,然后再layoutInformation字典中缓存那些属性以供以后使用。 更多关于prepareLayout方法的信息,请看 “Preparing the Layout.”

Creating the Layout Attributes

1、创建布局属性

The example implementation of the prepareLayout method is split into two parts. Figure 6-2 shows the goal for the first half of the method. The code iterates over every cell, and if that cell has children, relates those children to the parent cell. As you can see in the figure, this process is done for every cell, including the children cells of other parent cells.

示例中prepareLayout方法的实现分为2部分。 图6-2 显示了该方法第一部分的目标。 代码遍历所有的单元格,如果单元格有子单元格则关联那些子单元格到父单元格。正如图中所示,每个单元格都需要完成该过程,包括其他父单元格的子单元格。

Figure 6-2  Connecting parent and child index paths

图6-2 连接父单元格和子单元格的索引路径

Listing 6-4 shows the first half of the prepareLayout method’s implementation. Both mutable dictionaries initialized at the beginning of the code form the basis of the caching mechanism. The first, layoutInformation, is the local equivalent of the layoutInformation property. Creating a local mutable copy allows the instance variable to be immutable, which makes sense in the custom layout’s implementation because layout attributes should not be modified after theprepareLayout method finishes running. The code then iterates over each section in increasing order and then over each item within each section to create attributes for every cell. The custom method attributesWithChildrenForIndexPath: returns an instance of the custom layout attributes, with the childrenproperty populated with the index paths of the children for the item at the current index path. The attributes object is then stored within the localcellInformation dictionary with its index path as the key. This initial pass over all of the items allows the code to set the children for each item before setting the item’s frame.

列表 6-4 显示了prepareLayout方法的第一个部分实现。可变字典都在代码开始处从缓存机制的基础上被初始化。 首先,layoutInformation是 layoutInformation 特性的本地对应物(equivalent)。 创建一个本地可变拷贝可以让实例变量变成不可变变量,它在自定义布局的实现中很有意义因为布局属性在prepareLayout方法运行结束后就不能修改。 然后,代码以升序遍历每个部分(section),接着遍历每个section中的每个数据项来为每个单元格(cell)创建属性。 自定义方法attributesWithChildrenForIndexPath: 返回一个自定义布局属性的实例。该实例带有一个children特性,它表示当前索引路径的数据项所有子数据项的索引路径。 然后属性对象被存储在本地的cellInformation字典中,键(key)为它的索引路径。该过程的所有数据项都允许代码在为每个数据项设置它们的frame(框架)之前先设置它们的子数据项。

Listing 6-4  Creating layout attributes

- (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

Storing the Layout Attributes

2. 存储布局属性

Figure 6-3 depicts the process that occurs in the second half of the prepareLayout method in which the tree hierarchy is built from the last row to the first. This approach may at first seem peculiar, but it is a clever way of eliminating the complexity involved with adjusting children cell’s frames. Because the frames of children cells need to match up with those of their parent, and because the amount of space between cells on a row-to-row basis is dependent upon how many children a cell has (including how many children each child cell has and so on), you want to set the child’s frame before setting the parent’s. In this way, the child cell and all of its children cells can be adjusted to match their overall parent’s cell.

图6-3显示了prepareLayout方法的第二部分,图中显示树的层次是从最后一行开始建立知道第一行。 该方法在一开始也许看起来很奇特(peculiar),但是它是消除(eliminating)调整子单元格g的框架所涉及的复杂性的聪明方法。你会想要在设置父单元格的框架之前先设置好子单元格的框架,因为子单元格的框架需要跟它们的父单元格相配对,而且单元格之间的行间距取决于一个单元有多少个子单元(包括每个子单元格又有多少个子单元格等等)。通过这种方法可以让子单元格以及子单元格的子单元格与它们的父单元格完美匹配。

In step 1, the cells of the last column have been placed in sequential order. In step 2, the layout is determining the frames for the second column. In this column, the cells can be laid out sequentially since no cell has more than one child. However, the green cell’s frame must be adjusted to match that of its parent cell, so it is shifted down one space. In the final step, the cells for the first column are being placed. The first three cells of the second column are the children of the first cell in the first column, so the cell’s following the first cell in the first column are shifted down. In this case, it is not actually necessary to do so since the two cell’s following the first have no children of their own, but the layout object is not smart enough to know this. Rather, it always adjusts the space in case any cell following one with children has children of its own. As well, the green cells have now both shifted down to match that of their parent.

在步骤1中,最后一列的单元格已经按顺序排好。步骤2,布局(layout)正确定第二列的框架。该列中的单元格(cell)这时可以开始排序了,因为已经所有的单元格都只有一个子单元格了。 然而,绿色单元格的框架必须跟它的父单元格相匹配,所以它被下移一个空间。 最后一步,开始布置第一列。第二列的前三个是第一列第一个单元格的子单元格,因此第一列第一个单元格下面的单元格都需要被下移。在本例中,其实不需要这么做,因为第一列的第二第三个单元格没有子单元格,但是布局对象并不知道。 所以它总是把跟在有子单元格的单元格后面的单元格向下移动以空出空间,不管它们有没有子单元格。同时,绿色单元格也向下移动与它们的父单元格相匹配。

Figure 6-3  The framing process

图 6-3 调整框架过程

Listing 6-5 shows the second half of the prepareLayout method, in which the frames for each item are set. The commented numbers following some lines of code correspond to the numbered explanations found after the code.

列表 6-5 显示了prepareLayout方法的第二部分,它设置了每个单元格的框架。代码后面注释的数字表示稍后做出代码解释的顺序。

Listing 6-5  Storing layout attributes

    //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
}

In Listing 6-5, the code traverses the sections in descending order, building the tree from the back to the front. The totalHeight variable tracks how many rows down the current item needs to be. This implementation does not track spacing smartly, simply leaving empty space below cells with children so that two cell’s children never overlap, and the totalHeight variable helps accomplish this. The code accomplishes this in the following order:

列表6-5中,代码以降序遍历了所有部分(sections), 从后往前建立了树。 totalHeight 变量表示当前数据项需要跟踪多少行。该实现并没有灵活地跟踪空间,只是简单的在带有子单元格的单元格下面留出空间,这样两个单元格的子单元格就不会重叠,同时totalHeight变量也有助于完成该目的。代码按以下顺序完成这次实现:

  1. The layout attributes created in our first pass over the data are retrieved from the local dictionary before the cell’s frame is set.

    注释1,首先被传递过来的布局属性是在单元格的框架被设置之前从本地字典中取回的。

  2. The custom adjustFramesOfChildrenAndConnectorsForClassAtIndexPath: method recursively adjusts the frames of all the cell’s children and grandchildren and so on to match the cell’s frame.

    注释2,自定义方法adjustFramesOfChildrenAndConnectorsForClassAtIndexPath: 轮番调整单元格的子单元以及孙单元格,以此类推的单元格的框架来匹配父单元格的框架。

  3. After putting the adjusted attributes back in the dictionary, the totalHeight variable is adjusted to reflect where the next item’s frame needs to be. This is where the code takes advantage of the custom protocol. Whatever object implements that protocol needs to implement thenumRowsForClassAndChildrenAtIndexPath: method, which returns how many rows each class needs to occupy given how many children it has.

    注释3,把调整好的属性放回字典之后,totalHeight变量用来反映下一个数据项的所需要的框架信息。这就是自定义协议代码的优势。不管哪个实现该协议的对象都需要实现 numRowsForClassAndChildrenAtIndexPath: 方法, 它返回每个类需要占用多少行,以及它有几个子类。

  4. The maxNumRows property (later needed to set the content size) is set to section 0’s total height. The column with the longest height is always section 0, which has a height adjusted for all of the children in the tree, because this implementation doesn’t include smart space adjusting.

    注释4,maxNumRows 特性(稍后用于设置内容尺寸)设置为第0部分的总高度。因为该实现不包括灵活的空间调整,所以列的最长高度总是第0部分的高度,它的高度能用来调整树中的所有子单元格的高度。

  5. The method concludes by inserting the dictionary with all of the cell attributes into the local layoutInformation dictionary with a unique string identifier as its key.

    注释5,该方法把所有的单元格属性插入本地layoutInformation字典,并设置一个唯一的字符串标示符作为关键字。

The string identifier used to insert the dictionary in the final step is used throughout the rest of the custom layout to retrieve the correct attributes for the cells. It becomes even more important when supplementary views come into play further along in the example.

最后一步中用来插入字典的字符串标示符将被用于整个自定义布局,它被用来取回单元格的正确属性。在稍后的例子中,当补充视图出现时,它将变得更加重要,。

Providing the Content Size

四、提供内容尺寸

In preparing the layout, the code sets the value of maxNumRows to be the number of rows in the largest section in the layout. This information can be leveraged to set the appropriate content size, which is the next step in the layout process. Listing 6-6 shows the implementation for collectionViewContentSize. It relies on the constants ITEM_WIDTH and ITEM_HEIGHT, which are presumably global to the application (for instance, they are needed in the custom cell implementation to size the cell label correctly).

在准备布局阶段,代码把maxNumRows的值设置为布局中最大部分(section)的行数。该信息同时可以用来正确地设置布局过程中下一步的内容尺寸(content size)。列表 6-6 显示了 collectionViewContentSize 方法。它依赖于常量 ITEM_WIDTH 和 ITEM_HEIGHT,该常量想必是应用程序的一个全局常量。(举个例子,它们在自定义单元格实现时需要用来正确定义单元格标签的尺寸)

Listing 6-6  Sizing the content area

- (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);
}

Providing Layout Attributes

五、提供布局属性

With all of the layout attributes objects initialized and cached, the code is fully prepared to provide all of the layout information requested in thelayoutAttributesForElementsInRect: method. This method is the second step in the layout process, and unlike the prepareLayout method, it is required. The method provides a rectangle and expects an array of layout attributes objects for any views contained within the provided rectangle. In some cases, collection views that house thousands of items might wait until this method is called to initialize layout attributes objects for only the elements contained within the provided rectangle, but this implementation relies on caching instead. Therefore, the layoutAttributesForElementsInRect: method simply requires looping over all of the stored attributes and gathering them into a single array returned to the caller.

现在所有的布局属性对象都已经被初始化并缓存了,代码已经完全准备好在ayoutAttributesForElementsInRect:  方法中请求提供的所有布局信息。该方法是布局过程中的第二步,但是它并不像prepareLayout方法,它是必须的。该方法提供了一个矩形,并为该矩形内的所有视图提供一个布局属性对象的数组。有时,装有成千上万数据项的集合视图会一直等待,直到调用该方法为所提供矩形内的元素初始化布局属性对象,但是这个实现由缓存替代。 因此,layoutAttributesForElementsInRect: 方法只是简单的请求遍历所有被存储的属性并把它们收集到一个单一数组并返回给调用者。

Listing 6-7 shows the implementation of the layoutAttributesForElementsInRect: method. The code traverses all of the subdictionaries that contain layout attributes objects for specific types of views within the main dictionary _layoutInformation. If the attributes examined within the subdictionary are contained within the given rectangle, they’re added to an array storing all of the attributes within that rectangle, which is returned after all of the stored attributes have been checked.

列表 6-7 显示了layoutAttributesForElementsInRect:  方法的实现。代码遍历所有子字典,这些子字典包含了主字典_layoutInformation中的特定类型的视图所需要的布局属性对象。如果在子字典中检查的属性被包含在给定矩形中,它们将被添加到一个数组中,该数组存储了那个矩形中的所有属性,它将在所有被存储的属性都被检查完成后返回。

Listing 6-7  Collecting and processing stored attributes

- (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;
}

Note: The implementation for layoutAttributesForElementsInRect: never references whether or not the view for a given attributes is visible. Remember that the rectangle provided by this method is not necessarily going to be the visible rectangle and that, no matter what your implementation is, it should never assume the attributes it is returning are for visible views. For a lengthier discussion about the layoutAttributesForElementsInRect: method, see“Providing Layout Attributes for Items in a Given Rectangle.”

 

注意:layoutAttributesForElementsInRect: 方法的实现从未引用一个给定属性的视图,不管它是否可见。 请记住,该方法提供的矩形没有必要是可见矩形,不管你怎么实现,也绝不应该假设它返回的属性是给可见视图的。关于layoutAttributesForElementsInRect:方法的进一步讨论,请看“Providing Layout Attributes for Items in a Given Rectangle.”

 

Providing Individual Attributes When Requested

六、请求时提供单独的属性

As discussed in the section “Providing Layout Attributes On Demand,” the layout object must be prepared to return layout attributes for any singular item of any kind of view within your collection view at any time once the layout process is complete. There are methods for all three kinds of views—cells, supplementary views and decoration views—but the app currently uses cells exclusively, so the only method that requires an implementation for the time being is thelayoutAttributesForItemAtIndexPath: method.

正如“Providing Layout Attributes On Demand,”  中所讨论,一旦布局过程完成,布局对象必须准备好在任何时候为集合视图中的任何类型视图的任何单一数据项返回布局属性。目前ios有为所有的三种视图---单元格,补充视图,以及装饰视图---提供方法,但是应用程序目前只使用单元格,因此现在只需要实现layoutAttributesForItemAtIndexPath: 方法。

Listing 6-8 shows the implementation for this method. It taps into the stored dictionary for cells, and within that subdictionary, it returns the attributes object stored with the specified index path as its key.

列表6-8实现了该方法。它访问了单元格的字典以及子字典,并用关键字返回指定索引路径的属性对象。

Listing 6-8  Providing attributes for specific items

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

Figure 6-4 shows what the layout looks like at this point in the code. All of the cells have been placed and adjusted correctly to match their parents, but the lines connecting them haven’t been drawn yet.

 

图6-4展示了代码显示的布局形状。所有的单元格都已经被放置完成,并已经与它们的父单元格正确匹配,但是连接它们的线还没有被绘制。

Figure 6-4  The layout so far

 

Incorporating Supplementary Views

七、结合补充视图

In its current state, the app displays all of its cells correctly in a hierarchical sense, but because there are no lines connecting parents to their children, the class diagram is difficult to interpret. To draw lines connecting class cells to their children, this app implementation relies on a custom view that can be incorporated into the layout as a supplementary view. For more information on designing supplementary views, see “Elevating Content Through Supplementary Views.”

目前,应用程序以树的形式正确显示了所有的单元格,但是因为父单元格和子单元格之间没有用线连接,所以类图很难阐述清楚。为了绘制连线,应用程序需要实现一个自定义视图作为补充视图,并让它跟布局结合。 关于补充视图的更多信息,请看“Elevating Content Through Supplementary Views.”

Listing 6-9 shows the lines of code that could be incorporated into the implementation of prepareLayout to include supplementary views. The minor difference between creating attributes objects for cells and for supplementary views is that the method for supplementary views requires a string identifier to tell what kind of supplementary view the attributes object is for. This is because a custom layout can have multiple different types of supplementary views, whereas each layout can have only one type of cell.

列表 6-9 显示了让prepareLayout方法包含补充视图的部分实现代码。 为单元格创建属性对象 和 为补充视图创建属性对象的微小差别是补充视图需要一个字符串标示符来区分属性对象需要什么类型的补充视图。这是因为一个自定义布局可以有多个不同类型的补充视图,但是每个布局只能有一种类型的单元格。

Listing 6-9  Creating attributes objects for supplementary views

// 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"];

Because the code for supplementary views is similar to that for cells, incorporating this code into the prepareLayout method is simple. The code employs the same caching mechanism for supplementary views as it does for the cells, using another dictionary specifically for the ConnectionViewKind supplementary view. If you were going to add more than one kind of supplementary view, you would create another dictionary for that kind of view and add these lines of code for that kind of view, too. But in this case, the layout requires only one kind of supplementary view. As in the code for initializing the cell layout attributes, this implementation employs the custom frameForSupplementaryViewOfKind:AtIndexPath: method for determining a supplementary view’s frame based on what kind of view it is. Remember that the custom adjustFramesOfChildrenAndConnectorsForClassAtIndexPath: shown in the implementation of theprepareLayout method needs to incorporate the adjustment of any supplementary views relevant to the class hierarchy layout.

因为补充视图的代码实现与单元格类似,把这些代码加入prepareLayout方法很简单。 代码采用了跟单元格一样的缓存机制,专门为ConnectionViewKind补充视图创建了另一个字典。 如果你想使用多种补充视图,你可以为那种视图创建另一个字典并为那个视图添加这些代码。但是在这里,布局只需要一种补充视图。正如初始化单元格布局属性代码,该实现采用了自定义的frameForSupplementaryViewOfKind:AtIndexPath:  方法来决定补充视图的框架,该框架基于视图的类型。请记住,prepareLayout方法中的 自定义frameForSupplementaryViewOfKind:AtIndexPath: 方法中实现的任何补充视图框架需要跟相关类层次结构布局相匹配。

In the case of the example code, nothing needs to be modified in the layoutAttributesForElementsInRect: implementation because it is designed to loop over all the attributes stored in the main dictionary. As long as the supplementary view attributes are added to the main dictionary, the provided implementation of layoutAttributesForElementsInRect: works as expected.

本例中, layoutAttributesForElementsInRect: 方法实现不需要更改任何代码,因为它遍历了主字典中的所有属性。只要补充视图的属性被加入住字典, layoutAttributesForElementsInRect: 方法就能完美完成任务。

Last, as was the case for cells, the collection view may request supplementary view attributes for specific views at any time. As such, an implementation of layoutAttributesForSupplementaryElementOfKind:atIndexPath: is required.

最后,当处于单元格情况下,结合视图可能在任何时候为指定视图请求补充视图。正因为如此,layoutAttributesForSupplementaryElementOfKind:atIndexPath: 方法是必须的。

Listing 6-10 shows the implementation for the method, which is nearly identical to that of layoutAttributesForItemAtIndexPath:. As an exception, using the provided kind string rather than hardcoding a type of view into the return value allows you to use multiple supplementary views in your custom layout.

列表 6-10实现了该方法,它跟layoutAttriburesForItemAtIndexPath:方法基本相同。 只除了,在返回值中使用了提供的kind字符串,而不是硬性编码一种类型的视图,它让你可以在自定义布局中使用多个补充视图。

Listing 6-10  Providing supplementary view attributes on demand

列表 6-1, 根据需要提供补充视图

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

 

Recap

 八、总结

By including supplementary views, you now have a layout object that can adequately reproduce a class hierarchy diagram. In a final implementation, you probably want to incorporate adjustments into your custom layout to conserve space. This example explores what a real, base implmentation of a custom collection view layout might look like. Collection views are incredibly robust, and provide so much more capability than seen here. Highlighting and selecting (or even animating) cells when moved, inserted, or deleted are all easy enhancements that can be incorporated into your app. To take your custom layout to the next level, take a look at the last few sections of “Creating Custom Layouts.”

通过包含补充视图,你现在有了一个可以充分再现一个类层次结构图的布局对象。 在实现的最后,你可能想要在自定义布局中结合各种调整方法(adjustments),以节省空间。本例看起来像开发了一个真正的基础的自定义集合视图的实现。集合视图很强劲,它提供的强大功能不止这些。 移动,插入,或者删除时,高亮并选择(或甚至动画)都能简单的实现。 要想提高你的自定义布局到新水平,请看“Creating Custom Layouts.”的最后几节。

posted on 2014-12-15 17:36  cainiaozhang  阅读(566)  评论(0编辑  收藏  举报