藍懿ios技術交流和心得分享 16.1.30

UICollectionView 在 iOS6 中第一次被引入,也是 UIKit 視圖類中的一顆新星

。它和 UITableView 共享一套 API 設計,但也在 UITableView 上做了一些擴展。UICollectionView 最強大、同時顯著超出 UITableView 的特色就是其完全靈活的佈局結構。在這篇文章中,我們將會實現一個相當複雜的自定義 collection view 佈局,並且順便討論一下這個類設計的重要部分。項目的示例代碼在 GitHub 上。

佈局對象 (Layout Objects)

UITableView 和 UICollectionView 都是 data-source 和 delegate 驅動

的。它們在顯示其子視圖集的過程中僅扮演容器角色(dumb containers),且對子視圖集真正的內容毫不知情。

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 的 cell 必須是 UICollectionViewCell 的子類。除了 cell,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 純粹爲一個裝飾品。他們完全屬於佈局對象,並被佈局對象管理,他們並不從 data source 獲取的 contents。當佈局對象指定需要一個 decoration view 的時候,collection view 會自動創建,並將佈局對象提供的佈局參數應用到上面去。並不需要爲自定義視圖準備任何內容。

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 顯示,位置和大小代表事件起始日期時間和持續時間。

一般有兩種類型的 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 並且返回足夠大的內容寬度,這會使得用戶感覺在兩個方向上滑動自由。

- (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 的日期。

layoutAttributesForElementsInRect:

這是任何佈局類中最重要的方法了,同時可能也是最容易讓人迷惑的方法。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 和佈局對象將會爲那些不可見的視圖做額外不必要的工作。

你的實現需要做這幾步:

  1. 創建一個空的可變數組來存放所有的佈局屬性。

  • 確定 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; }

layoutAttributesFor…IndexPath

有時,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:,用他們來創建正確的佈局屬性對象。

shouldInvalidateLayoutForBoundsChange:

最後,當 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

擴展閱讀

  • Collection View Programming Guide

.

  • NSHipster on 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.

學習ios  重要還是要理清楚思路  在做或者看老師代碼的時候 自己多想想爲什麼  不要自己看着就抄       另外還是要推薦一下 藍懿IOS這個培訓機構  和劉國斌老師劉國斌老師還是很有名氣的,聽朋友說劉老師成立了藍懿iOS,,老師講課方式很獨特,能夠儘量讓每個人都能弄明白,有的比較難懂的地方,如果有的地方還是不懂得話,老師會換個其它方法再講解,這對於我們這些學習iOS的同學是非常好的,多種方式的講解會理解得更全面,這個必須得給個贊,嘻嘻,還有就是這裏的學習環境很好,很安靜,可以很安心的學習,安靜的環境是學習的基礎,小班講課,每個班20幾個學生,學習氛圍非常好,每天都學到9點多才離開教室,練習的時間很充裕,而且如果在練習的過程中有什麼困難,隨時可以向老師求助,不像其它機構,通過視頻教學,有的甚至學完之後都看不到講師本人,問點問題都不方便,這就是藍懿與其它機構的區別,相信在劉國斌老師的細心指導下,每個藍懿學員都能找到滿意的工作,加油!

  •                                                                   寫博客第一百一十二天;

                                                                             qq :565803433

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章