国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計 > 正文

iOS系列譯文:自定義CollectionView布局

2019-11-14 20:31:56
字體:
供稿:網(wǎng)友

原文出處: Ole Begemann   譯文出處: 黃愛武(@answer-huang)。歡迎加入技術(shù)翻譯小組

UICollectionView在iOS6中第一次被介紹,也是UIKit視圖類中的一顆新星。它和UITableView共享API設(shè)計,但也在UITableView上做了一些擴(kuò)展。UICollectionView最強(qiáng)大、同時顯著超出UITableView的特色就是其完全靈活的布局結(jié)構(gòu)。在這篇文章中,我們將會實現(xiàn)一個相當(dāng)復(fù)雜的自定義collection view布局,并且順便討論一下這個類設(shè)計的重要部分。項目的示例代碼在GitHub上。

布局對象

UITableView和UICollectionView都是由data-source和delegate驅(qū)動的。他們?yōu)槠滹@示的子視圖集扮演為愚蠢的容器(dumb containers),對他們真實的內(nèi)容(contents)毫不知情。

UICollectionView進(jìn)一步抽象了。它將其子視圖的位置,大小和外觀的控制權(quán)委托給一個單獨的布局對象。通過提供一個自定義布局對象,你幾乎可以實現(xiàn)任何你能想象到的布局。布局繼承自UICollectionViewLayout這個抽象基類。iOS6中以UICollectionViewFlowLayout類的形式提出了一個具體的布局實現(xiàn)。

flow layout可以被用來實現(xiàn)一個標(biāo)準(zhǔn)的grid view,這可能是在collection view中最常見的使用案例了。盡管大多數(shù)人都這么想,但是Apple很聰明,沒有明確的命名這個類為UICollectionViewGridLayout。而使用了更為通用的術(shù)語flow layout,這更好的描述了該類的能力:它通過一個接一個的放置cell來建立自己的布局,當(dāng)需要的時候,插入橫排或豎排的分欄符。通過自定義滾動方向,大小和cell之間的間距,flow layout也可以在單行或單列中布局cell。實際上,UITableView的布局可以想象成flow layout的一種特殊情況。

在你準(zhǔn)備自己寫一個UICollectionViewLayout的子類之前,你需要問你自己,你是否能夠使用UICollectionViewFlowLayout實現(xiàn)你心里的布局。這個類是很容易定制的,并且可以繼承本身進(jìn)行近一步的定制。感興趣的看這篇文章

Cells和其他Views

為了適應(yīng)任意布局,collection view建立一個了類似,但比table view更靈活的視圖層級(view hierarchy)。像往常一樣,你的主要內(nèi)容顯示在cell中,cell可以被任意分組到section中。Collection view的cells必須是UICollectionViewCell的子類。除了cells,collection view額外管理著兩種視圖:supplementary views和decoration views。

collection view中的Supplementary views相當(dāng)于table view的section header和footer views。像cells一樣,他們的內(nèi)容都由數(shù)據(jù)源對象驅(qū)動。然而,和table view中用法不一樣,supplementary view并不一定會作為header或footer view;他們的數(shù)量和放置的位置完全由布局控制。

Decoration views純粹為一個裝飾品。他們完全屬于布局對象,并被布局對象管理,他們并不從數(shù)據(jù)源獲取他們的contents。當(dāng)布局對象指定它需要一個decoration view的時候,collection view會自動創(chuàng)建,并為其應(yīng)用布局對象提供的布局參數(shù)。并不需要準(zhǔn)備任何自定義視圖的內(nèi)容。

Supplementary views和decoration views必須是UICollectionResuableView的子類。每個你布局所使用的視圖類都需要在collection view中注冊,這樣當(dāng)data source讓他從reuse pool中出列時,它才能夠創(chuàng)建新的實例。如果你是使用的Interface Builder,則可以通過在可視編輯器中拖拽一個cell到collection view上完成cell在collection view中的注冊。同樣的方法也可以用在supplementary view上,前提是你使用了UICollectionViewFlowLayout。如果沒有,你只能通過調(diào)用registerClass:或者registerNib:方法手動注冊視圖類了。你需要在viewDidLoad中做這些操作。

 

自定義布局

作為一個非常有意義的自定義collection view布局的例子,我們不妨設(shè)想一個典型的日歷應(yīng)用程序中的周(week)視圖。日歷一次顯示一周,星期中的每一天顯示在列中。每一個日歷事件將會在我們的collection view中以一個cell顯示,位置和大小代表事件起始日期時間和持續(xù)時間。

一般有兩種類型的collection view布局:

1.獨立于內(nèi)容的布局計算。這正是你所知道的像UITableView和UICollectionViewFlowLayout這些情況。每個cell的位置和外觀不是基于其顯示的內(nèi)容,但所有cell的顯示順序是基于內(nèi)容的順序。可以把默認(rèn)的flow layout做為例子。每個cell都基于前一個cell放置(或者如果沒有足夠的空間,則從下一行開始)。布局對象不必訪問實際數(shù)據(jù)來計算布局。

2.基于內(nèi)容的布局計算。我們的日歷視圖正是這樣類型的例子。為了計算顯示事件的起始和結(jié)束時間,布局對象需要直接訪問collection view的數(shù)據(jù)源。在很多情況下,布局對象不僅需要取出當(dāng)前可見cell的數(shù)據(jù),還需要從所有記錄中取出一些決定當(dāng)前哪些cell可見的數(shù)據(jù)。

在我們的日歷示例中,布局對象如果訪問某一個矩形內(nèi)cells的屬性,那就必須迭代數(shù)據(jù)源提供的所有事件來決定哪些位于要求的時間窗口中。 與一些相對簡單,數(shù)據(jù)源獨立計算的flow layout比起來,這足夠計算出cell在一個矩形內(nèi)的index paths了(假設(shè)網(wǎng)格中所有cells的大小都一樣)。

如果有一個依賴內(nèi)容的布局,那就是暗示你需要寫自定義的布局類了,同時不能使用自定義的UICollectionViewFlowLayout。所以這正是我們需要做的事情。

UICollectionViewLayout的文檔列出了子類需要重寫的方法。

collectionViewContentSize

由于collection view對它的content并不知情,所以布局首先要提供的信息就是滾動區(qū)域大小,這樣collection view才能正確的管理滾動。布局對象必須在此時計算它內(nèi)容的總大小,包括supplementary views和decoration views。注意,盡管大多數(shù)經(jīng)典的collection view限制在一個軸方向上滾動(正如UICollectionViewFlowLayout一樣),但這不是必須的。

在我們的日歷示例中,我們想要視圖垂直的滾動。比如,如果我們想要在垂直空間上一個小時占去100點,這樣顯示一整天的內(nèi)容高度就是2400點。注意,我們不能夠水平滾動,這就意味這我們collection view只能顯示一周。為了能夠在日歷中的多個星期間分頁,我們可以在一個獨立(分頁)的scroll view(可以使用UipageViewController)中使用多個collection view(一周一個),或者堅持使用一個collection view并且返回足夠大的內(nèi)容寬度,這會使得用戶感覺在兩個方向上滑動自由。

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

為了清楚起見,我選擇布局在一個非常簡單模型上:假定每周天數(shù)相同,每天時長相同,

也就是說天數(shù)用0-6表示。在一個真實的日歷程序中,布局將會為自己的計算大量使用基于NSCalendar的日期。

 

layoutAttributesForElementsInRect:

這是任何布局類中最重要的方法了,同時可能也是最容易讓人迷惑的方法。collection view調(diào)用這個方法并傳遞一個自身坐標(biāo)系統(tǒng)中的矩形過去。這個矩形代表了這個視圖的可見矩形區(qū)域(也就是它的bounds),你需要準(zhǔn)備好處理傳給你的任何矩形。

你的實現(xiàn)必須返回一個包含UICollectionViewLayoutAttributes對象的數(shù)組,為每一個cell包含這樣的一個對象,supplementary view或decoration view在矩形區(qū)域內(nèi)是可見的。UICollectionViewLayoutAttributes類包含了collection view內(nèi)item的所有相關(guān)布局屬性。默認(rèn)情況下,這個類包含frame,center,size,transform3D,alpha,zIndex屬性(PRoperties),和hidden特性(attributes)。如果你的布局想要控制其他視圖的屬性(比如,背景顏色),你可以建一個UICollectionViewLayoutAttributes的子類,然后加上你自己的屬性。

布局屬性對象通過indexPath屬性和他們對應(yīng)的cell,supplementary view或者decoration view關(guān)聯(lián)在一起。collection view為所有items從布局對象中請求到布局屬性后,它將會實例化所有視圖,并將對應(yīng)的屬性應(yīng)用到每個視圖上去。

注意!這個方法涉及到所有類型的視圖,也就是cell,supplementary views和decoration views。一個幼稚的實現(xiàn)可能會選擇忽略傳入的矩形,并且為collection view中的所有視圖返回布局屬性。在原型設(shè)計和開發(fā)布局階段,這是一個有效的方法。但是,這將對性能產(chǎn)生非常壞的影響,特別是可見cell遠(yuǎn)少于所有cell數(shù)量的時候,collection view和布局對象將會為那些不可見的視圖做額外不必要的工作。

你的實現(xiàn)需要做這幾步:

1.創(chuàng)建一個空的mutable數(shù)組來存放所有的布局屬性。

2.確定index paths中哪些cells的frame完全或部分位于矩形中。這個計算需要你從collection view的數(shù)據(jù)源中取出你需要顯示的數(shù)據(jù)。然后在循環(huán)中調(diào)用你實現(xiàn)的layoutAttributesForItemAtIndexPath:方法為每個index path創(chuàng)建并配置一個合適的布局屬性對象,并將每個對象添加到數(shù)組中。

3.如果你的布局包含supplementary views,計算矩形內(nèi)可見supplementary view的index paths。在循環(huán)中調(diào)用你實現(xiàn)的layoutAttributesForSupplementaryViewOfKind:atIndexPath:,并且將這些對象加到數(shù)組中。通過為kind參數(shù)傳遞你選擇的不同字符,你可以區(qū)分出不同種類的supplementary views(比如headers和footers)。當(dāng)需要創(chuàng)建視圖時,collection view會將kind字符傳回到你的數(shù)據(jù)源。記住supplementary和decoration views的數(shù)量和種類完全由布局控制。你不會受到headers和footers的限制。

4.如果布局包含decoration views,計算矩形內(nèi)可見decoration views的index paths。在循環(huán)中調(diào)用你實現(xiàn)的layoutAttributesForDecorationViewOfKind:atIndexPath:,并且將這些對象加到數(shù)組中。

5.返回數(shù)組。

我們自定義的布局沒有使用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向布局對象請求布局屬性,而非所有可見的對象。這就是當(dāng)其他三個方法開始起作用時,你實現(xiàn)的layoutAttributesForItemAtIndexPath:需要創(chuàng)建并返回一個單獨的布局屬性對象,這樣才能正確的格式化傳給你的index path所對應(yīng)的cell。

你可以通過調(diào)用 +[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:]這個方法,然后根據(jù)index path修改屬性。為了得到需要顯示在這個index path內(nèi)的數(shù)據(jù),你可能需要訪問collection view的數(shù)據(jù)源。到目前為止,至少確保設(shè)置了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;
 
}

如果你正在使用自動布局,你可能會感到驚訝,我們正在直接修改布局參數(shù)的frame屬性,而不是和約束共事,但這正是UICollectionViewLayout的工作。盡管你可能使用自動布局來定義collection view的frame和它內(nèi)部每個cell的布局,但cells的frames還是需要通過老式的方法計算出來。

類似的,layoutAttributesForSupplementaryViewOfKind:atIndexPath: 和 layoutAttributesForDecorationViewOfKind:atIndexPath:方法分別需要為supplementary和decoration views做相同的事。只有你的布局包含這樣的視圖你才需要實現(xiàn)這兩個方法。UICollectionViewLayoutAttributes包含另外兩個工廠方法,+layoutAttributesForSupplementaryViewOfKind:withIndexPath: 和 +layoutAttributesForDecorationViewOfKind:withIndexPath:,他們是用來創(chuàng)建正確的布局屬性對象。

shouldInvalidateLayoutForBoundsChange:

最后,當(dāng)collection view的bounds改變時,布局需要告訴collection view是否需要重新計算布局。我的猜想是:當(dāng)collection view改變大小時,大多數(shù)布局會被作廢,比如設(shè)備旋轉(zhuǎn)的時候。因此,一個幼稚的實現(xiàn)可能只會簡單的返回YES。雖然實現(xiàn)功能很重要,但是scroll view的bounds在滾動時也會改變,這意味著你的布局每秒會被丟棄多次。根據(jù)計算的復(fù)雜性判斷,這將會對性能產(chǎn)生很大的影響。

當(dāng)collection view的寬度改變時,我們自定義的布局必須被丟棄,但這滾動并不會影響到布局。幸運(yùn)的是,collection view將它的新bounds傳給shouldInvalidateLayoutForBoundsChange: method。這樣我們便能比較視圖當(dāng)前的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自帶了一套非常漂亮的插入和刪除動畫。但是當(dāng)為UICollectionView增加和刪除cell定義動畫功能時,UIKit工程師遇到這樣一個問題:如果collection view的布局是完全可變的,那么預(yù)先定義好的動畫就沒辦法和開發(fā)者自定義的布局很好的融合。他們提出了一個優(yōu)雅的方法:當(dāng)一個cell(或者supplementary或者decoration view)被插入到collection view中時,collection view不僅向其布局請求cell正常狀態(tài)下的布局屬性,同時還請求其初始的布局屬性,比如,需要在開始有插入動畫的cell。collection view會簡單的創(chuàng)建一個animation block,并在這個block中,將所有cell的屬性從初始(initial)狀態(tài)改變到常態(tài)(normal)。

通過提供不同的初始布局屬性,你可以完全自定義插入動畫。比如,設(shè)置初始的alpha為0將會產(chǎn)生一個淡入的動畫。同時設(shè)置一個平移和縮放將會產(chǎn)生移動縮放的效果。

同樣的原理應(yīng)用到刪除上,這次動畫是從常態(tài)到一系列你設(shè)置的最終布局屬性。這些都是你需要在布局類中為initial或final布局參數(shù)實現(xiàn)的方法.

initialLayoutAttributesForAppearingitemAtIndexPath:

initialLayoutAttributesForAppearingSupplementaryElementOfKind:atIndexPath:

initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:

finalLayoutAttributesForDisappearingItemAtIndexPath:

finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:

finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:

布局間切換

可以通過類似的方式將一個collection view布局動態(tài)的切換到另外一個布局。當(dāng)發(fā)送一個setCollectionViewLayout:animated:消息時,collection view會為cells在新的布局中查詢新的布局參數(shù),然后動態(tài)的將每個cell(通過index path在新舊布局中判斷出相同的cell)從舊參數(shù)變換到新的布局參數(shù)。你不需要做任何事情。

結(jié)論

根據(jù)自定義collection view布局的復(fù)雜性,寫一個通常很不容易。確切的說,本質(zhì)上這和從頭寫一個完整的實現(xiàn)相同布局自定義視圖類一樣困難了。因為所涉及的計算需要確定哪些子視圖當(dāng)前是可見的,以及他們的位置。盡管如此,使用UICollectionView還是給你帶來了一些很好的效果,比如cell重用,自動支持動畫,更不要提整潔的獨立布局,子視圖管理,以及數(shù)據(jù)提供架構(gòu)規(guī)定(data preparation its architecture prescribes.)。

自定義collection view布局也是向輕量級view controller邁出很好的一步,正如你的view controller不要包含任何布局代碼。正如Chris的文章中解釋的一樣,將這一切和一個獨立的datasource類結(jié)合在一起,collection view的視圖控制器將很難再包含任何代碼。

每當(dāng)我使用UICollectionView的時候,我被其簡潔的設(shè)計所折服。對于一個有經(jīng)驗的Apple工程師,為了想出如此靈活的類,很可能需要首先考慮NSTableView和UITableView。


發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 陵川县| 新郑市| 宣恩县| 武邑县| 日照市| 海门市| 九龙县| 台江县| 海城市| 贵溪市| 乌兰察布市| 临泽县| 正镶白旗| 郓城县| 江西省| 上犹县| 阜南县| 焦作市| 太康县| 通化市| 凤翔县| 虹口区| 清涧县| 衡阳县| 大城县| 资中县| 孟津县| 临武县| 昂仁县| 永清县| 喀喇| 禹州市| 遂昌县| 凤山县| 同仁县| 西峡县| 巴彦淖尔市| 抚宁县| 洪湖市| 浏阳市| 雅安市|