有了預估高度這個先決條件,一切都好說了.我們直接從代碼入手.
接下來我們實現一個簡單的信息展示功能,如:
Demo最終效果
每個cell里面可能只有圖或者只有文字,更多的情況是圖文并茂,但是文字的長短也是不一樣的.
創建項目和展示輸入的過程就不說了,這里只講幾個主要的部分:
1.最主要的當然是在我們控制器內部加上前面講的協議方法
- (CGFloat)tableView:(UITableView*)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath*)indexPath {return55.f;}
注意這里的預估高度當然是越接近越好,但其實還是比較隨意,即使和真實高度差大一點也沒有關系.但是還是不要寫得太小吧.
2.自定義cell,這里使用的是xib
cell內部控件的約束
顯示文字的label,一開始應該都會想到上下左右間距,于是這里我們暫時給label上、左、右都距離父控件為10的間距(后面會調整),然后下面距離imageView的間距也是10,imageView左邊和label左邊對齊,然后寬高固定.
接著把兩個控件連線到cell的.m文件中:
xib拖出的屬性
3.繪制cell的時候,一般情況下控制器會向cell傳遞一個數據模型,讓cell負責數據的顯示.
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {? ? MessageCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MessageCell"];? ? cell.message =self.dataList[indexPath.row];returncell;}
代碼中self.dataList是存放所有消息模型的數組.
4.來到MessageCell.m文件中,手動實現模型的setter方法:
- (void)setMessage:(Message *)message {_message= message;? ? self.contentLabel.text=_message.content;? ? self.contentImageView.image= [UIImage imageNamed:_message.imageName];}
到此,我們就完成了cell內容的基本展示.由于高度我們還沒開始適應,暫時給了一個固定的150的高度,先看下效果:
Snip20150608_18.png
數據的展示是沒問題了,我們開始進行關鍵的一步,自適應.
5.還是循著最早的思路,我們希望在繪制cell的時候拿到cell的高度.
比較好的方法是:cell在拿到數據模型并展示后,我們就可以得到cell準確的高度,這時候把它存放在數據模型里面.(放到數據模型里面的好處是:tableView在需要cell高度的時候就可以直接從數據模型里面取.)
所以我們的數據模型除了文字和圖片,需要再添加一個屬性,模型的頭文件如下:
#import@interfaceMessage:NSObject@property(nonatomic,copy)NSString*imageName;@property(nonatomic,copy)NSString*content;@property(nonatomic,assign)CGFloatcellHeight;+ (instancetype)messageWithDic:(NSDictionary*)dic;@end
tips:由于模型直接繼承自NSObject,創建的時候只包含了Fundation框架,所以添加CGFloat類型的屬性的時候會報錯,這時候只要把fundation改成UIKit就可以了(UIKit內部也包含了Fundation).
接下來我們就可以計算cellHeight的值了,還是在cell的模型setter方法里面:
- (void)setMessage:(Message *)message {? ? _message = message;self.contentLabel.text = _message.content;self.contentImageView.image = [UIImageimageNamed:_message.imageName];// 獲取imageView底部的frame再加上一些間距作為行高self.message.cellHeight =CGRectGetMaxY(self.contentImageView.frame) +10;}
同時,在控制器heightForRow...協議方法里面寫上:
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {? ? Message *message =self.dataList[indexPath.row];returnmessage.cellHeight;}
一切看起來是那么的天衣無縫,接下來是見證奇跡的時刻:
效果0.7
WTF?說好的自適應呢?
其實問題出現在這里:
- (void)setMessage:(Message *)message {? ? _message = message;self.contentLabel.text = _message.content;self.contentImageView.image = [UIImageimageNamed:_message.imageName];self.message.cellHeight =CGRectGetMaxY(self.contentImageView.frame) +10;}
我們在得到cellHeight的時候,直接是取imageView的底部+10作為行高,但是在這句之前,label和imageView剛剛拿到數據,還沒開始布局,所以我們要在獲取cellHeight之前調用layoutIfNeeded方法把他們強制布局一下. 升級后的代碼:
- (void)setMessage:(Message *)message {? ? _message = message;// 有的模型不存在文字,這里判斷一下if(_message.content.length) {self.contentLabel.text = _message.content;? ? }else{self.contentLabel.text =nil;? ? }// 有的模型不存在圖片,這里進行一下判斷if(_message.imageName.length) {self.contentImageView.image = [UIImageimageNamed:_message.imageName];? ? }else{self.contentImageView.image =nil;? ? }// 強制布局[selflayoutIfNeeded];self.message.cellHeight =CGRectGetMaxY(self.contentImageView.frame) +10;}
再運行看看效果:
效果0.8
好像有那么點意思了,起碼對于文字和圖片齊全的模型已經可以了.然后我們處理那些特殊的情況.
還是那個setter方法里面,我們對image的有無進行判讀,如果沒有圖片,我們直接取label的底邊(加點間距)作為cellHeight,代碼如下:
- (void)setMessage:(Message *)message {? ? _message = message;if(_message.content.length) {self.contentLabel.text = _message.content;? ? }else{self.contentLabel.text =nil;? ? }? ? [selflayoutIfNeeded];if(_message.imageName.length) {self.contentImageView.image = [UIImageimageNamed:_message.imageName];self.message.cellHeight =CGRectGetMaxY(self.contentImageView.frame) +10;? ? }else{self.contentImageView.image =nil;self.message.cellHeight =CGRectGetMaxY(self.contentLabel.frame) +10;? ? }}
再看效果:
效果0.9
好很多了.但是還有一些細節的問題,比如:
沒有完全適應的cell
這行沒有圖片的cell,我們設置行高是label底部加10,但一看這個距離明顯是大于10了.當把這行cell滑出屏幕再滑回來,又恢復正常.
這個其實是label的問題.
目前我們在label身上設置的和寬度有關的約束是左右距離父控件各為10,但這種約束算出來的label的高度有時候會不準,所以我們需要給label再設定一個屬性:
在cell的awakeFromNib:方法里面:
- (void)awakeFromNib {? ? self.contentLabel.preferredMaxLayoutWidth = [UIScreen mainScreen].bounds.size.width-20;}
這個屬性表示設置lable文字的最大寬度,是專門為多行label準備的,使用這個屬性可以準確算出label的高度.ps:設置了這個屬性后,label右邊的約束可以省略不寫,label仍然可以換行顯示.
完成90%了,還剩最后一個問題:
只有圖片的cell
在只有圖片沒有文字的cell中,圖片距離頂部的高度比我們期望的(10)略高(其實是20),因為這時候沒有文字,所以label的高度自動變為0,但是label頂部距離cell上邊還有10,label底部距離imageView還有10,加起來就是20的距離.
這個問題我們可以這樣解決:當沒有文字的時候,我們調整label距離頂部的約束為0,有文字的時候再變回10.所以需要把表示label距離cell頂部的約束從xib中拖出來.
然后在setter方法中分別進行判斷和設置:
if(_message.content.length) {self.contentLabel.text = _message.content;//有文字的時候距離頂部是10self.labelTopConstraint.constant =10;}else{self.contentLabel.text =nil;//沒文字的時候距離頂部為0self.labelTopConstraint.constant =0; }
大功告成啦!
是不是發現使用AutoLayout后cell自適應的高度比設置frame時代簡單了不是一點半點.
但是,雖然用起來爽,這種方式也是有缺陷的:
1.由于cell在estimatedHeightForRow...方法中拿到的只是估計的高度,滑動屏幕的時候,tableView不斷拿到真實的高度對contentSize及滾動條的大小等重新計算,由于實際值和預估值的偏差,可能導致滾動條大小不穩定甚至明顯跳動.
2.另外,如果使用的estimatedHeightForRow...方法后,如果你想滾動到最后一行(比如聊天功能,可能在鍵盤彈上去后tableView滾到底部),也會計算不準.因為開啟估算高度胡,cell出現在屏幕上才會返回真實高度,如果根據indexPath直接跳轉到最后一行,后面的cell沒有出現在屏幕上過,依然是根據估算高度來算的,所以會導致滾動的位置不準確.
不過呢,如果對這方面要求不是特別高,一般的需求是可以滿足了.