UICollectionView 在 iOS6 中第一次被引入,也是 UIKit視圖類中的一顆新星弥奸。它和 UITableView 共享一套 API 設(shè)計(jì)蘑拯,但也在 UITableView 上做了一些擴(kuò)展厌衙。UICollectionView 最強(qiáng)大辜纲、同時(shí)顯著超出 UITableView 的特色就是其完全靈活的布局結(jié)構(gòu)笨觅。在這篇文章中,我們將會(huì)實(shí)現(xiàn)一個(gè)相當(dāng)復(fù)雜的自定義 collection view 布局耕腾,并且順便討論一下這個(gè)類設(shè)計(jì)的重要部分屋摇。項(xiàng)目的示例代碼在GitHub上。
布局對(duì)象 (Layout Objects)
UITableView 和 UICollectionView 都是data-source 和 delegate 驅(qū)動(dòng)的幽邓。它們?cè)陲@示其子視圖集的過(guò)程中僅扮演容器角色(dumb containers),且對(duì)子視圖集真正的內(nèi)容毫不知情火脉。
UICollectionView在此之上進(jìn)行了進(jìn)一步抽象牵舵。它將其子視圖的位置,大小和外觀的控制權(quán)委托給一個(gè)單獨(dú)的布局對(duì)象倦挂。通過(guò)提供一個(gè)自定義布局對(duì)象畸颅,你幾乎可以實(shí)現(xiàn)任何你能想象到的布局。布局繼承自UICollectionViewLayout抽象基類方援。iOS6 中以UICollectionViewFlowLayout類的形式提出了一個(gè)具體的布局實(shí)現(xiàn)没炒。
我們可以使用 flow layout 實(shí)現(xiàn)一個(gè)標(biāo)準(zhǔn)的 grid view,這可能是在 collection view 中最常見(jiàn)的使用案例了犯戏。盡管大多數(shù)人都這么想送火,但是 Apple 很聰明,沒(méi)有明確的命名這個(gè)類為UICollectionViewGridLayout先匪,而使用了更為通用的術(shù)語(yǔ) flow layout种吸,更好的描述了該類的功能:它通過(guò)一個(gè)接一個(gè)的放置 cell 來(lái)建立自己的布局,當(dāng)需要的時(shí)候呀非,插入橫排或豎排的分欄符坚俗。通過(guò)自定義滾動(dòng)方向,大小和 cell 之間的間距岸裙,flow layout 也可以在單行或單列中布局 cell猖败。實(shí)際上,UITableView的布局可以想象成 flow layout 的一種特殊情況降允。
在你準(zhǔn)備自己寫(xiě)一個(gè)UICollectionViewLayout的子類之前恩闻,你需要問(wèn)你自己,你是否能夠使用UICollectionViewFlowLayout實(shí)現(xiàn)你心里的布局拟糕。這個(gè)類是很容易定制的判呕,并且可以繼承本身進(jìn)行近一步的定制倦踢。感興趣的看這篇文章。
Cells 和其他 Views
為了適應(yīng)任意布局侠草,collection view 建立一個(gè)了類似辱挥、但比 table view 更靈活的視圖層級(jí)(view hierarchy)。像往常一樣边涕,你的主要內(nèi)容顯示在 cell 中晤碘,cell 可以被任意分組到 section 中。Collection view 的 cell 必須是UICollectionViewCell的子類功蜓。除了 cell园爷,collection view 額外管理著兩種視圖:supplementary views 和 decoration views。
collection view 中的Supplementary views相當(dāng)于 table view 的 section header 和 footer views式撼。像 cells 一樣童社,他們的內(nèi)容都由數(shù)據(jù)源對(duì)象驅(qū)動(dòng)。然而和 table view 中用法不一樣著隆,supplementary view 并不一定會(huì)作為 header 或 footer view扰楼;他們的數(shù)量和放置的位置完全由布局控制。
Decoration views純粹為一個(gè)裝飾品美浦。他們完全屬于布局對(duì)象弦赖,并被布局對(duì)象管理,他們并不從 data source 獲取的 contents浦辨。當(dāng)布局對(duì)象指定需要一個(gè) decoration view 的時(shí)候蹬竖,collection view 會(huì)自動(dòng)創(chuàng)建,并將布局對(duì)象提供的布局參數(shù)應(yīng)用到上面去流酬。并不需要為自定義視圖準(zhǔn)備任何內(nèi)容币厕。
Supplementary views 和 decoration views 必須是UICollectionReusableView的子類。布局使用的每個(gè)視圖類都需要在 collection view 中注冊(cè)芽腾,這樣當(dāng) data source 讓它們從 reuse pool 中出列時(shí)劈榨,它們才能夠創(chuàng)建新的實(shí)例。如果你是使用的 Interface Builder晦嵌,則可以通過(guò)在可視編輯器中拖拽一個(gè) cell 到 collection view 上完成 cell 在 collection view 中的注冊(cè)同辣。同樣的方法也可以用在 supplementary view 上,前提是你使用了UICollectionViewFlowLayout惭载。如果沒(méi)有旱函,你只能通過(guò)調(diào)用registerClass:或者registerNib:方法手動(dòng)注冊(cè)視圖類了。你需要在viewDidLoad中做這些操作描滔。
自定義布局
作為一個(gè)非常有意義的自定義 collection view 布局的例子棒妨,我們不妨設(shè)想一個(gè)典型的日歷應(yīng)用程序中的周 (week) 視圖。日歷一次顯示一周,星期中的每一天顯示在列中券腔。每一個(gè)日歷事件將會(huì)在我們的 collection view 中以一個(gè) cell 顯示伏穆,位置和大小代表事件起始日期時(shí)間和持續(xù)時(shí)間。
一般有兩種類型的 collection view 布局:
1.獨(dú)立于內(nèi)容的布局計(jì)算纷纫。這正是你所知道的像 UITableView 和 UICollectionViewFlowLayout 這些情況枕扫。每個(gè) cell 的位置和外觀不是基于其顯示的內(nèi)容,但所有 cell 的顯示順序是基于內(nèi)容的順序辱魁⊙糖疲可以把默認(rèn)的 flow layout 做為例子。每個(gè) cell 都基于前一個(gè) cell 放置(或者如果沒(méi)有足夠的空間染簇,則從下一行開(kāi)始)参滴。布局對(duì)象不必訪問(wèn)實(shí)際數(shù)據(jù)來(lái)計(jì)算布局。
2.基于內(nèi)容的布局計(jì)算锻弓。我們的日歷視圖正是這樣類型的例子砾赔。為了計(jì)算顯示事件的起始和結(jié)束時(shí)間,布局對(duì)象需要直接訪問(wèn) collection view 的數(shù)據(jù)源青灼。在很多情況下粒褒,布局對(duì)象不僅需要取出當(dāng)前可見(jiàn) cell 的數(shù)據(jù)姐呐,還需要從所有記錄中取出一些決定當(dāng)前哪些 cell 可見(jiàn)的數(shù)據(jù)呼奢。
在我們的日歷示例中戴已,布局對(duì)象如果訪問(wèn)某一個(gè)矩形內(nèi) cells 的屬性本橙,那就必須迭代數(shù)據(jù)源提供的所有事件來(lái)決定哪些位于要求的時(shí)間窗口中扳躬。 與一些相對(duì)簡(jiǎn)單,數(shù)據(jù)源獨(dú)立計(jì)算的 flow layout 比起來(lái)甚亭,這足夠計(jì)算出 cell 在一個(gè)矩形內(nèi)的 index paths 了(假設(shè)網(wǎng)格中所有cells的大小都一樣)贷币。
如果有一個(gè)依賴內(nèi)容的布局,那就是暗示你需要寫(xiě)自定義的布局類了亏狰,同時(shí)不能使用自定義的UICollectionViewFlowLayout役纹,所以這正是我們需要做的事情。
UICollectionViewLayout的文檔列出了子類需要重寫(xiě)的方法暇唾。
collectionViewContentSize
由于 collection view 對(duì)它的 content 并不知情促脉,所以布局首先要提供的信息就是滾動(dòng)區(qū)域大小,這樣 collection view 才能正確的管理滾動(dòng)策州。布局對(duì)象必須在此時(shí)計(jì)算它內(nèi)容的總大小瘸味,包括 supplementary views 和 decoration views。注意够挂,盡管大多數(shù)經(jīng)典的 collection view 限制在一個(gè)軸方向上滾動(dòng)(正如UICollectionViewFlowLayout一樣)旁仿,但這不是必須的。
在我們的日歷示例中孽糖,我們想要視圖垂直的滾動(dòng)枯冈。比如毅贮,如果我們想要在垂直空間上一個(gè)小時(shí)占去 100 點(diǎn),這樣顯示一整天的內(nèi)容高度就是 2400 點(diǎn)尘奏。注意滩褥,我們不能夠水平滾動(dòng),這就意味這我們 collection view 只能顯示一周罪既。為了能夠在日歷中的多個(gè)星期間分頁(yè)铸题,我們可以在一個(gè)獨(dú)立(分頁(yè))的 scroll view (可以使用UIPageViewController)中使用多個(gè)collection view(一周一個(gè)),或者堅(jiān)持使用一個(gè) collection view 并且返回足夠大的內(nèi)容寬度琢感,這會(huì)使得用戶感覺(jué)在兩個(gè)方向上滑動(dòng)自由丢间。
為了清楚起見(jiàn),我選擇布局在一個(gè)非常簡(jiǎn)單的模型上:假定每周天數(shù)相同驹针,每天時(shí)長(zhǎng)相同烘挫,也就是說(shuō)天數(shù)用 0-6 表示。在一個(gè)真實(shí)的日歷程序中柬甥,布局將會(huì)為自己的計(jì)算大量使用基于NSCalendaar的日期饮六。
layoutAttributesForElementsInRect:
這是任何布局類中最重要的方法了,同時(shí)可能也是最容易讓人迷惑的方法苛蒲。collection view 調(diào)用這個(gè)方法并傳遞一個(gè)自身坐標(biāo)系統(tǒng)中的矩形過(guò)去卤橄。這個(gè)矩形代表了這個(gè)視圖的可見(jiàn)矩形區(qū)域(也就是它的 bounds ),你需要準(zhǔn)備好處理傳給你的任何矩形臂外。
你的實(shí)現(xiàn)必須返回一個(gè)包含UICollectionViewLayoutAttributes對(duì)象的數(shù)組窟扑,為每一個(gè) cell 包含一個(gè)這樣的對(duì)象,supplementary view 或 decoration view 在矩形區(qū)域內(nèi)是可見(jiàn)的漏健。UICollectionViewLayoutAttributes類包含了 collection view 內(nèi) item 的所有相關(guān)布局屬性嚎货。默認(rèn)情況下,這個(gè)類包含frame蔫浆,center殖属,size,transform3D瓦盛,alpha洗显,zIndex和hidden屬性。如果你的布局想要控制其他視圖的屬性(比如背景顏色)原环,你可以建一個(gè)UICollectionViewLayoutAttributes的子類墙懂,然后加上你自己的屬性。
布局屬性對(duì)象 (layout attributes objects) 通過(guò)indexPath屬性和他們對(duì)應(yīng)的 cell扮念,supplementary view 或者 decoration view 關(guān)聯(lián)在一起损搬。collection view 為所有 items 從布局對(duì)象中請(qǐng)求到布局屬性后,它將會(huì)實(shí)例化所有視圖,并將對(duì)應(yīng)的屬性應(yīng)用到每個(gè)視圖上去巧勤。
注意嵌灰!這個(gè)方法涉及到所有類型的視圖,也就是 cell颅悉,supplementary views 和 decoration views沽瞭。一個(gè)幼稚的實(shí)現(xiàn)可能會(huì)選擇忽略傳入的矩形,并且為 collection view 中的所有視圖返回布局屬性剩瓶。在原型設(shè)計(jì)和開(kāi)發(fā)布局階段驹溃,這是一個(gè)有效的方法。但是延曙,這將對(duì)性能產(chǎn)生非常壞的影響豌鹤,特別是可見(jiàn) cell 遠(yuǎn)少于所有 cell 數(shù)量的時(shí)候,collection view 和布局對(duì)象將會(huì)為那些不可見(jiàn)的視圖做額外不必要的工作枝缔。
你的實(shí)現(xiàn)需要做這幾步:
1, 創(chuàng)建一個(gè)空的可變數(shù)組來(lái)存放所有的布局屬性布疙。
2, 確定 index paths 中哪些 cells 的 frame 完全或部分位于矩形中。這個(gè)計(jì)算需要你從 collection view 的數(shù)據(jù)源中取出你需要顯示的數(shù)據(jù)愿卸。然后在循環(huán)中調(diào)用你實(shí)現(xiàn)的layoutAttributesForItemAtIndexPath:方法為每個(gè) index path 創(chuàng)建并配置一個(gè)合適的布局屬性對(duì)象灵临,并將每個(gè)對(duì)象添加到數(shù)組中。
3, 如果你的布局包含 supplementary views趴荸,計(jì)算矩形內(nèi)可見(jiàn) supplementary view 的 index paths儒溉。在循環(huán)中調(diào)用你實(shí)現(xiàn)的layoutAttributesForSupplementaryViewOfKind:atIndexPath:,并且將這些對(duì)象加到數(shù)組中发钝。通過(guò)為 kind 參數(shù)傳遞你選擇的不同字符顿涣,你可以區(qū)分出不同種類的supplementary views(比如headers和footers)。當(dāng)需要?jiǎng)?chuàng)建視圖時(shí)笼平,collection view 會(huì)將 kind 字符傳回到你的數(shù)據(jù)源。記住 supplementary 和 decoration views 的數(shù)量和種類完全由布局控制舔痪。你不會(huì)受到 headers 和 footers 的限制寓调。
4, 如果布局包含 decoration views,計(jì)算矩形內(nèi)可見(jiàn) decoration views 的 index paths锄码。在循環(huán)中調(diào)用你實(shí)現(xiàn)的layoutAttributesForDecorationViewOfKind:atIndexPath:夺英,并且將這些對(duì)象加到數(shù)組中。
5, 返回?cái)?shù)組滋捶。
我們自定義的布局沒(méi)有使用 decoration views痛悯,但是使用了兩種 supplementary views(column headers和row headers):
layoutAttributesFor…IndexPath
有時(shí),collection view 會(huì)為某個(gè)特殊的 cell重窟,supplementary 或者 decoration view 向布局對(duì)象請(qǐng)求布局屬性载萌,而非所有可見(jiàn)的對(duì)象。這就是當(dāng)其他三個(gè)方法開(kāi)始起作用時(shí),你實(shí)現(xiàn)的layoutAttributesForItemAtIndexPath:需要?jiǎng)?chuàng)建并返回一個(gè)單獨(dú)的布局屬性對(duì)象扭仁,這樣才能正確的格式化傳給你的 index path 所對(duì)應(yīng)的 cell垮衷。
你可以通過(guò)調(diào)用+[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:]這個(gè)方法,然后根據(jù) index path 修改屬性乖坠。為了得到需要顯示在這個(gè) index path 內(nèi)的數(shù)據(jù)搀突,你可能需要訪問(wèn) collection view 的數(shù)據(jù)源。到目前為止熊泵,至少確保設(shè)置了 frame 屬性仰迁,除非你所有的 cell 都位于彼此上方。
如果你正在使用自動(dòng)布局顽分,你可能會(huì)感到驚訝徐许,我們正在直接修改布局參數(shù)的 frame 屬性,而不是和約束共事怯邪,但這正是 UICollectionViewLayout 的工作绊寻。盡管你可能使用自動(dòng)布局來(lái)定義collection view 的 frame 和它內(nèi)部每個(gè) cell 的布局,但 cells 的 frames 還是需要通過(guò)老式的方法計(jì)算出來(lái)悬秉。
類似的澄步,layoutAttributesForSupplementaryViewOfKind:atIndexPath:和layoutAttributesForDecorationViewOfKind:atIndexPath:方法分別需要為 supplementary 和 decoration views 做相同的事。只有你的布局包含這樣的視圖你才需要實(shí)現(xiàn)這兩個(gè)方法和泌。UICollectionViewLayoutAttributes包含另外兩個(gè)工廠方法村缸,+layoutAttributesForSupplementaryViewOfKind:withIndexPath:和+layoutAttributesForDecorationViewOfKind:withIndexPath:,用他們來(lái)創(chuàng)建正確的布局屬性對(duì)象武氓。
shouldInvalidateLayoutForBoundsChange:
最后梯皿,當(dāng) collection view 的 bounds 改變時(shí),布局需要告訴 collection view 是否需要重新計(jì)算布局县恕。我的猜想是:當(dāng) collection view 改變大小時(shí)东羹,大多數(shù)布局會(huì)被作廢,比如設(shè)備旋轉(zhuǎn)的時(shí)候忠烛。因此属提,一個(gè)幼稚的實(shí)現(xiàn)可能只會(huì)簡(jiǎn)單的返回 YES。雖然實(shí)現(xiàn)功能很重要美尸,但是 scroll view 的 bounds 在滾動(dòng)時(shí)也會(huì)改變冤议,這意味著你的布局每秒會(huì)被丟棄多次。根據(jù)計(jì)算的復(fù)雜性判斷师坎,這將會(huì)對(duì)性能產(chǎn)生很大的影響恕酸。
當(dāng) collection view 的寬度改變時(shí),我們自定義的布局必須被丟棄胯陋,但這滾動(dòng)并不會(huì)影響到布局蕊温。幸運(yùn)的是袱箱,collection view 將它的新 bounds 傳給shouldInvalidateLayoutForBoundsChange:方法。這樣我們便能比較視圖當(dāng)前的bounds 和新的 bounds 來(lái)確定返回值:
動(dòng)畫(huà)
插入和刪除
UITableView 中的 cell 自帶了一套非常漂亮的插入和刪除動(dòng)畫(huà)寿弱。但是當(dāng)為 UICollectionView 增加和刪除 cell 定義動(dòng)畫(huà)功能時(shí)犯眠,UIKit 工程師遇到這樣一個(gè)問(wèn)題:如果 collection view 的布局是完全可變的,那么預(yù)先定義好的動(dòng)畫(huà)就沒(méi)辦法和開(kāi)發(fā)者自定義的布局很好的融合症革。他們提出了一個(gè)優(yōu)雅的方法:當(dāng)一個(gè) cell (或者supplementary或者decoration view)被插入到 collection view 中時(shí)筐咧,collection view 不僅向其布局請(qǐng)求 cell 正常狀態(tài)下的布局屬性,同時(shí)還請(qǐng)求其初始的布局屬性噪矛,比如量蕊,需要在開(kāi)始有插入動(dòng)畫(huà)的 cell。collection view 會(huì)簡(jiǎn)單的創(chuàng)建一個(gè) animation block艇挨,并在這個(gè) block 中残炮,將所有 cell 的屬性從初始(initial)狀態(tài)改變到常態(tài)(normal)。
通過(guò)提供不同的初始布局屬性缩滨,你可以完全自定義插入動(dòng)畫(huà)势就。比如,設(shè)置初始的 alpha 為 0 將會(huì)產(chǎn)生一個(gè)淡入的動(dòng)畫(huà)脉漏。同時(shí)設(shè)置一個(gè)平移和縮放將會(huì)產(chǎn)生移動(dòng)縮放的效果苞冯。
同樣的原理應(yīng)用到刪除上,這次動(dòng)畫(huà)是從常態(tài)到一系列你設(shè)置的最終布局屬性侧巨。這些都是你需要在布局類中為initial或final布局參數(shù)實(shí)現(xiàn)的方法.
initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingSupplementaryElementOfKind:atIndexPath:
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
布局間切換
可以通過(guò)類似的方式將一個(gè) collection view 布局動(dòng)態(tài)的切換到另外一個(gè)布局舅锄。當(dāng)發(fā)送一個(gè)setCollectionViewLayout:animated:消息時(shí),collection view 會(huì)為 cells 在新的布局中查詢新的布局參數(shù)司忱,然后動(dòng)態(tài)的將每個(gè) cell(通過(guò)index path在新舊布局中判斷出相同的cell)從舊參數(shù)變換到新的布局參數(shù)皇忿。你不需要做任何事情。
結(jié)論
根據(jù)自定義 collection view 布局的復(fù)雜性坦仍,寫(xiě)一個(gè)通常很不容易鳍烁。確切的說(shuō),本質(zhì)上這和從頭寫(xiě)一個(gè)完整的實(shí)現(xiàn)相同布局自定義視圖類一樣困難了繁扎。因?yàn)樗婕暗挠?jì)算需要確定哪些子視圖當(dāng)前是可見(jiàn)的幔荒,以及它們的位置。盡管如此锻离,使用UICollectionView還是給你帶來(lái)了一些很好的效果铺峭,比如 cell 重用墓怀,自動(dòng)支持動(dòng)畫(huà)汽纠,更不要提整潔的獨(dú)立布局,子視圖管理傀履,以及數(shù)據(jù)提供架構(gòu)規(guī)定(data preparation its architecture prescribes.)虱朵。
自定義 collection view 布局也是向輕量級(jí) view controller邁出很好的一步莉炉,正如你的 view controller 不要包含任何布局代碼。正如 Chris 的文章中解釋的一樣碴犬,將這一切和一個(gè)獨(dú)立的 datasource 類結(jié)合在一起絮宁,collection view 的視圖控制器將很難再包含任何代碼。
每當(dāng)我使用UICollectionView的時(shí)候服协,我被其簡(jiǎn)潔的設(shè)計(jì)所折服绍昂。對(duì)于一個(gè)有經(jīng)驗(yàn)的 Apple 工程師,為了想出如此靈活的類偿荷,很可能需要首先考慮NSTableView和UITableView窘游。
擴(kuò)展閱讀
Collection View Programming Guide.
UICollectionView: The Complete Guide, e-book by Ash Furrow.
MSCollectionViewCalendarLayoutby Eric Horacek is an excellent and more complete implementation of a custom layout for a week calendar view.