有了預估高度這個先決條件,一切都好說了.我們直接從代碼入手.
接下來我們實現一個簡單的信息展示功能,如:
每個cell里面可能只有圖或者只有文字,更多的情況是圖文并茂,但是文字的長短也是不一樣的.
創建項目和展示輸入的過程就不說了,這里只講幾個主要的部分:
- 1.最主要的當然是在我們控制器內部加上前面講的協議方法
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 55.f;
}
注意這里的預估高度當然是越接近越好,但其實還是比較隨意,即使和真實高度差大一點也沒有關系.但是還是不要寫得太小吧.
-
2.自定義cell,這里使用的是xib
cell內部控件的約束
顯示文字的label,一開始應該都會想到上下左右間距,于是這里我們暫時給label上、左、右都距離父控件為10的間距(后面會調整),然后下面距離imageView的間距也是10,imageView左邊和label左邊對齊,然后寬高固定.
接著把兩個控件連線到cell的.m文件中:
- 3.繪制cell的時候,一般情況下控制器會向cell傳遞一個數據模型,讓cell負責數據的顯示.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MessageCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MessageCell"];
cell.message = self.dataList[indexPath.row];
return cell;
}
代碼中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的高度,先看下效果:
數據的展示是沒問題了,我們開始進行關鍵的一步,自適應.
- 5.還是循著最早的思路,我們希望在繪制cell的時候拿到cell的高度.
比較好的方法是:cell在拿到數據模型并展示后,我們就可以得到cell準確的高度,這時候把它存放在數據模型里面.(放到數據模型里面的好處是:tableView在需要cell高度的時候就可以直接從數據模型里面取.)
所以我們的數據模型除了文字和圖片,需要再添加一個屬性,模型的頭文件如下:
#import <UIKit/UIKit.h>
@interface Message : NSObject
@property (nonatomic, copy) NSString *imageName;
@property (nonatomic, copy) NSString *content;
@property (nonatomic, assign) CGFloat cellHeight;
+ (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 = [UIImage imageNamed:_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];
return message.cellHeight;
}
一切看起來是那么的天衣無縫,接下來是見證奇跡的時刻:
WTF?說好的自適應呢?
其實問題出現在這里:
- (void)setMessage:(Message *)message {
_message = message;
self.contentLabel.text = _message.content;
self.contentImageView.image = [UIImage imageNamed:_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 = [UIImage imageNamed:_message.imageName];
}
else {
self.contentImageView.image = nil;
}
// 強制布局
[self layoutIfNeeded];
self.message.cellHeight = CGRectGetMaxY(self.contentImageView.frame) + 10;
}
再運行看看效果:
好像有那么點意思了,起碼對于文字和圖片齊全的模型已經可以了.然后我們處理那些特殊的情況.
還是那個setter方法里面,我們對image的有無進行判讀,如果沒有圖片,我們直接取label的底邊(加點間距)作為cellHeight,代碼如下:
- (void)setMessage:(Message *)message {
_message = message;
if (_message.content.length) {
self.contentLabel.text = _message.content;
}
else {
self.contentLabel.text = nil;
}
[self layoutIfNeeded];
if (_message.imageName.length) {
self.contentImageView.image = [UIImage imageNamed:_message.imageName];
self.message.cellHeight = CGRectGetMaxY(self.contentImageView.frame) + 10;
}
else {
self.contentImageView.image = nil;
self.message.cellHeight = CGRectGetMaxY(self.contentLabel.frame) + 10;
}
}
再看效果:
好很多了.但是還有一些細節的問題,比如:
這行沒有圖片的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中,圖片距離頂部的高度比我們期望的(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;
// 有文字的時候距離頂部是10
self.labelTopConstraint.constant = 10;
}
else {
self.contentLabel.text = nil;
// 沒文字的時候距離頂部為0
self.labelTopConstraint.constant = 0;
}
大功告成啦!
是不是發現使用AutoLayout后cell自適應的高度比設置frame時代簡單了不是一點半點.
但是,雖然用起來爽,這種方式也是有缺陷的:
1.由于cell在estimatedHeightForRow...方法中拿到的只是估計的高度,滑動屏幕的時候,tableView不斷拿到真實的高度對contentSize及滾動條的大小等重新計算,由于實際值和預估值的偏差,可能導致滾動條大小不穩定甚至明顯跳動.
2.另外,如果使用的estimatedHeightForRow...方法后,如果你想滾動到最后一行(比如聊天功能,可能在鍵盤彈上去后tableView滾到底部),也會計算不準.因為開啟估算高度胡,cell出現在屏幕上才會返回真實高度,如果根據indexPath直接跳轉到最后一行,后面的cell沒有出現在屏幕上過,依然是根據估算高度來算的,所以會導致滾動的位置不準確.
不過呢,如果對這方面要求不是特別高,一般的需求是可以滿足了.
demo地址:https://github.com/CoderAO/AutoCellHeightWithAutolayout