iOS Collection View 編程指導(dǎo)(五)-創(chuàng)建自定義Layout

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)用:

  1. 使用prepareLayout方法來做一些布局前的準(zhǔn)備, 計算布局信息.
  2. 使用collectionViewContentSize方法來提供全部內(nèi)容區(qū)域大小.
  3. 使用layoutAttributesForElementsInRect:方法來提供特定區(qū)域內(nèi)cell和view的attribute對象

圖5-1, 展示了如何使用上面方法來生成布局信息


圖5-1 layout你的內(nèi)容
  • 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實例:

在創(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信息.

圖5-2 只計算可見區(qū)域的布局信息

在方法prepareLayout調(diào)用時就應(yīng)該計算方法layoutAttributesForElementsInRect:需要的信息. 實現(xiàn)layoutAttributesForElementsInRect:的步驟如下:

  1. 獲取方法prepareLayout計算的數(shù)據(jù), 然后獲取緩存的Attribute對象或者新創(chuàng)建一個
  2. 檢查item的frame, 確定該item在給定的矩形區(qū)域內(nèi)
  3. 將item對應(yīng)的UICollectionViewLayoutAttribute實例添加到一個數(shù)組
  4. 將上面的數(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)此項功能:

在這些方法中, 你需要返回當(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:


圖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ǔ)充視圖的步驟如下:

  1. 通過registerClass:forSupplementaryViewOfKind:withReuseIdentifier:或者registerNib:forSupplementaryViewOfKind:withReuseIdentifier:來注冊補(bǔ)充視圖.
  2. 在DataSource中, 實現(xiàn)collectionView:viewForSupplementaryElementOfKind:atIndexPath:, 并在使用dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:來dequeue補(bǔ)充視圖.
  3. 給你的supplementary視圖提供attribute對象
  4. layoutAttributesForElementsInRect:方法中, 將attribute放入數(shù)組中后將數(shù)組返回.
  5. 使用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視圖的步驟:

  1. registerClass:forDecorationViewOfKind:registerNib:forDecorationViewOfKind:這兩個方法用來注冊裝飾視圖, 看起來和注冊cell和補(bǔ)充視圖一樣, 不過你需要牢記的是, 這個兩個方法需要在layout對象調(diào)用而不是DataSource中
  2. 在layout對象中的layoutAttributesForElementsInRect:方法中創(chuàng)建attribute對象后返回
  3. 實現(xiàn)layoutAttributesForDecorationViewOfKind:atIndexPath:方法, 為特定decoration視圖提供attribute對象
  4. 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).

圖5-3 給一個開始顯示的item一個初始attribute

下面的代碼展示了如何在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)容置中, 這樣可以提高用戶體驗.

圖5-4 微調(diào)proposed content offset來調(diào)整collectionView的content offset以便更好的顯示內(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的位置信息.
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市贱鄙,隨后出現(xiàn)的幾起案子饥努,更是在濱河造成了極大的恐慌锋叨,老刑警劉巖咙咽,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丧诺,死亡現(xiàn)場離奇詭異脯颜,居然都是意外死亡悠汽,警方通過查閱死者的電腦和手機(jī)潜圃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門缸棵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人谭期,你說我怎么就攤上這事堵第。” “怎么了崇堵?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵型诚,是天一觀的道長。 經(jīng)常有香客問我鸳劳,道長狰贯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮涵紊,結(jié)果婚禮上傍妒,老公的妹妹穿的比我還像新娘。我一直安慰自己摸柄,他們只是感情好颤练,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著驱负,像睡著了一般嗦玖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上跃脊,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天宇挫,我揣著相機(jī)與錄音,去河邊找鬼酪术。 笑死器瘪,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绘雁。 我是一名探鬼主播橡疼,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼庐舟!你這毒婦竟也來了欣除?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤继阻,失蹤者是張志新(化名)和其女友劉穎耻涛,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瘟檩,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡抹缕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了墨辛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卓研。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖睹簇,靈堂內(nèi)的尸體忽然破棺而出奏赘,到底是詐尸還是另有隱情,我是刑警寧澤太惠,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布磨淌,位于F島的核電站,受9級特大地震影響凿渊,放射性物質(zhì)發(fā)生泄漏梁只。R本人自食惡果不足惜缚柳,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搪锣。 院中可真熱鬧秋忙,春花似錦、人聲如沸构舟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狗超。三九已至弹澎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抡谐,已是汗流浹背裁奇。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工桐猬, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留麦撵,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓溃肪,卻偏偏與公主長得像免胃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子惫撰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容