UICollectionView 在 iOS6 中第一次被引入,也是 UIKit 視圖類中的一顆新星。它和 UITableView 共享一套 API 設計,但也在 UITableView 上作了一些擴展。UICollectionView 最強大、同時顯著超出 UITableView 的特點就是其徹底靈活的佈局結構。在這篇文章中,咱們將會實現一個至關複雜的自定義 collection view 佈局,而且順便討論一下這個類設計的重要部分。項目的示例代碼在 GitHub 上。html
UITableView 和 UICollectionView 都是 data-source 和 delegate 驅動的。它們在顯示其子視圖集的過程當中僅扮演容器角色(dumb containers
),且對子視圖集真正的內容絕不知情。ios
UICollectionView
在此之上進行了進一步抽象。它將其子視圖的位置,大小和外觀的控制權委託給一個單獨的佈局對象。經過提供一個自定義佈局對象,你幾乎能夠實現任何你能想象到的佈局。佈局繼承自 UICollectionViewLayout
抽象基類。iOS6 中以 UICollectionViewFlowLayout
類的形式提出了一個具體的佈局實現。git
咱們可使用 flow layout 實現一個標準的 grid view,這多是在 collection view 中最多見的使用案例了。儘管大多數人都這麼想,可是 Apple 很聰明,沒有明確的命名這個類爲 UICollectionViewGridLayout
,而使用了更爲通用的術語 flow layout,更好的描述了該類的功能:它經過一個接一個的放置 cell 來創建本身的佈局,當須要的時候,插入橫排或豎排的分欄符。經過自定義滾動方向,大小和 cell 之間的間距,flow layout 也能夠在單行或單列中佈局 cell。實際上,UITableView
的佈局能夠想象成 flow layout 的一種特殊狀況。github
在你準備本身寫一個 UICollectionViewLayout
的子類以前,你須要問你本身,你是否可以使用 UICollectionViewFlowLayout
實現你內心的佈局。這個類是很容易定製的,而且能夠繼承自己進行進一步的定製。感興趣的看這篇文章。數組
爲了適應任意佈局,collection view 創建了一個相似、但比 table view 更靈活的視圖層級(view hierarchy)。像往常同樣,你的主要內容顯示在 cell 中,cell 能夠被任意分組到 section 中。Collection view 的 cell 必須是 UICollectionViewCell
的子類。除了 cell,collection view 額外管理着兩種視圖:supplementary views 和 decoration views。bash
collection view 中的 Supplementary views 至關於 table view 的 section header 和 footer views。像 cells 同樣,他們的內容都由數據源對象驅動。然而和 table view 中用法不同,supplementary view 並不必定會做爲 header 或 footer view;他們的數量和放置的位置徹底由佈局控制。架構
Decoration views 純粹爲一個裝飾品。他們徹底屬於佈局對象,並被佈局對象管理,他們並不從 data source 獲取的 contents。當佈局對象指定須要一個 decoration view 的時候,collection view 會自動建立,並將佈局對象提供的佈局參數應用到上面去。並不須要爲自定義視圖準備任何內容。app
Supplementary views 和 decoration views 必須是 UICollectionReusableView 的子類。佈局使用的每一個視圖類都須要在 collection view 中註冊,這樣當 data source 讓它們從 reuse pool 中出列時,它們纔可以建立新的實例。若是你是使用的 Interface Builder,則能夠經過在可視編輯器中拖拽一個 cell 到 collection view 上完成 cell 在 collection view 中的註冊。一樣的方法也能夠用在 supplementary view 上,前提是你使用了 UICollectionViewFlowLayout
。若是沒有,你只能經過調用 registerClass:
或者 registerNib:
方法手動註冊視圖類了。你須要在 viewDidLoad
中作這些操做。編輯器
做爲一個很是有意義的自定義 collection view 佈局的例子,咱們不妨設想一個典型的日曆應用程序中的周 (week) 視圖。日曆一次顯示一週,星期中的每一天顯示在列中。每個日曆事件將會在咱們的 collection view 中以一個 cell 顯示,位置和大小表明事件起始日期時間和持續時間。ide
通常有兩種類型的 collection view 佈局:
1.獨立於內容的佈局計算。這正是你所知道的像 UITableView 和 UICollectionViewFlowLayout 這些狀況。每一個 cell 的位置和外觀不是基於其顯示的內容,但全部 cell 的顯示順序是基於內容的順序。能夠把默認的 flow layout 作爲例子。每一個 cell 都基於前一個 cell 放置(或者若是沒有足夠的空間,則從下一行開始)。佈局對象沒必要訪問實際數據來計算佈局。
2.基於內容的佈局計算。咱們的日曆視圖正是這樣類型的例子。爲了計算顯示事件的起始和結束時間,佈局對象須要直接訪問 collection view 的數據源。在不少狀況下,佈局對象不只須要取出當前可見 cell 的數據,還須要從全部記錄中取出一些決定當前哪些 cell 可見的數據。
在咱們的日曆示例中,佈局對象若是訪問某一個矩形內 cells 的屬性,那就必須迭代數據源提供的全部事件來決定哪些位於要求的時間窗口中。 與一些相對簡單,數據源獨立計算的 flow layout 比起來,這足夠計算出 cell 在一個矩形內的 index paths 了(假設網格中全部cells的大小都同樣)。
若是有一個依賴內容的佈局,那就是暗示你須要寫自定義的佈局類了,同時不能使用自定義的 UICollectionViewFlowLayout
,因此這正是咱們須要作的事情。
UICollectionViewLayout的文檔列出了子類須要重寫的方法。
因爲 collection view 對它的 content 並不知情,因此佈局首先要提供的信息就是滾動區域大小,這樣 collection view 才能正確的管理滾動。佈局對象必須在此時計算它內容的總大小,包括 supplementary views 和 decoration views。注意,儘管大多數經典的 collection view 限制在一個軸方向上滾動(正如 UICollectionViewFlowLayout
同樣),但這不是必須的。
在咱們的日曆示例中,咱們想要視圖垂直的滾動。好比,若是咱們想要在垂直空間上一個小時佔去 100 點,這樣顯示一成天的內容高度就是 2400 點。注意,咱們不可以水平滾動,這就意味這咱們 collection view 只能顯示一週。爲了可以在日曆中的多個星期間分頁,咱們能夠在一個獨立(分頁)的 scroll view (可使用 UIPageViewController)中使用多個collection view(一週一個),或者堅持使用一個 collection view 而且返回足夠大的內容寬度,這會使得用戶感受在兩個方向上滑動自由。
- (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 表示。在一個真實的日曆程序中,佈局將會爲本身的計算大量使用基於 NSCalendaar
的日期。
這是任何佈局類中最重要的方法了,同時可能也是最容易讓人迷惑的方法。collection view 調用這個方法並傳遞一個自身座標系統中的矩形過去。這個矩形表明了這個視圖的可見矩形區域(也就是它的 bounds ),你須要準備好處理傳給你的任何矩形。
你的實現必須返回一個包含 UICollectionViewLayoutAttributes
對象的數組,爲每個 cell 包含一個這樣的對象,supplementary view 或 decoration view 在矩形區域內是可見的。UICollectionViewLayoutAttributes
類包含了 collection view 內 item 的全部相關佈局屬性。默認狀況下,這個類包含 frame
,center
,size
,transform3D
,alpha
,zIndex
和 hidden
屬性。若是你的佈局想要控制其餘視圖的屬性(好比背景顏色),你能夠建一個 UICollectionViewLayoutAttributes
的子類,而後加上你本身的屬性。
佈局屬性對象 (layout attributes objects) 經過 indexPath
屬性和他們對應的 cell,supplementary view 或者 decoration view 關聯在一塊兒。collection view 爲全部 items 從佈局對象中請求到佈局屬性後,它將會實例化全部視圖,並將對應的屬性應用到每一個視圖上去。
注意!這個方法涉及到全部類型的視圖,也就是 cell,supplementary views 和 decoration views。一個幼稚的實現可能會選擇忽略傳入的矩形,而且爲 collection view 中的全部視圖返回佈局屬性。在原型設計和開發佈局階段,這是一個有效的方法。可是,這將對性能產生很是壞的影響,特別是可見 cell 遠少於全部 cell 數量的時候,collection view 和佈局對象將會爲那些不可見的視圖作額外沒必要要的工做。
你的實現須要作這幾步:
建立一個空的可變數組來存放全部的佈局屬性。
肯定 index paths 中哪些 cells 的 frame 徹底或部分位於矩形中。這個計算須要你從 collection view 的數據源中取出你須要顯示的數據。而後在循環中調用你實現的 layoutAttributesForItemAtIndexPath:
方法爲每一個 index path 建立並配置一個合適的佈局屬性對象,並將每一個對象添加到數組中。
若是你的佈局包含 supplementary views,計算矩形內可見 supplementary view 的 index paths。在循環中調用你實現的 layoutAttributesForSupplementaryViewOfKind:atIndexPath:
,而且將這些對象加到數組中。經過爲 kind 參數傳遞你選擇的不一樣字符,你能夠區分出不一樣種類的supplementary views(好比headers和footers)。當須要建立視圖時,collection view 會將 kind 字符傳回到你的數據源。記住 supplementary 和 decoration views 的數量和種類徹底由佈局控制。你不會受到 headers 和 footers 的限制。
若是佈局包含 decoration views,計算矩形內可見 decoration views 的 index paths。在循環中調用你實現的 layoutAttributesForDecorationViewOfKind:atIndexPath:
,而且將這些對象加到數組中。
返回數組。
咱們自定義的佈局沒有使用 decoration views,可是使用了兩種 supplementary views(column headers和row headers):
- (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 都位於彼此上方。
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
CalendarDataSource *dataSource = self.collectionView.dataSource;
id 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:
,用他們來建立正確的佈局屬性對象。
最後,當 collection view 的 bounds 改變時,佈局須要告訴 collection view 是否須要從新計算佈局。個人猜測是:當 collection view 改變大小時,大多數佈局會被做廢,好比設備旋轉的時候。所以,一個幼稚的實現可能只會簡單的返回 YES。雖然實現功能很重要,可是 scroll view 的 bounds 在滾動時也會改變,這意味着你的佈局每秒會被丟棄屢次。根據計算的複雜性判斷,這將會對性能產生很大的影響。
當 collection view 的寬度改變時,咱們自定義的佈局必須被丟棄,但這滾動並不會影響到佈局。幸運的是,collection view 將它的新 bounds 傳給 shouldInvalidateLayoutForBoundsChange:
方法。這樣咱們便能比較視圖當前的bounds 和新的 bounds 來肯定返回值:
- (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
。
UICollectionView
.UICollectionView
: The Complete Guide, e-book by Ash Furrow.MSCollectionViewCalendarLayout
by Eric Horacek is an excellent and more complete implementation of a custom layout for a week calendar view.