WWDC14_226: What's New in Table and Collection Views

前言:本來(lái)我昨天遇到一個(gè)需要?jiǎng)討B(tài)調(diào)整 UICollectionViewCell 尺寸和布局的需求,自己手動(dòng)實(shí)現(xiàn)了,想起來(lái)去年 iOS 8 中的 Self-sizing cells,于是過(guò)來(lái)學(xué)習(xí)一下。發(fā)現(xiàn)這個(gè)新特性與我的需求不怎么搭配,但是這是個(gè)很有意思并且很實(shí)用的新特性。

首先來(lái)看看應(yīng)用場(chǎng)景:
下面兩種布局要怎么實(shí)現(xiàn)?要按以往的方法,前者需要在 delegate 中的– tableView:heightForRowAtIndexPath:根據(jù) UILabel 的內(nèi)容來(lái)計(jì)算每個(gè) cell 的高度(此話有點(diǎn)不對(duì),因?yàn)檫@個(gè)方法實(shí)在那個(gè) cellForXXX 的方法前調(diào)用的,不知道具體內(nèi)容是沒(méi)法計(jì)算的,解決方案可以參考這篇博客),后者需要自定義布局來(lái)計(jì)算每個(gè) cell 的大小,兩者都比較麻煩,而且很容易造成性能低下,特別是后者。但是利用新特性,在設(shè)定了相應(yīng)的約束的前提下,僅僅需要幾行代碼就可以搞定,準(zhǔn)確來(lái)說(shuō),前者三行代碼,后者僅需一行代碼,還避免老方法的性能缺陷,酷到?jīng)]朋友。另外,上面的 tableview 中的字體是動(dòng)態(tài)變化的,只要用戶在設(shè)置中更改字體大小,這里也會(huì)做出相應(yīng)的調(diào)整,是不是很方便。這也是 iOS 8 中的新特性。

動(dòng)態(tài)高度的 TableViewCell

動(dòng)態(tài)寬度的 CollectionViewCell

筆記內(nèi)容

  • 動(dòng)態(tài)類型適應(yīng)(動(dòng)態(tài)字體, TableView 以及 CollectionView 都適用)
  • Self-sizing cells(兩者都適用)
  • CollectionView 智能布局更新

1. 動(dòng)態(tài)類型適應(yīng)(Dynamic Type adoption)

在 iOS 8 中允許應(yīng)用使用動(dòng)態(tài)字體,在通用-輔助功能-更大字體的設(shè)置中,可以為支持動(dòng)態(tài)字體的應(yīng)用設(shè)置字體大小了。而在支持動(dòng)態(tài)字體的應(yīng)用中,TableView 中使用了動(dòng)態(tài)字體的地方將會(huì)根據(jù)字體的大小來(lái)自動(dòng)調(diào)整大小和布局,真是很方便,主講工程師也推薦大家使用動(dòng)態(tài)字體。工程師并未提及 CollectionView 支持 Dynamic Type adoption,根據(jù)我的試驗(yàn),CollectionView 也是支持的,但是沒(méi)有 TableView 中那么完美,不像后者在在設(shè)置中調(diào)整好后再次進(jìn)入應(yīng)用即可調(diào)整,前者需要?dú)⒌魬?yīng)用再次進(jìn)入應(yīng)用才會(huì)調(diào)整大小,嚴(yán)格來(lái)說(shuō)不是 Dynamic Type adoption。
支持 Dynamic Type adoption 的前提是使用預(yù)定義的字體樣式,也就是說(shuō)你使用了其他的字體是不支持動(dòng)態(tài)適應(yīng)的。有兩者方法可以設(shè)置:
1.在代碼中通過(guò) [UIFont preferredFontForTextStyle:] 來(lái)設(shè)定;
2.在 IB 中選擇預(yù)定義字體樣式。
預(yù)定義的字體支持六種樣式:

NSString *const UIFontTextStyleHeadline;
NSString *const UIFontTextStyleSubheadline;
NSString *const UIFontTextStyleBody;
NSString *const UIFontTextStyleFootnote;
NSString *const UIFontTextStyleCaption1;
NSString *const UIFontTextStyleCaption2;

2. Self-sizing Cells

1) TableView

在 TableView 中有兩種手法來(lái)調(diào)整每一行的高度:
1.property:rowHeight;
2.delegate: - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

第一個(gè)方法只能將所有行設(shè)置為同一高度;第二個(gè)方法有很大的性能缺陷,因?yàn)?TableView 每次生成 cell 的時(shí)候都要向 delegate 詢問(wèn)這個(gè)方法。
新特性 Self-sizing Cells 的實(shí)現(xiàn)代碼擁有良好的性能,且僅需幾行代碼:

self.tableView.estimatedRowHeight = 44.0f;
self.tableView.rowHeight = UITableViewAutomaticDimension;

而要實(shí)現(xiàn)開頭圖中的布局,還需要對(duì) UILabel 進(jìn)行設(shè)置:

label.numberOfLines = 0//使UILabel 的高度根據(jù)其內(nèi)容來(lái)變化` 

estimatedRowHeight 是 iOS 7 中新增的屬性,顧名思義,預(yù)計(jì)高度;同時(shí)需要將 rowHeight 屬性設(shè)置為 UITableViewAutomaticDimension,意思是告訴 TableView 沒(méi)有 rowHeight,請(qǐng)根據(jù)其他信息來(lái)判斷每一行的高度。另外,TableView 還有 HeaderView 和 FooterView,也有類似的屬性來(lái)達(dá)到同樣的效果。

實(shí)現(xiàn) Self-sizing Cells 的關(guān)鍵屬性:estimatedRowHeight。當(dāng)滾動(dòng) TableView 使得 cell 即將顯示在屏幕上的時(shí)候,生成一個(gè) cell,cell 的高度根據(jù) estimatedRowHeight 或者 delegate 的-tableView: heightForRowAtIndexPath: 返回的高度來(lái)決定。使用 Self-sizing Cells 時(shí),則是根據(jù) estimatedRowHeight 來(lái)決定(此時(shí)在 delegate 中不要實(shí)現(xiàn)對(duì)應(yīng)的方法),當(dāng)生成了 cell 后,向 cell 詢問(wèn)它到底應(yīng)該有多高(在下面講解實(shí)現(xiàn)機(jī)制),如果結(jié)果和 estimatedRowHeight 不一樣,則調(diào)整 TableView 的 contentSize,最后將 cell 顯示在屏幕上。
怎么知道 cell 到底應(yīng)該有多高呢?有兩種方法可以知道,而這也是實(shí)現(xiàn) Self-sizing Cells 的前提條件:
根據(jù) contentView 中的約束,TableView 向 cell 發(fā)送 -systemLayoutSizeFittingSize: 消息來(lái)得到它應(yīng)該具備的高度;如果你沒(méi)有添加約束,還有一個(gè)機(jī)會(huì)來(lái)知道這個(gè)高度,systemLayoutSizeFittingSize: 會(huì)調(diào)用 - (CGSize)sizeThatFits:(CGSize)size,這時(shí)候就需要覆寫該方法來(lái)手工計(jì)算了。

2) CollectionView

從這個(gè)部分開始是另外一個(gè)工程師講的,應(yīng)該是一位印度工程師,那口音,以后我搞英文演講也不是夢(mèng)了。
TableView 中 cell 的寬度是相對(duì)而言是個(gè)固定值,高度是可變的,而 CollectionView 則需要兩個(gè)方向的尺寸才能工作。在 CollectionView 中,所有 cells 和 supplementary view 以及 decoration view 的尺寸、位置以及其他布局屬性都是由 CollectionView 的 layout 對(duì)象決定。默認(rèn)情況下,CollectionView 采用FlowLayout 布局,與 TableView 的 estimatedRowHeight 屬性對(duì)應(yīng)的是 UICollectionViewFlowLayout 中新的 estimatedItemSize 屬性。

1)FlowLayout
在 CollectionView 中使用 Self-sizing Cells 的前提條件同 TableView 類似,對(duì) cell 的 contentView 添加約束條件或是重寫 - (CGSize)sizeThatFits:(CGSize)size,如果采用后者,則還會(huì)遇到 rotation 的問(wèn)題,比較麻煩,推薦使用約束。注意,如果約束條件并不是非常嚴(yán)格,動(dòng)態(tài)尺寸布局就不會(huì)那么完整,比如,在開頭的布局中,只對(duì)寬度進(jìn)行約束的話,那么在高度方面就不會(huì)進(jìn)行動(dòng)態(tài)適應(yīng)了。
在 CollectionView 中使用 Self-sizing Cells,只需要將 FlowLayout 的 estimatedItemSize 指定為非零尺寸即可。一行代碼搞定,在- viewDidLoad:中:

UICollectionViewFlowLayout *selfFlowLayout = (UICollectionViewFlowLayout *)self.collectionViewLayout;
selfFlowLayout.estimatedItemSize = CGSizeMake(50, 50);

好吧,兩行。具體的 cell 的尺寸則會(huì)根據(jù)約束條件以及 estimatedItemSize 綜合來(lái)考慮。

  1. 自定義 layout
    如果不使用 FlowLayout 或者其子類而自定義其他布局的話,也可以使用 Self-sizing cells,需要重寫以下方法,這些是 iOS8 新增的方法:
    • shouldInvalidateLayoutForPreferredLayoutAttributes:withOriginalAttributes:
    • invalidationContextForPreferredLayoutAttributes:withOriginalAttributes:
      其中的 PreferredLayoutAttributes 其實(shí)是指 UICollectionReusableView 中的新增方法:
    • preferredLayoutAttributesFittingAttributes:
      該方法使得 cell 有機(jī)會(huì)在應(yīng)用 layout 返回的布局前做出最后一次修改。
      實(shí)際上 CollectionView 的 Self-sizing Cells 與 UICollectionViewLayoutInvalidationContext 類有較大關(guān)系,這就引出下面了的內(nèi)容。

CollectionView 的智能布局更新

iOS 8 為 CollectionView 提供了更細(xì)節(jié)化而且更全面的布局控制,對(duì)于提升自定義布局的性能很有幫助。

1)布局策略
Cell Size Strategies

上面的圖本來(lái)是視頻中工程師講述 CollectionView 中 Self-sizing Cells 的截圖,放在這里是想說(shuō)明 cell 的布局是如何驅(qū)動(dòng)的。圖中第一種方法就是在 iOS 6 中隨著 UICollectionView 一起推出的 UICollectionViewLayout 類,它決定了 cell 以及 SupplementaryView 的位置、大小、alpha以及其他布局信息,是以往實(shí)現(xiàn)自定義布局的唯一選擇;Self-sizing Cells 就是今天講到的新特性,利用約束條件或是重寫 - sizeThatFits: 結(jié)合新增的 estimatedItemSize 屬性實(shí)現(xiàn)動(dòng)態(tài)布局;第三種則是利用了 cell 的父類 UICollectionReusableView 的新增方法
- preferredLayoutAttributesFittingAttributes: 在應(yīng)用 Self-sizing Cells 的布局信息前最后一次修改布局信息。后面兩種都是 iOS 8 中的新特性:

上面圖中三種策略的布局

FlowLayout 中的 estimatedItemSize 在 Self-sizing Cells 中起到了什么作用呢?與 TableView 中的 estimatedRowHeight 類似,只是由于UICollectionView 多了布局對(duì)象以及 cell 多出了一個(gè)維度的尺寸變化,estimatedItemSize 參與的布局過(guò)程比 estimatedRowHeight 在 TableView 中更加復(fù)雜了。首先由 CollectionView 的 FlowLayout 對(duì)象根據(jù) estimatedItemSize 以及其他信息計(jì)算出 cell 的布局信息,CollectionView 根據(jù)數(shù)據(jù)源重用或生成 cell,Self-sizing Cells 機(jī)制在這里發(fā)揮作用,實(shí)現(xiàn)手法和前面提到的一樣:利用 AutoLayout 或是 -sizeThatFits:;或者由 - preferredLayoutAttributesFittingAttributes: 做出最終修改,CollectionView 將更新的布局信息反饋給 FlowLayout 對(duì)象,后者響應(yīng)更新的布局信息,最后向 CollectionView 提供最終的布局信息將 cell 顯示在屏幕上。

2)布局更新(Layout Invalidation)

I) 傳統(tǒng)的布局更新
UICollectionViewLayout 使用-invalidateLayout來(lái)更新布局,布局對(duì)象會(huì)依次調(diào)用以下方法:

- prepareLayout
- collectionViewContentSize
- layoutAttributesForElementsInRect:

再次更新布局時(shí),上面的方法再循環(huán)一次。在 iOS 8 之前,更新布局只能對(duì) CollectionView 上的所有 elements 進(jìn)行布局更新。顯然,這里面有很多本不需要進(jìn)行計(jì)算的,也是性能隱患;從 iOS 8 開始可以針對(duì)局部的布局進(jìn)行更新了,不需要重新計(jì)算所有 elements 的布局信息,這對(duì)性能的提升有很大的幫助,主講工程師稱之為「Smart Invalidation」。

II) 新的布局系統(tǒng)
實(shí)現(xiàn)智能布局更新的關(guān)鍵在于 UICollectionViewLayoutInvalidationContext 類,這并不是 iOS 8 才推出的,而是在 iOS 7 中出現(xiàn)的。InvalidationContext 類用來(lái)提供布局更新的信息,但在 iOS 7 中只是用來(lái)重構(gòu)原來(lái)的布局系統(tǒng),并沒(méi)有提供新的特性。

在 iOS 7 中 InvalidationContext 類僅有兩個(gè)公開接口, 而且調(diào)用者無(wú)法設(shè)置;在添加或是刪除 items 時(shí),由 CollectionView 來(lái)自動(dòng)設(shè)置:

//指示是否有增減 element
@property (nonatomic, readonly) BOOL invalidateDataSourceCounts;
//指示是否需要更新全部的布局信息
@property (nonatomic, readonly) BOOL invalidateEverything;

為了搭建新的布局系統(tǒng),在 iOS 7 中 UICollectionViewLayout 類新增了三個(gè)方法,用來(lái)配合 InvalidationContext 類使用:

+ invalidationContextClass    //指定布局環(huán)境
- invalidateLayoutWithContext://根據(jù)提供的布局環(huán)境來(lái)更新布局`

對(duì) bounds 變化時(shí)的支持:

- invalidationContextForBoundsChange://返回一個(gè)包含了所需信息的InvalidationContext對(duì)象

FlowLayout 在處理 rotation 時(shí)使用了 InvalidationContext 來(lái)更新布局,流程如下:

//首先詢問(wèn)是否需要更新布局
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
//如果需要更新布局,接下來(lái)調(diào)用下面的方法獲取InvalidationContext對(duì)象,如果重寫下面的方法,需要調(diào)用父類的實(shí)現(xiàn)
- (UICollectionViewLayoutInvalidationContext *)invalidationContextForBoundsChange:(CGRect)newBounds
//最后利用上面獲取的InvalidationContext 來(lái)更新布局
- (void)invalidateLayoutWithContext:(UICollectionViewLayoutInvalidationContext *)context

Note: 在 FlowLayout 中使用 Self-sizing Cells 來(lái)實(shí)現(xiàn)文章開頭的布局的時(shí)候,其處理 rotation 的性能相當(dāng)?shù)牟?。在我?iPad mini 上,要經(jīng)過(guò)3秒左右才能響應(yīng)屏幕的旋轉(zhuǎn)。說(shuō)好的提升性能呢,不知道是哪里出了問(wèn)題,不然也不至于把這個(gè)特性放出來(lái)。在不使用 Self-sizing 的時(shí)候,稍好一點(diǎn),不知道是不是字符串的處理有性能缺陷。也有可能我的機(jī)器太老了。

III) 智能布局更新
有了 iOS 7 中打下的基礎(chǔ),iOS 8 在 InvalidationContext 類中增加了新的 API,可以對(duì)針對(duì)局部的布局更新了:

- invalidateItemsAtIndexPaths:
- invalidateSupplementaryElementsOfKind:atIndexPaths:
- invalidateDecorationElementsOfKind:atIndexPaths:

新的 API 和 CollectionView 更新 cell 內(nèi)容的 API 類似,只是控制的尺度更細(xì)微,能夠具體到單個(gè)的 element 的布局,在以往更新布局只能對(duì)整體進(jìn)行操作。利用新的 API,實(shí)現(xiàn) Photos 應(yīng)用中時(shí)間線里面的浮動(dòng) Header 就相當(dāng)簡(jiǎn)單了,調(diào)用 - invalidateSupplementaryElementsOfKind:atIndexPaths: 使得對(duì)應(yīng)位置的 HeaderView 布局失效即可。
Note:說(shuō)得好聽,但目前為止我還沒(méi)有利用這個(gè)特性成功地實(shí)現(xiàn)這個(gè)浮動(dòng) Header,一般稱之為 sticky header。目前我用 google 搜出來(lái)的答案基本抄的這個(gè)答案下的代碼。而這個(gè)嘗試?yán)?InvalidContext 的帖子,特別是那個(gè)meelawsh的回答,經(jīng)試驗(yàn)完全是鬼扯好吧。可是我對(duì)這個(gè)帖子沒(méi)有任何權(quán)限,既不能贊同反對(duì),甚至不能評(píng)論?。?!而且那個(gè)家伙甚至連個(gè)郵箱都沒(méi)有留下!
5/12 更新:被這個(gè)東西折騰了幾天,發(fā)現(xiàn)用這個(gè)來(lái)提升性能比寫一個(gè)浮動(dòng) header 布局麻煩多了。需要考慮屏幕上的 HeaderView 是否超出了屏幕從而單獨(dú)更新這個(gè) HeaderView 的布局,以及下方的 HeaderView 上升到當(dāng)前懸浮的 HeaderView 的位置時(shí)。

IV) 對(duì) Self-sizing Cells 的支持
上面的章節(jié)里提到在自定義 layout 中使用 Self-sizing Cells,需要重寫以下兩個(gè)方法:
- shouldInvalidateLayoutForPreferredLayoutAttributes:withOriginalAttributes:
- invalidationContextForPreferredLayoutAttributes:withOriginalAttributes:
在第二個(gè)方法中,返回一個(gè) InvalidationContext 對(duì)象。在 iOS 8 中 InvalidationContext 類新增了兩個(gè)屬性:

@property(nonatomic) CGPoint contentOffsetAdjustment;
@property(nonatomic) CGPoint contentSizeAdjustment

顧名思義,cell 的尺寸發(fā)生變化,那么在 UICollectionViewcontentOffsetcontentSize 都要發(fā)生變化。后者很好理解,cell 變大變小,需要用來(lái)展示內(nèi)容的面積也回發(fā)生變化,前者呢,可以看看這篇文章:理解 ScrollView。在第二個(gè)方法返回的 InvalidationContext 對(duì)象需要提供這兩個(gè)信息。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,195評(píng)論 4 61
  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一種新的協(xié)議。它實(shí)...
    香橙柚子閱讀 24,730評(píng)論 8 183
  • 洗腦~ 2016年9月23日 東莞 早上,六點(diǎn)多起床,我媳婦說(shuō)她也要跟我一起去上課。 我說(shuō)你上什么課啊,要提前報(bào)名...
    laiyuchao閱讀 216評(píng)論 0 0
  • 轉(zhuǎn)自:http://segmentfault.com/a/1190000000264347 Vagrant 是一款...
    1angxi閱讀 407評(píng)論 0 3
  • 我還記得你是紅屋子里的白胖子;我還記得你是中學(xué)課文中的落花生;我還記得你是科學(xué)課里一年生草本植物。我還記得你是你又...
    淡之水閱讀 387評(píng)論 0 1

友情鏈接更多精彩內(nèi)容