自定義CollectionViewLayout

原文出自: Custom Collection View Layouts 
 
UICollectionView在iOS6中第一次被介紹,也是UIKit視圖類中的一顆新星。它和UITableView共享API設計,但也在UITableView上作了一些擴展。UICollectionView最強大、同時顯著超出UITableView的特點就是其徹底靈活的佈局結構。在這篇文章中,咱們將會實現一個至關複雜的自定義collection view佈局,而且順便討論一下這個類設計的重要部分。項目的示例代碼在 GitHub上。
 
佈局對象
UITableView和UICollectionView都是由 data-source和delegate驅動的。他們爲其顯示的子視圖集扮演爲愚蠢的容器(dumb containers),對他們真實的內容(contents)絕不知情。
 
UICollectionView進一步抽象了。它將其子視圖的位置,大小和外觀的控制權委託給一個單獨的佈局對象。經過提供一個自定義佈局對象,你幾乎能夠實現任何你能想象到的佈局。佈局繼承自UICollectionViewLayout這個抽象基類。iOS6中以UICollectionViewFlowLayout類的形式提出了一個具體的佈局實現。
 
flow layout能夠被用來實現一個標準的grid view,這多是在collection view中最多見的使用案例了。儘管大多數人都這麼想,可是Apple很聰明,沒有明確的命名這個類爲UICollectionViewGridLayout。而使用了更爲通用的術語flow layout,這更好的描述了該類的能力:它經過一個接一個的放置cell來創建本身的佈局,當須要的時候,插入橫排或豎排的分欄符。經過自定義滾動方向,大小和cell之間的間距,flow layout也能夠在單行或單列中佈局cell。實際上,UITableView的佈局能夠想象成flow layout的一種特殊狀況。
 
在你準備本身寫一個UICollectionViewLayout的子類以前,你須要問你本身,你是否可以使用UICollectionViewFlowLayout實現你內心的佈局。這個類是很容易定製的,而且能夠繼承自己進行近一步的定製。感興趣的看 這篇文章
 
Cells和其餘Views
爲了適應任意佈局,collection view創建一個了相似,但比table view更靈活的視圖層級(view hierarchy)。像往常同樣,你的主要內容顯示在cell中,cell能夠被任意分組到section中。Collection view的cells必須是UICollectionViewCell的子類。除了cells,collection view額外管理着兩種視圖:supplementary views和decoration views。
 
collection view中的Supplementary views至關於table view的section header和footer views。像cells同樣,他們的內容都由數據源對象驅動。然而,和table view中用法不同,supplementary view並不必定會做爲header或footer view;他們的數量和放置的位置徹底由佈局控制。
 
Decoration views純粹爲一個裝飾品。他們徹底屬於佈局對象,並被佈局對象管理,他們並不從數據源獲取他們的contents。當佈局對象指定它須要一個decoration view的時候,collection view會自動建立,併爲其應用佈局對象提供的佈局參數。並不須要準備任何自定義視圖的內容。
 
Supplementary views和decoration views必須是UICollectionResuableView的子類。每一個你佈局所使用的視圖類都須要在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顯示,位置和大小表明事件起始日期時間和持續時間。
 
通常有兩種類型的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的文檔列出了子類須要重寫的方法。
 
collectionViewContentSize
因爲collection view對它的content並不知情,因此佈局首先要提供的信息就是滾動區域大小,這樣collection view才能正確的管理滾動。佈局對象必須在此時計算它內容的總大小,包括supplementary views和decoration views。注意,儘管大多數經典的collection view限制在一個軸方向上滾動(正如UICollectionViewFlowLayout同樣),但這不是必須的。
 
在咱們的日曆示例中,咱們想要視圖垂直的滾動。好比,若是咱們想要在垂直空間上一個小時佔去100點,這樣顯示一成天的內容高度就是2400點。注意,咱們不可以水平滾動,這就意味這咱們collection view只能顯示一週。爲了可以在日曆中的多個星期間分頁,咱們能夠在一個獨立(分頁)的scroll view(可使用UIPageViewController)中使用多個collection view(一週一個),或者堅持使用一個collection view而且返回足夠大的內容寬度,這會使得用戶感受在兩個方向上滑動自由。
  1. - (CGSize)collectionViewContentSize 
  2.   
  3.   
  4. // Don't scroll horizontally 
  5.   
  6. CGFloat contentWidth = self.collectionView.bounds.size.width; 
  7.   
  8. // Scroll vertically to display a full day 
  9.   
  10. CGFloat contentHeight = DayHeaderHeight + (HeightPerHour * HoursPerDay); 
  11.   
  12. CGSize contentSize = CGSizeMake(contentWidth, contentHeight); 
  13.   
  14. return contentSize; 
  15.   
 
爲了清楚起見,我選擇佈局在一個很是簡單模型上:假定每週天數相同,天天時長相同,
 
也就是說天數用0-6表示。在一個真實的日曆程序中,佈局將會爲本身的計算大量使用基於NSCalendar的日期。
 
layoutAttributesForElementsInRect:
這是任何佈局類中最重要的方法了,同時可能也是最容易讓人迷惑的方法。collection view調用這個方法並傳遞一個自身座標系統中的矩形過去。這個矩形表明了這個視圖的可見矩形區域(也就是它的bounds),你須要準備好處理傳給你的任何矩形。
 
你的實現必須返回一個包含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. - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 
  2.   
  3.   
  4. NSMutableArray *layoutAttributes = [NSMutableArray array]; 
  5.   
  6. // Cells 
  7.   
  8. // We call a custom helper method -indexPathsOfItemsInRect: here 
  9.   
  10. // which computes the index paths of the cells that should be included 
  11.   
  12. // in rect. 
  13.   
  14. NSArray *visibleIndexPaths = [self indexPathsOfItemsInRect:rect]; 
  15.   
  16. for (NSIndexPath *indexPath in visibleIndexPaths) { 
  17.   
  18. UICollectionViewLayoutAttributes *attributes = 
  19.   
  20. [self layoutAttributesForItemAtIndexPath:indexPath]; 
  21.   
  22. [layoutAttributes addObject:attributes]; 
  23.   
  24.   
  25. // Supplementary views 
  26.   
  27. NSArray *dayHeaderViewIndexPaths = [self indexPathsOfDayHeaderViewsInRect:rect]; 
  28.   
  29. for (NSIndexPath *indexPath in dayHeaderViewIndexPaths) { 
  30.   
  31. UICollectionViewLayoutAttributes *attributes = 
  32.   
  33. [self layoutAttributesForSupplementaryViewOfKind:@"DayHeaderView" 
  34.   
  35. atIndexPath:indexPath]; 
  36.   
  37. [layoutAttributes addObject:attributes]; 
  38.   
  39.   
  40. NSArray *hourHeaderViewIndexPaths = [self indexPathsOfHourHeaderViewsInRect:rect]; 
  41.   
  42. for (NSIndexPath *indexPath in hourHeaderViewIndexPaths) { 
  43.   
  44. UICollectionViewLayoutAttributes *attributes = 
  45.   
  46. [self layoutAttributesForSupplementaryViewOfKind:@"HourHeaderView" 
  47.   
  48. atIndexPath:indexPath]; 
  49.   
  50. [layoutAttributes addObject:attributes]; 
  51.   
  52.   
  53. return layoutAttributes; 
  54.   
 
layoutAttributesFor…IndexPath
有時,collection view會爲某個特殊的cell,supplementary或者decoration view向佈局對象請求佈局屬性,而非全部可見的對象。這就是當其餘三個方法開始起做用時,你實現的layoutAttributesForItemAtIndexPath:須要建立並返回一個單獨的佈局屬性對象,這樣才能正確的格式化傳給你的index path所對應的cell。
 
你能夠經過調用 +[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:]這個方法,而後根據index path修改屬性。爲了獲得須要顯示在這個index path內的數據,你可能須要訪問collection view的數據源。到目前爲止,至少確保設置了frame屬性,除非你全部的cell都位於彼此上方。
  1. - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath 
  2.   
  3.   
  4. CalendarDataSource *dataSource = self.collectionView.dataSource; 
  5.   
  6. id<CalendarEvent> event = [dataSource eventAtIndexPath:indexPath]; 
  7.   
  8. UICollectionViewLayoutAttributes *attributes = 
  9.   
  10. [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; 
  11.   
  12. attributes.frame = [self frameForEvent:event]; 
  13.   
  14. return attributes; 
  15.   
 
若是你正在使用自動佈局,你可能會感到驚訝,咱們正在直接修改佈局參數的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. - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 
  2.   
  3.   
  4. CGRect oldBounds = self.collectionView.bounds; 
  5.   
  6. if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds)) { 
  7.   
  8. return YES; 
  9.   
  10.   
  11. return NO; 
  12.   
 
動畫
插入和刪除
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。
相關文章
相關標籤/搜索