CollectionView自定義布局

p34p 7年前發布 | 7K 次閱讀 iOS開發 移動開發

想研究下collection view自定義布局,所以通讀apple文檔,順手翻譯記下來,供以后翻閱(水平有限,錯誤在所難免,請原諒我蹩腳的英文)

一、創建自定義layout

在你開始創建一個自定義layout的時候,先考慮一下是否真的需要。

UICollectionViewFlowLayout已經提供的特性,可以實現很多不同種類的布局。滿足一下條件,可以考慮用自定義layout:

  • 你需要的layout一點也不像一個網格狀的layout,或者line-based breaking layout(就是,當cell鋪滿一行后,接著再下一行鋪,一直到所有cell展示完畢),或者需要多方向滾動的時候
  • 你想要經常改變cell的位置,而且修改flow layout比創建一個自定義layout還要麻煩的時候

    解釋一下:

    1. 如果你需要的layout跟UICollectionViewFlowLayout樣式差別過大
    2. 多方向滾動
    3. 修改UICollectionViewFlowLayout比創建一個自定義還麻煩

好消息是API很清晰,實現一個自定義layout并不困難,最難的部分是在布局中通過計算確定每個cell的位置,當你搞定這些信息,提供給collection view是很簡單的。

二、繼承UICollectionViewLayout

對于自定義layout, 你需要繼承UICollectionViewLayout,只有一少部分核心方法必須需要你實現的,其他方法按需實現,這些核心方法只要來完成這些重要的任務:

  • 指定能滾動的區域大小
  • 給cell提供屬性對象, 使你的layout能夠正確的擺放cell(也就是給每個cell定位)

你可以只實現這些核心方法,但如果你實現一些可選方法會讓你的layout看起來更加牛逼!

layout對象可以根據data source提供的信息創建出collection view 的layout。

你的layout通過調用collectionView 屬性方法跟data source進行通信,這些屬性在所有layout方法中都是可以訪問的。

在layout過程中,你要明白,你的collection view知道什么,不知道什么,因為collection view不能追蹤布局或者views的位置,甚至,layout對象不會限制你去調用任何collection view的方法,所以,別指望collection view幫你計算布局。

三、理解布局過程

collection view布局工作都由自定義layout對象進行處理。當collection view需要布局信息的時候,它會向layout對象要求提供這些信息。

舉個例子,collection view首次顯示或者resize的時候,它會向layout要這些信息。

你也可以調用layout對象的 invalidateLayout 方法通知collection view更新自己的布局。這個方法把存在的layout信息全部丟棄,然后layout對象會重新生成布局信息。

注意:不要把 invalidateLayout 方法跟collection view的 reloadData 方法搞混了。

不恰當地調用 invalidateLayout 將導致collection view 廢棄掉已經存在的布局,和子視圖

當然了,如果刪除、移動或者添加cell,重新計算所有的布局是有必要的。

如果data source中得數據改變了,調用 reloadData 更好

在整個布局過程中, collection view 調用layout對象的方法。

你可以在這些方法中計算cell的位置和給collection view 提供一些必要的信息,其他的方法也可能調用,但是以下幾個方法在整個布局過程中調用最為頻繁,且調用順序如下:

  1. 使用 prepareLayout 方法為布局計算做一些準備工作
  2. 使用 collectionViewContentSize 方法返回內容區域的size
  3. 使用 layoutAttributesForElementsInRect: 方法返回矩形區域內cells或者views的屬性

5-1 說明了你怎樣使用上述方法產生布局信息

5-1

prepareLayout 方法里面做布局需要的所有cells和views位置相關的計算, 最少你也要在這個方法中計算出內容區域的size,以供第二步返回使用。

collection view 使用content size 配置自己scrollview,舉個例子,當你計算的content size超過設備的屏幕大小,scrollview便能夠同時橫向和縱向移動了。 不像 UICollectionViewFlowLayout, 它不默認的調節布局使之只能一個方向滾動。

基于當前的滾動位置,collection view 會調用 layoutAttributesForElementsInRect: 方法獲取指定矩形區域內cells和views的屬性,這個指定區域跟可見區域大小可能相同,也可能不相同,返回這些信息之后,核心布局過程已經完成了。

布局完成之后,你cells和views中的屬性,會被保留,除非你或者collection view主動廢棄了這些布局。

調用 invalidateLayout 會導致布局過程重新開始,再次從 prepareLayout 開始

collection view滾動的時候,也可能會自動廢棄布局,當用戶滾動它的內容的時候,collection view會調用layout 對象的 shouldInvalidateLayoutForBoundsChange: 方法,如果該方法返回YES,便會廢棄約束。

注意記住調用 invalidateLayout 方法不會立刻開始更新布局很有用。當數據和布局不一致的時候,才需要調用這個方法,在下一個視圖更新循環中,collection view會檢查是否自己的約束需要更新,如果需要,就更新,事實上,你可以在一個很短的時間內多次調用 invalidateLayout 方法,但并不會每次都出發布局更新

四、創建布局屬性

你layout生成的屬性是UICollectionViewLayoutAttributes的實例變量,這些實例變量可以在你app不同的方法里創建。

當你的app不是處理成千上萬條數據,你完全可以在 prepareLayout 方法里面創建,因為你的布局會被緩存和引用。

但如果這樣的成本高過所得到的效率的話,那在屬性使用的時候創建也是很容易的。

不管怎樣,當你新創建一個 UICollectionViewLayoutAttributes 實例的時候,從以下幾個方法中選一個吧:

  • layoutAttributesForCellWithIndexPath:
  • layoutAttributesForSupplementaryViewOfKind:withIndexPath:
  • layoutAttributesForDecorationViewOfKind:withIndexPath:

對于不同的view,你必須使用正確的類方法,因為collection view會使用這些信息取向data source對象請求view的類型,使用不正確的方法將導致collection view創建錯誤的視圖,你想要的布局也不會出現。

創建每個屬性對象之后,設置相應地屬性到對應的view上,最少你要設置view的大小和位置,view之間有重疊的部分,你需要給 zIndex 賦值,來保證這些view的層級關系。其他屬性讓你可以控制是否可見或者外觀,是否可以按照要求改變,如果這些標準的屬性類型不滿足你的需求,你可以實現子類,擴展他們去存儲其他屬性。當你使用了子類屬性對象,你必須實現 isEqual: 方法,用來比較屬性,因為collection view一些操作用到了這個方法。

五、 準備布局(Preparing the Layout)

在布局開始的時候,layout對象會先調用 prepareLayout 方法,這個方法里面你可以計算一會兒你要用到的信息。 prepareLayout 方法并不是必須實現的,但是它給你一個機會去做一些必要地初始化計算。

這個方法調用后,你計算出來的信息必須能夠計算出collection view的content size.

六、提供布局屬性

布局的最后一步,collection view會調用 layoutAttributesForElementsInRect: 方法,這個方法的目的就是提供指定區域內cells,supplementary,或者decoration view需要的屬性。

如果是一個很大的滾動區域,collection view可能只是需要可見區域的屬性, 在圖 5-2中, 需要就是6-20和第二個headerview的布局屬性, 你必須準備好這些布局屬性。這些屬性可能用來做刪除插入動畫。

5-2

因為這個 layoutAttributesForElementsInRect 方法在 prepareLayout 之后調用,所以你應該已經有了絕大多數的信息取創建并返回需要的屬性,實現 layoutAttributesForElementsInRect 方法需要以下幾步:

  1. 遍歷所有 prepareLayout 生成的數據,決定是訪問緩存還是創建一個新的。
  2. 檢查每個item的frame,確保在 layoutAttributesForElementsInRect 給的矩形區域內(可交叉)
  3. 對于每個符合步驟2條件的item,添加對應的 UICollectionViewLayoutAttributes 對象到一個數組
  4. 返回數組給collection view

取決于你怎樣管理你的布局信息,你可能會在 prepareLayout 方法,或者在 layoutAttributesForElementsInRect 方法中創建 UICollectionViewLayoutAttributes 對象。

不管使用哪種方式,謹記效率,重復計算一個新布局屬性是非常昂貴的操作,這樣對你app的體驗是非常有害的。換個說法,當你collection view item數量巨大,你應該考慮在需要的時候才去創建這些屬性,這是一個很簡單的策略。

注意:layout對象也需要能夠為一些item立刻提供屬性,collection view可能會因為一些特殊原因,包括創建動畫,去要求這些信息

七、立刻提供布局屬性

collection view會定期向你的layout對象要求特殊的屬性,舉個例子,當你配置插入和刪除動畫的時候,collection view會要求這些信息,你的layout對象必須準備好為這些cell,supplementary,decoration提供支持布局屬性,你可以復寫一下方法取做這件事:

  • layoutAttributesForItemAtIndexPath:
  • layoutAttributesForSupplementaryViewOfKind:atIndexPath:
  • layoutAttributesForDecorationViewOfKind:atIndexPath:

有時限這些方法應該取回cell或者view的布局屬性,每個自定義布局對象都有必要實現 layoutAttributesForItemAtIndexPath: 這個方法。如果你的布局不包含任何supplementary views,你不用實現 layoutAttributesForSupplementaryViewOfKind:atIndexPath 這個方法,同樣地,如果不包含decoration views, 你也不用實現 layoutAttributesForDecorationViewOfKind:atIndexPath: 這個方法,當返回這些屬性的時候,你不應該更新這些屬性,如果你需要更改布局信息,廢棄掉這個layout 對象,讓它重新更新,重新開始一個布局過程。

八、使用你的自定義布局

這里有兩個方法可以是使用你的自定義布局:純代碼,和通過storyboard,collection view會通過一個屬性與你的自定義布局相關聯-- collectionViewLayout .

self.collectionView.collectionViewLayout = [[MyCustomLayout alloc] init];

九、讓你的自定義布局更好

為每個cell提供布局屬性是必要的,但是你的layout還有其他可以提升用戶體驗的特性,實現這些特性不是必須的,但非常建議。

十、讓插入和刪除動畫更有趣

插入和刪除cells和views是一個非常有趣的問題, 插入一個cell會造成其他cell和view布局的改變。

因為layout對象知道怎樣對已經存在的cell和view從當前位置移動到一個新位置做動畫, 但是,它并不知道新cell會被插入的位置,無動畫的插入一個新的cell,collection view為了做這個動畫,會向layout對象要求提供一系列屬性。當一個cell被刪除的時候,過程也相似。

去理解這些初始化屬性怎樣工作,看一個例子是很有幫助的,圖5-3展示了一個只有三個cell的collection view,當一個新的cell被插入的時候,layout對象會提供給collection view這個cell的初始屬性。這樣,layout對象會設置cell的位置到collection view的中間,并且把alpha值從0設置為1,在動畫期間,這個新cell會漸漸地出現移動到collection view的中央,最后的位置在右下角。

5-3

5-2展示了相關代碼

- (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;

}

注意:當,一個cell插入的時候,5-2 代碼會將所有的cell都做動畫,但第四個之前的cell已經展示完畢了,再做動畫也不合適。只為剛插入的cell做動畫,檢查一下這個方法的index path是否跟 prepareForCollectionViewUpdates: 傳入的index path一致, 如果一致,則做動畫,否則就調用super的 initialLayoutAttributesForAppearingItemAtIndexPath: 方法

刪除的處理過程跟插入的完全一致,除了你需要指定最終屬性,而不是初始實行,根據剛才的例子,如果你使用相同的屬性刪除一個cell,cell會慢慢消失同時移動到collection view的中間,在 UICollectionViewLayout 中有六個方法可用--兩個分離的方法(初始參數和最終參數)

十一、提升滾動體驗

你自定義layout對象會影響滾動的效果去創建一個更好地體驗。當滾動相關的觸摸事件結束后,scrollview會根據當前的速度和減速率決定最后靜止的內容區域,當collection view知道了這個位置,它會調用layout對象的 targetContentOffsetForProposedContentOffset:withScrollingVelocity: 方法,是否位置需要改變。

 

來自:http://www.jianshu.com/p/0a2a71e20945

 

 本文由用戶 p34p 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!