iOS translation Series: Custom Collection View layout

Original Source:  Ole Begemann    translation Source:  A Trial (@ answer-huang) . Welcome to the technical translation team .

UICollectionView first introduced in iOS6, the star is a view UIKit class. It UITableView share API and design, but also on UITableView made some extensions. UICollectionView most powerful, while significantly exceed UITableView features is its fully flexible layout structure. In this article, we will achieve a fairly complex custom collection view layout, and the way to discuss important part of this class design. The sample code in the project GitHub on.

Layout Objects

UITableView UICollectionView are made and data-source and delegate driven. They view its sub-display set to play stupid container (dumb containers), for their true contents (contents) ignorant.

UICollectionView further abstract. The position of its child views it, the size and appearance of the control entrusted to a single layout object. By providing a custom layout object, you can achieve almost any layout you can imagine. UICollectionViewLayout layout inherited from the abstract base class. iOS6 proposes a specific layout implemented as UICollectionViewFlowLayout class.

flow layout can be used to implement a standard grid view, which may be in the collection view the most common use cases of. Although most people think so, but Apple is very smart, does not explicitly name the class UICollectionViewGridLayout. Use a more generic term flow layout, which better describes this type of capability: it to create your own layout by placing one by one cell, when needed, insert a horizontal or vertical columns symbol. By scrolling direction between custom, cell size and spacing, flow layout cell layout may be in single row or column. In fact, UITableView layout can be imagined as a special case of flow layout.

Before you prepare to write a subclass UICollectionViewLayout, you need to ask yourself if you can use UICollectionViewFlowLayout implement the layout of your heart. This class is very easy to customize, and can be customized to inherit itself taking a step forward. Look interested in this article .

Cells and Other Views

In order to adapt to any layout, collection view the establishment of a similar, but more flexible than table view of the view hierarchy (view hierarchy). As usual, your main content is displayed in the cell, any cell can be grouped into the section. Collection view of cells must be a subclass of UICollectionViewCell. In addition to cells, collection view additional management are two views: supplementary views and decoration views.

Supplementary views collection view corresponding to the section header table view and the footer views. Like cells, they are content driven by data source object. However, the use and the table view is not the same, supplementary view does not necessarily as a header or footer view; their number and placement is fully controlled by the position of the layout.

Decoration views purely as a decoration. They entirely layout object and layout object management, they do not get their contents from the data source. When the layout object specifies a decoration view it takes time, collection view is automatically created, and layout parameters for their application layout object provides. You do not need to prepare anything custom views.

Supplementary views and decoration views must be a subclass of UICollectionResuableView. Each view class layout you are using need to be registered in the collection view, so that when the data source to let him out of the line from the reuse pool when it was able to create a new instance. If you are using Interface Builder, you can drag and drop visual editor in a cell to complete cell registered in the collection view in the collection view. The same method can also be used in the supplementary view, provided that you use UICollectionViewFlowLayout. If not, you can only call registerClass: or registerNib: manual registration method of the view class. You need to do these operations in viewDidLoad.

 

Custom layout

As an example of the layout of a defined collection view very meaningful since, we might imagine a typical calendar application week (week) view. Calendar display once a week every day of the week is displayed in the column. Each calendar event will display the location and size of the event on behalf of the start date and duration to a cell in our collection view.

There are generally two types of collection view layout:

1. The layout calculation is independent of content. This is what you know and like UITableView UICollectionViewFlowLayout these cases. Each cell position not on the content and appearance of the display, but the display order based on the order of all the cell contents. You can set the default flow layout as an example. Each cell is based on a cell are placed before (or if there is not enough space, from the beginning of the next line). Layout objects do not have access to the actual data to calculate the layout.

2. Calculation based on the layout of the content. Our calendar view is the example of such type. In order to calculate the start and end time of the event display of layout objects collection view requires direct access to data sources. In many cases, not only need to remove the data layout objects currently visible cell, but also need to remove some of the decisions which the current cell is visible from all data records.

In our calendar example, if the layout objects accessible cells within a rectangle of a property, it must iterate over all event data sources to determine which is located in the required time window. And some relatively simple, independent data source compared to the calculated flow layout, which is enough to calculate the cell index paths within a rectangle of (all cells are the same size of the grid is assumed).

If there is a content dependent layout that suggests that you need to write custom layout classes, and at the same time you can not use custom UICollectionViewFlowLayout. So that's what we need to do.

UICollectionViewLayout document lists the subclass method needs to be rewritten.

collectionViewContentSize

Since the collection view its content did not know, so the layout of the first to provide information is the scroll area the size of this collection view to properly manage scrolling. Layout object must calculate the total size of its content at this time, including supplementary views and decoration views. Note that although most classic collection view limited to one axis direction scrolling (as UICollectionViewFlowLayout the same), but this is not necessary.

In our calendar example, we want to scroll the view vertical. For example, if we wanted one hour to take up 100 points on the vertical space, which displays the contents of a full day of height is 2400 points. Note that we can not scroll horizontally, which means that our collection view can only display a week. In order to be able to page during a calendar of more stars, we can scroll view of an independent (pagination) (you can use UIPageViewController) using multiple collection view (one per week), or stick with a collection view and returns large enough content width, which would make the user feel free to slide in both directions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (CGSize)collectionViewContentSize
 
{
 
// Don't scroll horizontally
 
CGFloat contentWidth = self.collectionView.bounds.size.width;
 
// Scroll vertically to display a full day
 
CGFloat contentHeight = DayHeaderHeight + (HeightPerHour * HoursPerDay);
 
CGSize contentSize = CGSizeMake(contentWidth, contentHeight);
 
return contentSize;
 
}

For clarity, I chose a very simple layout of the model: Assuming the same number of days per week, every day the same length,

That is indicated by the number of days 0-6. In a real calendar program, the layout will be based on the date NSCalendar extensive use for their calculations.

 

layoutAttributesForElementsInRect:

This is the most important class layout any way, and while perhaps most easily confusing way. collection view call this method and pass a rectangular coordinate system of its own past. This rectangle represents the visible rectangular region of this view (that is, its bounds), you need to be ready to handle any rectangle it to you.

你的实现必须返回一个包含UICollectionViewLayoutAttributes对象的数组,为每一个cell包含这样的一个对象,supplementary view或decoration view在矩形区域内是可见的。UICollectionViewLayoutAttributes类包含了collection view内item的所有相关布局属性。默认情况下,这个类包含frame,center,size,transform3D,alpha,zIndex属性(properties),和hidden特性(attributes)。如果你的布局想要控制其他视图的属性(比如,背景颜色),你可以建一个UICollectionViewLayoutAttributes的子类,然后加上你自己的属性。

布局属性对象通过indexPath属性和他们对应的cell,supplementary view或者decoration view关联在一起。collection view为所有items从布局对象中请求到布局属性后,它将会实例化所有视图,并将对应的属性应用到每个视图上去。

注意!这个方法涉及到所有类型的视图,也就是cell,supplementary views和decoration views。一个幼稚的实现可能会选择忽略传入的矩形,并且为collection view中的所有视图返回布局属性。在原型设计和开发布局阶段,这是一个有效的方法。但是,这将对性能产生非常坏的影响,特别是可见cell远少于所有cell数量的时候,collection view和布局对象将会为那些不可见的视图做额外不必要的工作。

你的实现需要做这几步:

1.创建一个空的mutable数组来存放所有的布局属性。

2.确定index paths中哪些cells的frame完全或部分位于矩形中。这个计算需要你从collection view的数据源中取出你需要显示的数据。然后在循环中调用你实现的layoutAttributesForItemAtIndexPath:方法为每个index path创建并配置一个合适的布局属性对象,并将每个对象添加到数组中。

3.如果你的布局包含supplementary views,计算矩形内可见supplementary view的index paths。在循环中调用你实现的layoutAttributesForSupplementaryViewOfKind:atIndexPath:,并且将这些对象加到数组中。通过为kind参数传递你选择的不同字符,你可以区分出不同种类的supplementary views(比如headers和footers)。当需要创建视图时,collection view会将kind字符传回到你的数据源。记住supplementary和decoration views的数量和种类完全由布局控制。你不会受到headers和footers的限制。

4.如果布局包含decoration views,计算矩形内可见decoration views的index paths。在循环中调用你实现的layoutAttributesForDecorationViewOfKind:atIndexPath:,并且将这些对象加到数组中。

5.返回数组。

我们自定义的布局没有使用decoration views,但是使用了两种supplementary views(column headers和row headers)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
 
{
 
NSMutableArray *layoutAttributes = [NSMutableArray array];
 
// Cells
 
// We call a custom helper method -indexPathsOfItemsInRect: here
 
// which computes the index paths of the cells that should be included
 
// in rect.
 
NSArray *visibleIndexPaths = [self indexPathsOfItemsInRect:rect];
 
for (NSIndexPath *indexPath in visibleIndexPaths) {
 
UICollectionViewLayoutAttributes *attributes =
 
[self layoutAttributesForItemAtIndexPath:indexPath];
 
[layoutAttributes addObject:attributes];
 
}
 
// Supplementary views
 
NSArray *dayHeaderViewIndexPaths = [self indexPathsOfDayHeaderViewsInRect:rect];
 
for (NSIndexPath *indexPath in dayHeaderViewIndexPaths) {
 
UICollectionViewLayoutAttributes *attributes =
 
[self layoutAttributesForSupplementaryViewOfKind:@"DayHeaderView"
 
atIndexPath:indexPath];
 
[layoutAttributes addObject:attributes];
 
}
 
NSArray *hourHeaderViewIndexPaths = [self indexPathsOfHourHeaderViewsInRect:rect];
 
for (NSIndexPath *indexPath in hourHeaderViewIndexPaths) {
 
UICollectionViewLayoutAttributes *attributes =
 
[self layoutAttributesForSupplementaryViewOfKind:@"HourHeaderView"
 
atIndexPath:indexPath];
 
[layoutAttributes addObject:attributes];
 
}
 
return layoutAttributes;
 
}

有时,collection view会为某个特殊的cell,supplementary或者decoration view向布局对象请求布局属性,而非所有可见的对象。这就是当其他三个方法开始起作用时,你实现的layoutAttributesForItemAtIndexPath:需要创建并返回一个单独的布局属性对象,这样才能正确的格式化传给你的index path所对应的cell。

你可以通过调用 +[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:]这个方法,然后根据index path修改属性。为了得到需要显示在这个index path内的数据,你可能需要访问collection view的数据源。到目前为止,至少确保设置了frame属性,除非你所有的cell都位于彼此上方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
 
{
 
CalendarDataSource *dataSource = self.collectionView.dataSource;
 
id<CalendarEvent> event = [dataSource eventAtIndexPath:indexPath];
 
UICollectionViewLayoutAttributes *attributes =
 
[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
 
attributes.frame = [self frameForEvent:event];
 
return attributes;
 
}

如果你正在使用自动布局,你可能会感到惊讶,我们正在直接修改布局参数的frame属性,而不是和约束共事,但这正是UICollectionViewLayout的工作。尽管你可能使用自动布局来定义collection view的frame和它内部每个cell的布局,但cells的frames还是需要通过老式的方法计算出来。

类似的,layoutAttributesForSupplementaryViewOfKind:atIndexPath: 和 layoutAttributesForDecorationViewOfKind:atIndexPath:方法分别需要为supplementary和decoration views做相同的事。只有你的布局包含这样的视图你才需要实现这两个方法。UICollectionViewLayoutAttributes包含另外两个工厂方法,+layoutAttributesForSupplementaryViewOfKind:withIndexPath: 和 +layoutAttributesForDecorationViewOfKind:withIndexPath:,他们是用来创建正确的布局属性对象。

shouldInvalidateLayoutForBoundsChange:

最后,当collection view的bounds改变时,布局需要告诉collection view是否需要重新计算布局。我的猜想是:当collection view改变大小时,大多数布局会被作废,比如设备旋转的时候。因此,一个幼稚的实现可能只会简单的返回YES。虽然实现功能很重要,但是scroll view的bounds在滚动时也会改变,这意味着你的布局每秒会被丢弃多次。根据计算的复杂性判断,这将会对性能产生很大的影响。

当collection view的宽度改变时,我们自定义的布局必须被丢弃,但这滚动并不会影响到布局。幸运的是,collection view将它的新bounds传给shouldInvalidateLayoutForBoundsChange: method。这样我们便能比较视图当前的bounds和新的bounds来确定返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
  
{
  
CGRect oldBounds = self.collectionView.bounds;
  
if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds)) {
  
return YES;
  
}
  
return NO;
  
}

动画

插入和删除

UITableView中的cell自带了一套非常漂亮的插入和删除动画。但是当为UICollectionView增加和删除cell定义动画功能时,UIKit工程师遇到这样一个问题:如果collection view的布局是完全可变的,那么预先定义好的动画就没办法和开发者自定义的布局很好的融合。他们提出了一个优雅的方法:当一个cell(或者supplementary或者decoration view)被插入到collection view中时,collection view不仅向其布局请求cell正常状态下的布局属性,同时还请求其初始的布局属性,比如,需要在开始有插入动画的cell。collection view会简单的创建一个animation block,并在这个block中,将所有cell的属性从初始(initial)状态改变到常态(normal)。

通过提供不同的初始布局属性,你可以完全自定义插入动画。比如,设置初始的alpha为0将会产生一个淡入的动画。同时设置一个平移和缩放将会产生移动缩放的效果。

同样的原理应用到删除上,这次动画是从常态到一系列你设置的最终布局属性。这些都是你需要在布局类中为initial或final布局参数实现的方法.

initialLayoutAttributesForAppearingitemAtIndexPath:

initialLayoutAttributesForAppearingSupplementaryElementOfKind:atIndexPath:

initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:

finalLayoutAttributesForDisappearingItemAtIndexPath:

finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:

finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:

布局间切换

可以通过类似的方式将一个collection view布局动态的切换到另外一个布局。当发送一个setCollectionViewLayout:animated:消息时,collection view会为cells在新的布局中查询新的布局参数,然后动态的将每个cell(通过index path在新旧布局中判断出相同的cell)从旧参数变换到新的布局参数。你不需要做任何事情。

结论

根据自定义collection view布局的复杂性,写一个通常很不容易。确切的说,本质上这和从头写一个完整的实现相同布局自定义视图类一样困难了。因为所涉及的计算需要确定哪些子视图当前是可见的,以及他们的位置。尽管如此,使用UICollectionView还是给你带来了一些很好的效果,比如cell重用,自动支持动画,更不要提整洁的独立布局,子视图管理,以及数据提供架构规定(data preparation its architecture prescribes.)。

自定义collection view布局也是向轻量级view controller迈出很好的一步,正如你的view controller不要包含任何布局代码。正如Chris的文章中解释的一样,将这一切和一个独立的datasource类结合在一起,collection view的视图控制器将很难再包含任何代码。

每当我使用UICollectionView的时候,我被其简洁的设计所折服。对于一个有经验的Apple工程师,为了想出如此灵活的类,很可能需要首先考虑NSTableView和UITableView。

转载于:https://www.cnblogs.com/zhengJason/p/3679402.html

Guess you like

Origin blog.csdn.net/weixin_34253126/article/details/93462287