iOSの翻訳シリーズ:カスタムコレクションビューのレイアウト

オリジナルソース:  オレBegemannの   翻訳出典:  試用版(@ アンサー黄)ようこそ、技術翻訳チーム

UICollectionViewが最初iOS6で導入された、星は、ビューのUIKitクラスです。これは、APIとデザインを共有するだけでなく、のUITableViewにいくつかの拡張機能を作ったのUITableView。UICollectionViewかなりのUITableViewの機能を超えている間、最も強力で、完全にフレキシブルなレイアウト構造です。この記事では、我々は、かなり複雑なカスタム・コレクション・ビューのレイアウト、およびこのクラスの設計の重要な部分を議論するための方法を実現します。プロジェクト内のサンプル・コードはGitHub上。

レイアウトオブジェクト

UITableView UICollectionViewが作られており、データ・ソースとデリゲートを駆動します。彼らは無知な自分の本当の内容(コンテンツ)のために、愚かなコンテナ(ダムコンテナ)を再生するために設定されたサブディスプレイを表示します。

UICollectionViewさらに抽象的。その子の位置は、1つのレイアウトオブジェクトに委託コントロールのサイズと外観を、それを見て。カスタムレイアウトオブジェクトを提供することにより、あなたが想像できるほぼすべてのレイアウトを実現することができます。UICollectionViewLayoutレイアウトは、抽象基本クラスから継承されました。iOS6はUICollectionViewFlowLayoutクラスとして実装され、特定のレイアウトを提案しています。

フローレイアウトは、最も一般的なユースケースを表示コレクションであってもよく、標準のグリッドビューを、実装するために使用することができます。ほとんどの人がそうだと思うが、Appleは非常にスマートですが、明示的にクラスUICollectionViewGridLayoutに名前を付けていません。より良いが、機能のこのタイプを説明し、より一般的な用語のフローレイアウトを、使用します。それは一つのセルずつ配置することにより、独自のレイアウトを作成するために、必要なときに、水平または垂直列を挿入文字。カスタム、細胞の大きさと間隔と方向をスクロールすることによって、フローレイアウトセルのレイアウトは、単一の行または列であってもよいです。実際には、のUITableViewのレイアウトは、フローレイアウトの特別なケースとして想像することができます。

あなたはサブクラスUICollectionViewLayoutを書くために準備をする前に、あなたはあなたの心のレイアウトを実装UICollectionViewFlowLayoutを使用することができるかどうかを自問する必要があります。このクラスは、カスタマイズが非常に簡単で、一歩前進を取って自分自身を継承するためにカスタマイズすることができます。興味を持って見て、この記事

細胞および他のビュー

任意のレイアウトに適合させるために、コレクションは、類似の確立を表示するが、ビューの階層(ビュー階層)のテーブルビューよりも柔軟。いつものように、あなたの主な内容は、セルに表示され、任意のセルをセクションに分類することができます。細胞のコレクションビューはUICollectionViewCellのサブクラスでなければなりません。補足意見と装飾ビュー:細胞に加えて、コレクションビューの追加管理は、2つのビューがあります。

セクションヘッダテーブルビューとフッタービューに対応する補助ビューのコレクションビュー。細胞のように、彼らは、データソースオブジェクトによって駆動されたコンテンツです。しかし、使用およびテーブルビューは、同じ、補助ビューは、必ずしもヘッダーまたはフッタービューとしないで、その数及び配置は、完全にレイアウトの位置によって制御されます。

純粋に装飾としてデコレーションを望みます。彼らは完全にレイアウトオブジェクト、レイアウトオブジェクト管理、彼らはデータソースからその内容を得ることはありません。レイアウトオブジェクトは装飾ビューを指定する場合には、コレクションビューが自動的に作成され、時間がかかり、そのアプリケーションのレイアウトオブジェクトが提供するためのレイアウトパラメータ。あなたは何もカスタムビューを用意する必要はありません。

補足意見や装飾ビューはUICollectionResuableViewのサブクラスでなければなりません。新しいインスタンスを作成することができたときにデータソースが再利用プールからのラインから彼をできるようにするときようにしますが、コレクションビューに登録する必要が使用している各ビュークラスのレイアウト。あなたがInterface Builderのを使用している場合は、コレクションビューのコレクションビューに登録セルを完了するために、セル内のビジュアルエディタをドラッグ&ドロップすることができます。同じ方法はまた、あなたがUICollectionViewFlowLayoutを使用することを提供し、補助的なビューで使用することができます。またはregisterNib:ビュークラスの手動登録する方法ではない場合、あなただけのregisterClassを呼び出すことができます。あなたは、のviewDidLoadでこれらの操作を行う必要があります。

 

カスタムレイアウト

以来、非常に意味のある定義されたコレクションビューのレイアウトの一例として、我々は典型的なカレンダーアプリケーションの週(週)ビューを想像するかもしれません。週の毎日が列に表示される週に一度カレンダー表示。各カレンダーのイベントは、私たちのコレクションビュー内のセルに開始日と期間に代わってイベントの位置とサイズを表示します。

コレクションビューのレイアウトの2種類が一般的にあります。

1.レイアウト計算は、コンテンツとは無関係です。これは、あなたが知っているとのUITableViewのようにこれらのケースをUICollectionViewFlowLayoutものです。ディスプレイの内容と外観上の各セル位置ではないが、すべてのセル内容の順序に基づいて表示順序。あなたは、一例として、デフォルトのフローレイアウトを設定することができます。各セルは、セルに基づいています(次の行の先頭から、または十分なスペースがない場合)の前に置かれています。レイアウトオブジェクトは、レイアウトを計算するために、実際のデータにアクセスすることはできません。

コンテンツのレイアウトに基づい2.計算。私たちのカレンダービューでは、このようなタイプの一例です。レイアウトのイベント表示の開始時刻と終了時刻を計算するためにはコレクションビューは、データソースへの直接アクセスを必要とするオブジェクト。多くの場合、現在表示さセルのデータレイアウトオブジェクトを削除する必要があるが、また、現在のセルがすべてのデータレコードから見える意思決定の一部を削除する必要はありませんのみ。

レイアウトプロパティの矩形内のアクセス可能なセルオブジェクト場合我々​​のカレンダーの例では、それは、必要な時間窓内に配置されているかを決定するために、すべてのイベント・データ・ソースを反復しなければなりません。(すべてのセルがグリッドの同一の大きさが想定されている)の長方形内のセル・インデックス・パスを計算するのに十分であり、算出されたフローレイアウトに比較していくつかの比較的簡単な、独立したデータソース。

カスタムレイアウトクラスを記述する必要があると同時に、カスタムUICollectionViewFlowLayoutを使用できないことを示唆しているコンテンツに依存レイアウトがある場合。だから、私たちが何をすべきかです。

UICollectionViewLayout文書は、サブクラスのメソッドを書き換える必要が一覧表示されます。

collectionViewContentSize

コレクション以来、その内容は知りませんでした表示なので、情報を提供する最初のレイアウトが正しくスクロールを管理するには、このコレクションビューのサイズスクロール領域です。レイアウトオブジェクトは、補助ビューと装飾ビューなど、現時点ではそのコンテンツの合計サイズを計算する必要があります。最も古典的なコレクションビューが(UICollectionViewFlowLayout同じように)一方の軸方向のスクロールに限定されるものではあるが、これは必要ではないことに留意されたいです。

私たちのカレンダーの例では、垂直方向のビューをスクロールします。我々は、高さの一日の内容を表示する垂直方向のスペース、上の100ポイントを取るために1時間を望んでいた場合たとえば、2400ポイントです。私たちは私たちのコレクションビューのみ週表示できることを意味し、水平方向にスクロールすることができないことに注意してください。より多くの星の暦の間、ページのことができるようにするために、我々は、複数のコレクションビュー(週1)を使用して(あなたがUIPageViewControllerを使用することができます)独立した(改ページ)のビューをスクロールすることができ、またはコレクションビューに固執し、十分な大きさを返します。ユーザーになるだろう、コンテンツの幅は、両方の方向にスライドして自由に感じます。

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

明確にするために、私はモデルの非常にシンプルなレイアウトを選んだ:週同じ日数、毎日同じ長さと仮定すると、

それは日0-6の数字で表示されます。実際のカレンダープログラムでは、レイアウトは、その計算のための日付NSCalendar大規模な使用に基づいて行われます。

 

layoutAttributesForElementsInRect:

これは、どのような方法で最も重要なクラスのレイアウトで、そしておそらく最も簡単な方法を混乱ながら。コレクションビューは、このメソッドを呼び出し、それ自身の過去の直交座標系を渡します。この長方形は、あなたがあなたにどんな四角形それを処理する準備ができてする必要があります(つまり、その境界である)このビューの可視矩形領域を表します。

你的实现必须返回一个包含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

おすすめ

転載: blog.csdn.net/weixin_34253126/article/details/93462287