前言:本來我昨天遇到一個(gè)需要?jiǎng)討B(tài)調(diào)整 UICollectionViewCell 尺寸和布局的需求,自己手動(dòng)實(shí)現(xiàn)了,想起來去年 iOS 8 中的 Self-sizing cells绷柒,于是過來學(xué)習(xí)一下茶行。發(fā)現(xiàn)這個(gè)新特性與我的需求不怎么搭配构诚,但是這是個(gè)很有意思并且很實(shí)用的新特性。
首先來看看應(yīng)用場景:
下面兩種布局要怎么實(shí)現(xiàn)峻呕?要按以往的方法,前者需要在 delegate 中的– tableView:heightForRowAtIndexPath:
根據(jù) UILabel 的內(nèi)容來計(jì)算每個(gè) cell 的高度(此話有點(diǎn)不對惑灵,因?yàn)檫@個(gè)方法實(shí)在那個(gè) cellForXXX 的方法前調(diào)用的山上,不知道具體內(nèi)容是沒法計(jì)算的,解決方案可以參考這篇博客)英支,后者需要自定義布局來計(jì)算每個(gè) cell 的大小佩憾,兩者都比較麻煩,而且很容易造成性能低下干花,特別是后者妄帘。但是利用新特性,在設(shè)定了相應(yīng)的約束的前提下池凄,僅僅需要幾行代碼就可以搞定抡驼,準(zhǔn)確來說,前者三行代碼肿仑,后者僅需一行代碼致盟,還避免老方法的性能缺陷碎税,酷到?jīng)]朋友。另外馏锡,上面的 tableview 中的字體是動(dòng)態(tài)變化的雷蹂,只要用戶在設(shè)置中更改字體大小,這里也會(huì)做出相應(yīng)的調(diào)整杯道,是不是很方便匪煌。這也是 iOS 8 中的新特性。
筆記內(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ù)字體的大小來自動(dòng)調(diào)整大小和布局驳规,真是很方便,主講工程師也推薦大家使用動(dòng)態(tài)字體创肥。工程師并未提及 CollectionView 支持 Dynamic Type adoption达舒,根據(jù)我的試驗(yàn),CollectionView 也是支持的叹侄,但是沒有 TableView 中那么完美巩搏,不像后者在在設(shè)置中調(diào)整好后再次進(jìn)入應(yīng)用即可調(diào)整,前者需要?dú)⒌魬?yīng)用再次進(jìn)入應(yīng)用才會(huì)調(diào)整大小趾代,嚴(yán)格來說不是 Dynamic Type adoption贯底。
支持 Dynamic Type adoption 的前提是使用預(yù)定義的字體樣式,也就是說你使用了其他的字體是不支持動(dòng)態(tài)適應(yīng)的撒强。有兩者方法可以設(shè)置:
1.在代碼中通過 [UIFont preferredFontForTextStyle:]
來設(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 中有兩種手法來調(diào)整每一行的高度:
1.property:rowHeight
;
2.delegate: - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
第一個(gè)方法只能將所有行設(shè)置為同一高度飘哨;第二個(gè)方法有很大的性能缺陷胚想,因?yàn)?TableView 每次生成 cell 的時(shí)候都要向 delegate 詢問這個(gè)方法。
新特性 Self-sizing Cells 的實(shí)現(xiàn)代碼擁有良好的性能芽隆,且僅需幾行代碼:
self.tableView.estimatedRowHeight = 44.0f;
self.tableView.rowHeight = UITableViewAutomaticDimension;
而要實(shí)現(xiàn)開頭圖中的布局浊服,還需要對 UILabel 進(jìn)行設(shè)置:
label.numberOfLines = 0//使UILabel 的高度根據(jù)其內(nèi)容來變化`
estimatedRowHeight
是 iOS 7 中新增的屬性,顧名思義胚吁,預(yù)計(jì)高度牙躺;同時(shí)需要將 rowHeight 屬性設(shè)置為 UITableViewAutomaticDimension
,意思是告訴 TableView 沒有 rowHeight腕扶,請根據(jù)其他信息來判斷每一行的高度孽拷。另外,TableView 還有 HeaderView 和 FooterView半抱,也有類似的屬性來達(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:
返回的高度來決定激蹲。使用 Self-sizing Cells 時(shí),則是根據(jù) estimatedRowHeight 來決定(此時(shí)在 delegate 中不要實(shí)現(xiàn)對應(yīng)的方法)江掩,當(dāng)生成了 cell 后,向 cell 詢問它到底應(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:
消息來得到它應(yīng)該具備的高度火本;如果你沒有添加約束,還有一個(gè)機(jī)會(huì)來知道這個(gè)高度聪建,systemLayoutSizeFittingSize:
會(huì)調(diào)用 - (CGSize)sizeThatFits:(CGSize)size
钙畔,這時(shí)候就需要覆寫該方法來手工計(jì)算了。
2) CollectionView
從這個(gè)部分開始是另外一個(gè)工程師講的金麸,應(yīng)該是一位印度工程師擎析,那口音,以后我搞英文演講也不是夢了挥下。
TableView 中 cell 的寬度是相對而言是個(gè)固定值揍魂,高度是可變的,而 CollectionView 則需要兩個(gè)方向的尺寸才能工作棚瘟。在 CollectionView 中现斋,所有 cells 和 supplementary view 以及 decoration view 的尺寸、位置以及其他布局屬性都是由 CollectionView 的 layout 對象決定偎蘸。默認(rèn)情況下庄蹋,CollectionView 采用FlowLayout 布局,與 TableView 的 estimatedRowHeight
屬性對應(yīng)的是 UICollectionViewFlowLayout
中新的 estimatedItemSize
屬性禀苦。
1)FlowLayout
在 CollectionView 中使用 Self-sizing Cells 的前提條件同 TableView 類似蔓肯,對 cell 的 contentView 添加約束條件或是重寫 - (CGSize)sizeThatFits:(CGSize)size
,如果采用后者振乏,則還會(huì)遇到 rotation 的問題蔗包,比較麻煩,推薦使用約束慧邮。注意调限,如果約束條件并不是非常嚴(yán)格舟陆,動(dòng)態(tài)尺寸布局就不會(huì)那么完整,比如耻矮,在開頭的布局中秦躯,只對寬度進(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
綜合來考慮琢唾。
- 自定義 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é)化而且更全面的布局控制工扎,對于提升自定義布局的性能很有幫助。
1)布局策略
上面的圖本來是視頻中工程師講述 CollectionView 中 Self-sizing Cells 的截圖泌豆,放在這里是想說明 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
多了布局對象以及 cell 多出了一個(gè)維度的尺寸變化袱结,estimatedItemSize
參與的布局過程比 estimatedRowHeight
在 TableView 中更加復(fù)雜了亮隙。首先由 CollectionView 的 FlowLayout 對象根據(jù) estimatedItemSize
以及其他信息計(jì)算出 cell 的布局信息,CollectionView 根據(jù)數(shù)據(jù)源重用或生成 cell垢夹,Self-sizing Cells 機(jī)制在這里發(fā)揮作用溢吻,實(shí)現(xiàn)手法和前面提到的一樣:利用 AutoLayout 或是 -sizeThatFits:
;或者由 - preferredLayoutAttributesFittingAttributes:
做出最終修改,CollectionView 將更新的布局信息反饋給 FlowLayout 對象促王,后者響應(yīng)更新的布局信息犀盟,最后向 CollectionView 提供最終的布局信息將 cell 顯示在屏幕上。
2)布局更新(Layout Invalidation)
I) 傳統(tǒng)的布局更新
UICollectionViewLayout
使用-invalidateLayout
來更新布局蝇狼,布局對象會(huì)依次調(diào)用以下方法:
- prepareLayout
- collectionViewContentSize
- layoutAttributesForElementsInRect:
再次更新布局時(shí)阅畴,上面的方法再循環(huán)一次。在 iOS 8 之前迅耘,更新布局只能對 CollectionView 上的所有 elements 進(jìn)行布局更新贱枣。顯然,這里面有很多本不需要進(jìn)行計(jì)算的颤专,也是性能隱患冯事;從 iOS 8 開始可以針對局部的布局進(jìn)行更新了,不需要重新計(jì)算所有 elements 的布局信息血公,這對性能的提升有很大的幫助,主講工程師稱之為「Smart Invalidation」缓熟。
II) 新的布局系統(tǒng)
實(shí)現(xiàn)智能布局更新的關(guān)鍵在于 UICollectionViewLayoutInvalidationContext
類累魔,這并不是 iOS 8 才推出的,而是在 iOS 7 中出現(xiàn)的够滑。InvalidationContext 類用來提供布局更新的信息垦写,但在 iOS 7 中只是用來重構(gòu)原來的布局系統(tǒng),并沒有提供新的特性彰触。
在 iOS 7 中 InvalidationContext 類僅有兩個(gè)公開接口, 而且調(diào)用者無法設(shè)置梯投;在添加或是刪除 items 時(shí),由 CollectionView 來自動(dòng)設(shè)置:
//指示是否有增減 element
@property (nonatomic, readonly) BOOL invalidateDataSourceCounts;
//指示是否需要更新全部的布局信息
@property (nonatomic, readonly) BOOL invalidateEverything;
為了搭建新的布局系統(tǒng)况毅,在 iOS 7 中 UICollectionViewLayout
類新增了三個(gè)方法分蓖,用來配合 InvalidationContext 類使用:
+ invalidationContextClass //指定布局環(huán)境
- invalidateLayoutWithContext://根據(jù)提供的布局環(huán)境來更新布局`
對 bounds 變化時(shí)的支持:
- invalidationContextForBoundsChange://返回一個(gè)包含了所需信息的InvalidationContext對象
FlowLayout 在處理 rotation 時(shí)使用了 InvalidationContext 來更新布局,流程如下:
//首先詢問是否需要更新布局
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
//如果需要更新布局尔许,接下來調(diào)用下面的方法獲取InvalidationContext對象么鹤,如果重寫下面的方法,需要調(diào)用父類的實(shí)現(xiàn)
- (UICollectionViewLayoutInvalidationContext *)invalidationContextForBoundsChange:(CGRect)newBounds
//最后利用上面獲取的InvalidationContext 來更新布局
- (void)invalidateLayoutWithContext:(UICollectionViewLayoutInvalidationContext *)context
Note: 在 FlowLayout 中使用 Self-sizing Cells 來實(shí)現(xiàn)文章開頭的布局的時(shí)候味廊,其處理 rotation 的性能相當(dāng)?shù)牟钫籼稹T谖业?iPad mini 上,要經(jīng)過3秒左右才能響應(yīng)屏幕的旋轉(zhuǎn)余佛。說好的提升性能呢柠新,不知道是哪里出了問題,不然也不至于把這個(gè)特性放出來辉巡。在不使用 Self-sizing 的時(shí)候恨憎,稍好一點(diǎn),不知道是不是字符串的處理有性能缺陷红氯。也有可能我的機(jī)器太老了框咙。
III) 智能布局更新
有了 iOS 7 中打下的基礎(chǔ)咕痛,iOS 8 在 InvalidationContext 類中增加了新的 API,可以對針對局部的布局更新了:
- invalidateItemsAtIndexPaths:
- invalidateSupplementaryElementsOfKind:atIndexPaths:
- invalidateDecorationElementsOfKind:atIndexPaths:
新的 API 和 CollectionView 更新 cell 內(nèi)容的 API 類似喇嘱,只是控制的尺度更細(xì)微茉贡,能夠具體到單個(gè)的 element 的布局,在以往更新布局只能對整體進(jìn)行操作者铜。利用新的 API腔丧,實(shí)現(xiàn) Photos 應(yīng)用中時(shí)間線里面的浮動(dòng) Header 就相當(dāng)簡單了,調(diào)用 - invalidateSupplementaryElementsOfKind:atIndexPaths:
使得對應(yīng)位置的 HeaderView 布局失效即可作烟。
Note:說得好聽愉粤,但目前為止我還沒有利用這個(gè)特性成功地實(shí)現(xiàn)這個(gè)浮動(dòng) Header,一般稱之為 sticky header拿撩。目前我用 google 搜出來的答案基本抄的這個(gè)答案下的代碼衣厘。而這個(gè)嘗試?yán)?InvalidContext 的帖子,特別是那個(gè)meelawsh的回答压恒,經(jīng)試驗(yàn)完全是鬼扯好吧影暴。可是我對這個(gè)帖子沒有任何權(quán)限探赫,既不能贊同反對型宙,甚至不能評論!B追汀妆兑!而且那個(gè)家伙甚至連個(gè)郵箱都沒有留下!
5/12 更新:被這個(gè)東西折騰了幾天毛仪,發(fā)現(xiàn)用這個(gè)來提升性能比寫一個(gè)浮動(dòng) header 布局麻煩多了搁嗓。需要考慮屏幕上的 HeaderView 是否超出了屏幕從而單獨(dú)更新這個(gè) HeaderView 的布局,以及下方的 HeaderView 上升到當(dāng)前懸浮的 HeaderView 的位置時(shí)箱靴。
IV) 對 Self-sizing Cells 的支持
上面的章節(jié)里提到在自定義 layout 中使用 Self-sizing Cells谱姓,需要重寫以下兩個(gè)方法:
- shouldInvalidateLayoutForPreferredLayoutAttributes:withOriginalAttributes:
- invalidationContextForPreferredLayoutAttributes:withOriginalAttributes:
在第二個(gè)方法中,返回一個(gè) InvalidationContext 對象刨晴。在 iOS 8 中 InvalidationContext 類新增了兩個(gè)屬性:
@property(nonatomic) CGPoint contentOffsetAdjustment;
@property(nonatomic) CGPoint contentSizeAdjustment
顧名思義屉来,cell 的尺寸發(fā)生變化,那么在 UICollectionView
的 contentOffset
和 contentSize
都要發(fā)生變化狈癞。后者很好理解茄靠,cell 變大變小,需要用來展示內(nèi)容的面積也回發(fā)生變化蝶桶,前者呢慨绳,可以看看這篇文章:理解 ScrollView。在第二個(gè)方法返回的 InvalidationContext 對象需要提供這兩個(gè)信息。