iOS Collection View 編程指導(dǎo)(五)-創(chuàng)建自定義Layout
在創(chuàng)建自定義layout之前, 考慮是否可使用UICollectionViewFlowLayout, 因為flow layout是蘋果精心設(shè)計的, 里面考慮了性能優(yōu)化, 易擴(kuò)展等特性. 而且大多數(shù)布局都可以使用flow layout來解決, 除非以下兩種情況才需要自定義布局:
- 你的布局不是基于線性斷裂式布局(item按照行排列, 填滿一行接著下一行), 或者collectionView中內(nèi)容可以在一個以上的方向上滾動
- 你需要頻繁的改變cell的位置, 如果使用flow layout的話工作量很大, 此時考慮自定義布局
從API層面上考慮的話, 實現(xiàn)自定義布局是比較簡單的, 除了計算item的位置(position)和大小(size)比較難外, 其他的工作都很簡單.
繼承UICollectionViewLayout
UICollectionViewLayout
類提供給你一個干凈良好的自定義布局環(huán)境. 該類中的少數(shù)方法是必須重寫的, 這些方法確定了layout對象的核心行為. 剩余的方法是非必須的, 按照你自己的需要去實現(xiàn). 以下列舉了你需要實現(xiàn)的兩個重要任務(wù):
- 確定可滾動的內(nèi)容區(qū)域大小
- 提供cell和view的layout attribute對象, 以確定collectionView中的cell和view的外觀和位置.
雖然你可以僅實現(xiàn)必須要實現(xiàn)的幾個方法, 但如果你的自定義layout對象還實現(xiàn)了其他方法, 那么你的自定義layout對象將更加完善.
layout對象使用DataSource提供的信息來創(chuàng)建collectionView的布局. layout對象通過它的屬性collectionView
來訪問DataSource, 該屬性指與當(dāng)前布局對象綁定的collectionView, 在layout類中任何位置都可以訪問. 在布局過程, 你需要注意, 使用collectionView能獲取啥和不能獲取啥. 因為布局發(fā)生在后臺, 所以collectionView無法知道view的layout或者位置. 所以, 即使layout對象沒有限制你在布局時調(diào)用collectionView的方法, 除了獲取布局對象需要的很布局相關(guān)的data以外, 你應(yīng)該避免去調(diào)用collectionView的其他方法.
理解布局過程的關(guān)鍵部分
collectionView能夠直接使用自定義layout來布局. 當(dāng)collectionView需要布局時(初次顯示/resize), 會要求layout對象提供布局信息. 也可以通過調(diào)用invalidateLayout方法來顯示地通知collectionView更新布局. 該方法會使collectionView丟棄現(xiàn)有的布局信息并讓layout對象產(chǎn)生信息的布局信息提供給collectionView.
注意:不要把layout的
invalidateLayout
方法和collectionView的reloadData
方法弄混淆了. 調(diào)用invalidateLayout
方法時, collectionView中的cell和view都不變, 變的是布局信息, 因為layout對象會重新計算一次所有item的layout attribute. 調(diào)用reloadData
方法, 是因為DataSource中的數(shù)據(jù)有變化, collectionView的布局信息不會變.
在布局過程中, collectionView會調(diào)用layout對象的一些方法, 在這些方法里面, 你可以計算item的位置和其他collectionView要用的一些關(guān)鍵信息, 部分方法可能調(diào)用, 但下面幾個布局步驟方法一定會調(diào)用:
- 使用
prepareLayout
方法來做一些布局前的準(zhǔn)備, 計算布局信息. - 使用
collectionViewContentSize
方法來提供全部內(nèi)容區(qū)域大小. - 使用
layoutAttributesForElementsInRect:
方法來提供特定區(qū)域內(nèi)cell和view的attribute對象
圖5-1, 展示了如何使用上面方法來生成布局信息
- 在
prepareLayout
方法中, 你需要計算任何可以確定cell和view的位置的信息, 至少需要計算出內(nèi)容大小的信息, 該信息在步驟2的方法中被返回. - collectionView使用content size來配置scrollView. 舉個例子, 如果你計算后的contentSize在水平和豎直方向上都超出屏幕, scrollView可以同時在兩個方向上滾動. 但是如果你使用的flow layout的話, 只能在一個方向滾動.
- 基于當(dāng)前的滾動位置, collectionView調(diào)用
layoutAttributesForElementsInRect:
方法來獲取特定矩形區(qū)域內(nèi)的cell和view的attribute信息, 該矩形區(qū)域可能和可見區(qū)域相同, 也可能不同. 當(dāng)信息獲取后, 代表布局過程已經(jīng)完成. - 當(dāng)布局完成后, cell和view的attribute信息保持不變, 直到你主動或者collectionView調(diào)用
invalidateLayout
方法來重啟布局過程. 調(diào)用了layout的invalidateLayout
方法后, 會再次開啟布局過程,prepareLayout
方法會被重新調(diào)用. 當(dāng)你滾動collectionView中的內(nèi)容時, collectionView調(diào)用layout對象的shouldInvalidateLayoutForBoundsChange:
方法, 如果該方法返回YES, 那么會invalidate你的layout.
注意:值得注意的是, 你去調(diào)用
invalidateLayout
方法時, layout不會立即更新, 而是標(biāo)記當(dāng)前的layout已經(jīng)和數(shù)據(jù)不一致了, 需要更新, 當(dāng)下一次view刷新時才會去更新. 在更新過程中, collectionView會檢查它的layout已經(jīng)過時了, 如果是就更新layout. 事實上, 如果你在一個點連續(xù)調(diào)用invalidateLayout
多次也不會立即更新layout.
創(chuàng)建Layout Attribute
attribute對象是UICollectionViewLayoutAttribute
的實例, 由layout對象負(fù)責(zé)創(chuàng)建. 當(dāng)APP處理大量的item時, 在準(zhǔn)備布局時創(chuàng)建attribute對象非常有用, 因為attribute記錄了item的布局信息而且可以緩存起來. 計算attribute屬性非常消耗時, 所以緩存的將變得有意義, 這樣在布局過程中可以隨時創(chuàng)建attribute對象. 你可以使用下面的類方法來創(chuàng)建UICollectionViewLayoutAttribute
實例:
- layoutAttributesForCellWithIndexPath:
- layoutAttributesForSupplementaryViewOfKind:withIndexPath:
- layoutAttributesForDecorationViewOfKind:withIndexPath:
在創(chuàng)建attribute對象時, 你必須根據(jù)view類型來選擇正確的類方法, 因為collectionView使用attribute對象從DataSource中獲取相應(yīng)的view, 如果你的類方法使用錯誤, 那么創(chuàng)建的view也會錯誤, layout也是失效
在創(chuàng)建attribute對象之后, 你可以給attribute的屬性賦值, 如果系統(tǒng)的類的屬性不能滿足需求, 你可以自定義一個attribute對象. 比如你可以給attribute對象復(fù)制size和position, 這些值在接下來的布局中會用到, 你要控制view的層疊順序可以使用zIndex
屬性來控制. 另外在定義的attribute對象中, 你需要實現(xiàn)isEqual:
方法, 因為在某些時候, collectionView會用來比較兩個attribute.
如果想知道更多關(guān)于layout attribute的信息, 請看該對象的API文檔UICollectionViewLayoutAttributes Class Reference
準(zhǔn)備布局
在布局時, layout對象會先調(diào)用prepareLayout
方法開啟布局過程. 在這個方法內(nèi), 你可以計算布局信息供后面使用. 在自定義layout中, prepareLayout
不是必須實現(xiàn)的, 調(diào)用該方法之后, 布局對象有了足夠的信息去計算collectionView的contentSize. 你還可以在該方法中計算attribute對象屬性值.
給指定矩形區(qū)域內(nèi)的item提供Layout Attribute
在布局的最后一步是調(diào)用layout對象的layoutAttributesForElementsInRect:
方法來獲取特定矩形中的cell和view的Attribute信息. 對于collectionView來說, 該特定的矩形區(qū)域一般指可見區(qū)域(visible rect), 如圖5-2所示的cell6到cell20和header2. 在該方法內(nèi), 你需要返回這些view的Attribute信息.
在方法prepareLayout
調(diào)用時就應(yīng)該計算方法layoutAttributesForElementsInRect:
需要的信息. 實現(xiàn)layoutAttributesForElementsInRect:
的步驟如下:
- 獲取方法
prepareLayout
計算的數(shù)據(jù), 然后獲取緩存的Attribute對象或者新創(chuàng)建一個 - 檢查item的frame, 確定該item在給定的矩形區(qū)域內(nèi)
- 將item對應(yīng)的
UICollectionViewLayoutAttribute
實例添加到一個數(shù)組 - 將上面的數(shù)組返回給collectionView
你是在prepareLayout
創(chuàng)建Attribute還是等到在layoutAttributesForElementsInRect:
創(chuàng)建, 取決于你如何管理的布局信息. 但你需要考慮緩存Attribute的好處. 當(dāng)你collectionView中的item太多時, 等到請求Attribute時再創(chuàng)建Attribute更好點.
注意: 有時layout對象會為個別item提供Attribute, 比如在布局過程外時, collectionView就可能為了做某些動畫來請求個別item的Attribute. 詳情請看Providing Layout Attributes On Demand
這里有個demo用來講解這些過程,請看Providing Layout Attributes
按需提供Layout Attribute
collectionView有時需要layout提供Attribute來做插入/刪除特定item的動畫, layout提供了下面方法來實現(xiàn)此項功能:
- layoutAttributesForItemAtIndexPath:
- layoutAttributesForSupplementaryViewOfKind:atIndexPath:
- layoutAttributesForDecorationViewOfKind:atIndexPath:
在這些方法中, 你需要返回當(dāng)前l(fā)ayout對象中對應(yīng)item的Attribute信息, layoutAttributesForItemAtIndexPath:
必須實現(xiàn), 其他兩個是可選的, 返回Attribute后, 你不要更新Attribute對象, 如果你的layout改變了, 使用invalidateLayout布局即可.
使用自定義布局
可以通過代碼和storyboard來使用自定義布局. 你可以使用collectionView的屬性collectionViewLayout來綁定你的布局對象, 如下代碼清單5-1所示:
代碼清單5-1 使用自定義布局
self.collectionView.collectionViewLayout = [[MyCustomLayout alloc] init];
在storyboard中使用自定義布局, 請看圖5-a:
讓自定義layout更強(qiáng)悍
自定義layout必須給cell和view提供layout attribute對象, 如果你也可給你的layout提供其他特性來提高用戶體驗的話, 這樣你的layout對象更加健壯, 這些特性是可選的, 但推薦你提供這特性.
通過supplementary視圖來將突出內(nèi)容
supplementary視圖是獨立, 它和cell一樣擁有自己的attribute, 由DataSource對象提供該view, 他們的作用是突出主體內(nèi)容. 比如, UICollectionViewFlowLayout
使用supplementary視圖作為section的header和footer. 有個的APP可能使用補(bǔ)充視圖作為展示每個cell的信息label. 和cell一樣, 補(bǔ)充視圖也要循環(huán)使用, 所以補(bǔ)充視圖是UICollectionReuseableView
的子類.
添加補(bǔ)充視圖的步驟如下:
- 通過
registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
或者registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
來注冊補(bǔ)充視圖. - 在DataSource中, 實現(xiàn)
collectionView:viewForSupplementaryElementOfKind:atIndexPath:
, 并在使用dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:
來dequeue補(bǔ)充視圖. - 給你的supplementary視圖提供attribute對象
- 在
layoutAttributesForElementsInRect:
方法中, 將attribute放入數(shù)組中后將數(shù)組返回. - 使用
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
方法為特定的supplementary視圖提供attribute對象.
supplementary視圖的創(chuàng)建過程和cell創(chuàng)建非常類似, 但有一點區(qū)別就是supplementary視圖還包含各種類別, layout對象使用一個字符串來標(biāo)識.
創(chuàng)建Decoration視圖
Decoration視圖是用來裝飾collectionView中的內(nèi)容的(cell), 和supplementary和cell不一樣的地方是, 裝飾視圖由layout對象提供和DataSource無關(guān), 僅用于內(nèi)容顯示. 裝飾視圖可以用來創(chuàng)建自定義背景, 填充cell的外圍, 用于模糊cell. 裝飾視圖完全由layout對象控制和DataSource不會有任何交互.
下面是創(chuàng)建decoration視圖的步驟:
-
registerClass:forDecorationViewOfKind:
或registerNib:forDecorationViewOfKind:
這兩個方法用來注冊裝飾視圖, 看起來和注冊cell和補(bǔ)充視圖一樣, 不過你需要牢記的是, 這個兩個方法需要在layout對象調(diào)用而不是DataSource中 - 在layout對象中的
layoutAttributesForElementsInRect:
方法中創(chuàng)建attribute對象后返回 - 實現(xiàn)
layoutAttributesForDecorationViewOfKind:atIndexPath:
方法, 為特定decoration視圖提供attribute對象 - initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:和finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:用來處理裝飾視圖的顯示/隱藏動畫的, 這兩個方法的實現(xiàn)是可選的.
裝飾視圖的創(chuàng)建和cell/supplementary不同的. 創(chuàng)建時, 你要用UICollectionReusableView
來創(chuàng)建或者用nib來創(chuàng)建. 當(dāng)需要裝飾視圖時, collectionView負(fù)責(zé)創(chuàng)建它, 然后使用layout提供的attribute信息來布局, 裝飾視圖是純用來顯示的, 不要用來做其他事, 而且裝飾視圖支持復(fù)用機(jī)制.
注意: 給裝飾視圖創(chuàng)建attribute時, 記得設(shè)置
zIndex
的值, 通過該屬性可以將裝飾置于cell/supplementary視圖的前面/后面
創(chuàng)建插入/刪除動畫
cell的插入/刪除操作會導(dǎo)致其他cell的布局變換, layout對象知道如何將collectionView中cell從初始位置(initial)動畫移動到最終位置(final), 但不知道要插入的cell的初始位置和要刪除的cell的最終位置, 所以做插入/刪除動畫時, 開發(fā)者需要告訴layout對象cell的初始位置/最終位置
圖5-3展示了cell的插入動畫的layout變換的示意圖, collectionView的本來有三個cell, 插入一個cell, 它的初始位置在section的中心,并且alpha為0, layout將其從初始狀態(tài)動畫移動到最終位置(右下角,alpha為1).
下面的代碼展示了如何在layout對象的-initialLayoutAttributesForAppearingItemAtIndexPath:
方法中提供item的初始attribute對象.
代碼清單5-2 給插入的cell一個初始的attribute
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
CGSize size = [self collectionView].frame.size;
attributes.center = CGPointMake(size.width / 2.0, size.height / 2.0);
return attributes;
}
注意: 代碼清單5-2中, 當(dāng)一個cell插入時, 其他所有的cell都會做一個從中心做彈出動畫. 為了只針對插入的cell做動畫, 需要對cell的index path是否在prepareForCollectionViewUpdates:方法中傳過來的items當(dāng)中做判斷, 如果在返回初始attribute, 如果不在則返回[super initialLayoutAttributesForAppearingItemAtIndexPath:indexPath]
cell的刪除和cell的插入雷同, 不過你需要返回刪除的final attribute, 才能做動畫. UICollectionViewLayout
類提供了6個方法(cell/supplementary/decoration, 這個view分別對應(yīng)的initial/final attribute)來返回相關(guān)的attribute
提高collectionView的滾動體驗
自定義的layout對象可以改善collectionView的體驗. 因為當(dāng)滾動事件結(jié)束時, scrollView要根據(jù)當(dāng)前的速度和減速度(負(fù)的加速度)來判斷scrollView將要停在那里. 當(dāng)collectionView知道了停留位置后, 會調(diào)用layout對象的targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法, 來做微調(diào). 因為在collectionView還在滾動的時候調(diào)用該方法, 所以你的自定義的layout可以影響滾動結(jié)束的最終位置.
圖5-4展示了你將如何使用自定義layout對象去修改滾動行為. 如圖所示, 通過targetContentOffsetForProposedContentOffset:withScrollingVelocity:
方法微調(diào)proposedContentOffset, 我們可以在結(jié)束滾動時將collectionView的內(nèi)容置中, 這樣可以提高用戶體驗.
關(guān)于自定義layout的幾點建議
這里有幾個關(guān)于實現(xiàn)自定義layout的提示和建議:
- 考慮使用
prepareLayout
方法去創(chuàng)建和保存UICollectionViewLayoutAttributes
對象.- collectionView會隨機(jī)的請求attribute對象, 所以你事先創(chuàng)建和保存好一些attribute以備使用.
- 這種方式適合在item比較少(幾百個)或者attribute對象不經(jīng)常改變
- 如果你的item很多時(幾千), 你就得權(quán)衡緩存和重新計算的利弊, 對于size可變的item且它們的layout不經(jīng)常變, 所以緩存策略會減少layout的復(fù)雜計算量. 對于大量的固定size的item來說, layout計算相對來說比價簡單, 而且如果layout老變得話, 需要反復(fù)計算layout, 所以緩存策略沒有用處, 且浪費空間.
- 不要繼承
UICollectionView
. collectionView本身很少或者沒有外觀上的顯示, 相反, 它只是從DataSource中獲取view和數(shù)據(jù), 再從layout對象中獲取布局信息, 將三者結(jié)合起來顯示你想要展示的內(nèi)容. 如果你想顯示三維內(nèi)容, 那么你可以自定義layout, 將3D變換加在cell上即可 - 在自定義layout時, 在
layoutAttributesForElementsInRect:
方法中不要向UICollectionView
發(fā)送visibleCells消息, 因為collectionView此時不知道各item的位置, 這些信息本來就是由layout像提供的, 你在layout的方法中去向collectionView請求visible cells, 最終還是要調(diào)用layout的方法來獲取, 這里就是調(diào)用循環(huán)了. - item的位置信息只能layout負(fù)責(zé), 所以計算item的位置信息時, 一般只能靠自己, 只有少數(shù)情況下會用到DataSource, 比如要將一些item畫在地圖上時, layout要根據(jù)DataSource來獲取map的位置信息.