在創(chuàng)建自定義的layout之前,你需要知道UICollectionViewFlowLayout提供的很多特性已經(jīng) 經(jīng)過(guò)優(yōu)化以滿(mǎn)足多種常用的layout。除非是如下情況,否則不建議自定義
1 你所想實(shí)現(xiàn)的外觀并不是網(wǎng)格或者 line-based breaking 布局(items排成一行直到行滿(mǎn),再繼續(xù)往下一行上去排,直到所有items都排列完成),或者必須要在多個(gè)方向上都可以滾動(dòng)
2 需要頻繁地改變所有 Cell的位置,以致于創(chuàng)建自定義layout比修改現(xiàn)有flow layout工作量更省
記住:自定義最難的部分是確定布局中各item位置所需要的計(jì)算
繼承UICollectionViewLayout
繼承UICollectionViewLayout之后只需要重載幾個(gè)提供布局核心特性的方法,其他方法只需按情況重載即可,核心特性如下:
1 指定可滾動(dòng)內(nèi)容區(qū)域的size
2 為布局中的每個(gè)Cell及view提供屬性對(duì)象
layout對(duì)象需要用到datasource以創(chuàng)建collection view的layout對(duì)象,其通過(guò)layout自身的collectionView屬性訪問(wèn)此datasource。需要注意的是,知道 layout過(guò)程中哪些信息可以從collection view中訪問(wèn)到,哪些不可以 是非常重要的。因?yàn)閘ayout過(guò)程中,collection view 是無(wú)法獲知各View的布局以及位置的。所以盡量避免通過(guò) collection view獲取除layout之外的信息。
深入理解布局過(guò)程
collection view完全通過(guò)你自定義的layout對(duì)象管理整個(gè)布局過(guò)程,如 collection view 首次布局或者resize的時(shí)候,會(huì)向布局對(duì)象獲取相關(guān)信息。你也可以手動(dòng)調(diào)用invalidateLayout方法以更新布局對(duì)象,此方法會(huì)強(qiáng)制生成新layout。(需要注意invalidateLayout與reloadData的區(qū)別,在移動(dòng),添加或者刪除item的時(shí)候,需要摒棄原有布局,重新生成新的布局,使用invalidateLayout,而如果只是datasource中的數(shù)據(jù)有更新,這時(shí)需要使用reloadData)
layout過(guò)程中,如下方法提供了layout的基本信息,其他方法也會(huì)被調(diào)用,但如下這些方法總是按如下順序調(diào)用的:
1 prepareLayout方法調(diào)用來(lái)為即將進(jìn)行的layout作前期的計(jì)算
2 collectionViewContentSize方法基于初始計(jì)算,返回整體內(nèi)容區(qū)域的size
3 layoutAttributesForElementsInRect:方法返回指定區(qū)域中cells和views的屬性
prepareLayout方法是為確定布局中各cell和view位置做計(jì)算,需要在此方法中算出足夠的信息以供后續(xù)方法計(jì)算內(nèi)容區(qū)域的整體size,collection view使用content size 以正確地配置scroll view。比如 content size 長(zhǎng)寬均超過(guò)屏幕的話(huà),水平與豎直方向的滾動(dòng)都會(huì)被enable。基于當(dāng)前滾動(dòng)位置,collection view會(huì)調(diào)用 layoutAttributesForElementsInRect:方法以請(qǐng)求特定rect(有可能是也可能不是可見(jiàn)rect)中cell和view的屬性。到此,core layout process已經(jīng)結(jié)束了。
layout結(jié)束之后,cells和views的屬性在你或者collection view invalidate布局之前都不會(huì)變,collection view可以在滾動(dòng)的過(guò)程中自動(dòng)invalidate 布局:用戶(hù)滾動(dòng)內(nèi)容過(guò)程中,collection view調(diào)用layout的shouldInvalidateLayoutForBoundsChange:方法,如果返回值為YES則invalidate 布局。(但需要知道的是,invalidateLayout并不會(huì)馬上觸發(fā)layout update process,而是在下一個(gè)view更新周期中,collection view發(fā)現(xiàn)layout已經(jīng)dirty才會(huì)去更新)
創(chuàng)建布局屬性
自定義layout需要返回UICollectionViewLayoutAttributes類(lèi)的對(duì)象,這些對(duì)象可以在很多不同方法中創(chuàng)建,但創(chuàng)建時(shí)間可以根據(jù)具體情況具體決定。如果collectionview未有數(shù)千的item,則prepare layout時(shí)創(chuàng)建會(huì)比在用戶(hù)滾動(dòng)過(guò)程中用到時(shí)再計(jì)算更可取,因?yàn)閯?chuàng)建的這些屬性可以緩存起來(lái)。如果計(jì)算所有屬性并緩存起來(lái)所帶來(lái)的性能消耗比請(qǐng)求時(shí)獲取的消耗更大,則可請(qǐng)求時(shí)再創(chuàng)建相關(guān)屬性對(duì)象。
創(chuàng)建UICollectionViewLayoutAttributes類(lèi)對(duì)象新實(shí)例時(shí),可以使用這樣幾個(gè)方法:layoutAttributesForCellWithIndexPath:,layoutAttributesForSupplementaryViewOfKind:withIndexPath:,layoutAttributesForDecorationViewOfKind:withIndexPath:,基于展示的view類(lèi)型的不同,必須使用正確的類(lèi)方法,因?yàn)閏ollection view使用這些信息向datasource對(duì)象請(qǐng)求適當(dāng)類(lèi)型的view。使用錯(cuò)誤的方法會(huì)引起collection view在錯(cuò)誤的地方創(chuàng)建錯(cuò)誤的view,你所希望呈現(xiàn)的layout就不會(huì)出現(xiàn)。
創(chuàng)建每個(gè)屬性對(duì)象之后,將相應(yīng)View的相關(guān)屬性都設(shè)置上。最少要在layout中設(shè)置view的size和position。如果在你的布局中有view重疊了,需要正確配置zIndex屬性以維持重疊views的一致的有序狀態(tài)。其他屬性可以讓你控制cell或者view的可見(jiàn)性或者外觀表現(xiàn)。如果標(biāo)準(zhǔn)屬性類(lèi)無(wú)法滿(mǎn)足你的需要,可以繼承并對(duì)其進(jìn)行擴(kuò)充以存儲(chǔ)每個(gè)View的其他信息。繼承l(wèi)ayout屬性時(shí),需要實(shí)現(xiàn)屬性的isEqual:方法因?yàn)閏ollectionview需要使用這個(gè)方法。
給定矩形中的items的布局屬性
layout processs的最后,collection view會(huì)調(diào)用你的layout對(duì)象的layoutAttributesForElementsInRect:方法。對(duì)一個(gè)大的可滾動(dòng)的內(nèi)容區(qū)域,collectionview可能只會(huì)請(qǐng)求當(dāng)前可見(jiàn)的那部分區(qū)域中的所有items的屬性。當(dāng)然,這個(gè)方法需要支持獲取任意rect中items的信息,因?yàn)橛锌赡茉诓迦爰皠h除時(shí)需要做動(dòng)畫(huà)效果。
layoutAttributesForElementsInRect:方法的實(shí)現(xiàn)需要遵循如下步驟:
1 遍歷prepareLayout方法產(chǎn)生的數(shù)據(jù)以訪問(wèn)緩存的屬性或者創(chuàng)建新的屬性
2 檢查每個(gè)item的frame以確定是否與layoutAttributesForElementsInRect:方法中指定rectangle有重疊部分
3 對(duì)每個(gè)重疊的item,添加一個(gè)對(duì)應(yīng)的UICollectionViewLayoutAttributes對(duì)象到一個(gè)數(shù)組中
4 返回布局屬性的數(shù)組給collection view
不僅要記住緩存layout信息能夠帶來(lái)性能提升,也要記住不斷重復(fù)為cells創(chuàng)建新layout屬性的計(jì)算代價(jià)是十分昂貴的,足以影響到app的性能。當(dāng)collection view管理的items量很大時(shí),采用在請(qǐng)求時(shí)創(chuàng)建layout屬性的方式是十分合理的。
按需提供布局屬性對(duì)象
collection view會(huì)在正常的layout 過(guò)程之外周期性地讓你提供單個(gè)items的layout對(duì)象。比如為某item配置插入和刪除動(dòng)畫(huà)時(shí)。自定義的layout通過(guò)如下方法提供這些信息:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
layoutAttributesForDecorationViewOfKind:atIndexPath:
返回屬性時(shí),不能更新這些layout屬性,如果需要改變layout信息,調(diào)用invalidateLayout,在接下來(lái)的layout周期中更新這些信息。上述方法中l(wèi)ayoutAttributesForItemAtIndexPath:是所有自定義 layout都必須重載的方法,如果有supplementary view和decoration view可以分別重載下面兩個(gè)方法。
可以通過(guò)self.collectionView.collectionViewLayout = [[MyCustomLayout alloc] init];方式也可以在storyboard文件中設(shè)置collection view 的class屬性
讓你的layout更優(yōu)異
除了上述這些必須實(shí)現(xiàn)的方法,還有一些特性能夠改善自定義layout的用戶(hù)體驗(yàn),實(shí)現(xiàn)這些屬性是可選但推薦實(shí)現(xiàn)的。
通過(guò) 附加view 提供內(nèi)容品質(zhì)
supplementary views與Cells分離且有自己的layout屬性,由Datasource提供,且其目的是為app主要內(nèi)容增強(qiáng)信息。與cells一樣,supplementary view也會(huì)經(jīng)歷重用的過(guò)程以最小化collection view使用的資源消耗。所以所有 supplementary view都需要繼承UICollectionReusableView。
添加supplementary view到layout中的過(guò)程如下:
1 注冊(cè)supplementary view到layout對(duì)象中,registerClass:forSupplementaryViewOfKind:withReuseIdentifier: or registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
2 在datasource中實(shí)現(xiàn)collectionView:viewForSupplementaryElementOfKind:atIndexPath:,由于這些view是可重用的,調(diào)用dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:來(lái)獲取可用的view
3 但為Cells創(chuàng)建一樣為supplementary Views創(chuàng)建layout 屬性對(duì)象
4 layoutAttributesForElementsInRect:方法中返回的屬性數(shù)組中包含supplementary view的layout屬性對(duì)象
5 實(shí)現(xiàn)layoutAttributesForSupplementaryViewOfKind:atIndexPath:方法為特定supplementary View返回屬性對(duì)象
處理supplementaryview布局屬性的過(guò)程和cell屬性的過(guò)程一樣,但不同的是supplementary view可以有很多種但只能有一種Cell。這是因?yàn)?supplementary view與它們是分離開(kāi)的,是為了烘托主旨,所以每個(gè)supplementary view方法都會(huì)指明其各類(lèi)以方便正確計(jì)算其特有的屬性。
在layout中添加Decoration Views
Decoration Views是layout UI特征的有效點(diǎn)綴,與cell和supplementary view不同的是,它只做外觀呈現(xiàn)用,所以與datasource無(wú)關(guān)。可以用來(lái)提供自定義背影,在Cells縫隙之間填充,甚至可以掩蓋cell,它完全由layout對(duì)象控制。
在layout中添加Decoration view步驟如下:
1 用registerClass:forDecorationViewOfKind: or registerNib:forDecorationViewOfKind: method方法注冊(cè)自定義的decoration view,但記住是在layout對(duì)象中注冊(cè)
2 layout對(duì)象中l(wèi)ayoutAttributesForElementsInRect:方法中為decoration view創(chuàng)建屬性
3 實(shí)現(xiàn)layoutAttributesForDecorationViewOfKind:atIndexPath:方法并在請(qǐng)求時(shí)返回decoration view的布局屬性
4 選擇性地實(shí)現(xiàn)initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath: 和 finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:方法以處理出現(xiàn)和消失的動(dòng)畫(huà),可參考下面的插入和刪除動(dòng)畫(huà)部分
由于decoration view與cell和supplementary view的創(chuàng)建過(guò)程不同,注冊(cè)class或者 nib即可,最多需要調(diào)用 一個(gè)initWithFrame:方法。但任何decoration view也需要是UICollectionReusableView子類(lèi),因?yàn)?也對(duì)其啟用了回收機(jī)制。
插入和刪除動(dòng)畫(huà)
插入及刪除cell時(shí)collection view會(huì)詢(xún)問(wèn)layout對(duì)象提供一組初始化屬性用于動(dòng)畫(huà),同樣,刪除元素時(shí)會(huì)詢(xún)問(wèn)一組終值屬性。
item插入時(shí),layout對(duì)象提供正要被插入的item的初始化layout信息。在此例中,layout先將Cell的初始化位置設(shè)置到Collection view中間,并將其alpha通道設(shè)置為0,動(dòng)畫(huà)期間,此item會(huì)漸現(xiàn)并從中間移動(dòng)到右下角。下面的代碼描述了如何設(shè)置初始化信息及實(shí)現(xiàn)動(dòng)畫(huà):
需要注意的是,上述代碼會(huì)使得此item插入的時(shí)候?qū)λ蠧ell都會(huì)添加此插入的動(dòng)畫(huà),若只想對(duì)插入的item做插入動(dòng)畫(huà),可以檢查 index path是否與傳入prepareForCollectionViewUpdates:方法的item的index path匹配,并只在有匹配到的時(shí)候才進(jìn)行動(dòng)畫(huà),否則只返回super initialLayoutAttributesForAppearingItemAtIndexPath:
delete動(dòng)畫(huà)與插入類(lèi)似,提供正確的final 屬性即可
提升layout的滾動(dòng)體驗(yàn)
滾動(dòng)的時(shí)候scrollview會(huì)根據(jù)當(dāng)前的speed和減速狀況決定最終會(huì)停在哪個(gè)偏移,當(dāng)算出這個(gè)停留位置之后,其會(huì)調(diào)用 targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法是否要改變這個(gè)位置,由于其是在滾動(dòng)過(guò)程中調(diào)用此方法,所以自定義layout可以改變滾動(dòng)的儀停留位置。
下圖展示了調(diào)整滾動(dòng)特性的效果
假定collection view開(kāi)始于(0,0),且用戶(hù)向左側(cè)滑,collection view計(jì)算出滾動(dòng)原本會(huì)停下的位置,自定義layout可能會(huì)改變這個(gè)值以確保滾動(dòng)停下的時(shí)候,某個(gè)item正好停留在可見(jiàn)區(qū)域正中間。這個(gè)新值會(huì)成為新的目標(biāo)content offset,且會(huì)從targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法返回。
友情提示
1 items數(shù)較小(數(shù)百),或者items layout信息變化較小 時(shí),可以在prepareLayout中創(chuàng)建并緩存layout信息
2 盡量不要繼承UICollectionView
3 不要在layoutAttributesForElementsInRect:方法中調(diào)用uicollectionview的visiblecells方法,因?yàn)槠鋵?shí)這個(gè)調(diào)用是轉(zhuǎn)化成了向layout對(duì)象請(qǐng)求visible cells