在使用 table view 的時(shí)侯經(jīng)常會(huì)遇到這樣的需求:table view 的 cell 中的內(nèi)容是動(dòng)態(tài)的,導(dǎo)致在開發(fā)的時(shí)候不知道一個(gè) cell 的高度具體是多少,所以需要提供一個(gè)計(jì)算 cell 高度的算法,在每次加載到這個(gè) cell 的時(shí)候計(jì)算出 cell 真正的高度。
在 ios 8 之前
沒(méi)有使用 autolayout 的情況下,需要實(shí)現(xiàn) table view delegate 的 tableview(tableview: uitableview, heightforrowatindexpath indexpath: nsindexpath) -> cgfloat 方法,在這個(gè)方法中計(jì)算并返回 cell 的高度。比如,我有一個(gè)可以顯示任意行數(shù)的純文本 cell,計(jì)算 cell 的代碼可以是這樣:
override func tableview(tableview: uitableview, heightforrowatindexpath indexpath: nsindexpath) -> cgfloat {
let content = self.datas[indexpath.row] as string
let padding: cgfloat = 20
let width = tableview.frame.size.width - padding * 2;
let size = cgsizemake(width, cgfloat.max)
let attributes = [nsfontattributename: uifont(name: "helvetica", size: 14)!]
let frame = content.boundingrectwithsize(size,
options: nsstringdrawingoptions.useslinefragmentorigin,
attributes: attributes,
context: nil)
return frame.size.height+1;
}
上面的代碼是一個(gè)最簡(jiǎn)單的例子,這個(gè)例子看起來(lái)好像沒(méi)有什么問(wèn)題。但是通過(guò)查看這個(gè) delegate 方法的文檔后,可以知道,在每次 reload tableview 的時(shí)候,程序會(huì)先計(jì)算出每一個(gè) cell 的高度,等所有高度計(jì)算完畢,確定了 tableview 的總的高度后,才開始渲染視圖并顯示在屏幕上。這意味著在顯示 table view 之前需要執(zhí)行一堆的計(jì)算,并且這是在主線程中進(jìn)行的,如果計(jì)算量太大程序就很有可能出現(xiàn)卡頓感。比如: table view 的數(shù)據(jù)有上千條,或者計(jì)算高度的代碼中還要先獲取圖片再根據(jù)圖片計(jì)算高度,這些操作都是非常慢的。
如果在 cell 中使用了 autolayout,在計(jì)算 cell 高度時(shí)會(huì)更麻煩。有興趣的可以看這里有篇關(guān)于如何在 autolayout 下動(dòng)態(tài)計(jì)算高度 的文章。
為什么不能等滾動(dòng)到某個(gè) cell 的時(shí)候,再調(diào)用計(jì)算這個(gè) cell 高度的 delegate 呢?原因是 tableview 需要獲得它的內(nèi)容的總高度,用這個(gè)高度去確定滾動(dòng)條的大小等。直到 ios 7 uitableviewdelegate中添加了新的 api
tableview(tableview: uitableview, estimatedheightforrowatindexpath indexpath: nsindexpath) -> cgfloat
這個(gè)方法用于返回一個(gè) cell 的預(yù)估高度,如果在程序中實(shí)現(xiàn)了這個(gè)方法,tableview 首次加載的時(shí)候就不會(huì)調(diào)用heightforrowatindexpath 方法,而是用 estimatedheightforrowatindexpath 返回的預(yù)估高度計(jì)算 tableview 的總高度,然后 tableview 就可以顯示出來(lái)了,等到 cell 可見的時(shí)候,再去調(diào)用heightforrowatindexpath 獲取 cell 的正確高度。
通過(guò)使用estimatedheightforrowatindexpath 這個(gè) delegate 方法,解決了首次加載 table view 出現(xiàn)的性能問(wèn)題。但還有一個(gè)麻煩的問(wèn)題,就是在 cell 沒(méi)有被加載的時(shí)候計(jì)算 cell 的高度,上面給出的代碼中,僅僅是計(jì)算一個(gè) nsstring 的高度,就需要不少代碼了。這種計(jì)算實(shí)際上是必須的,然而在 ios 8 開始,你可能可以不用再寫這些煩人的計(jì)算代碼了!
ios 8 的魔法
在 ios 8 中,self size cell 提供了這樣一種機(jī)制:cell 如果有一個(gè)確定的寬度/高度,autolayout 會(huì)自動(dòng)根據(jù) cell 中的內(nèi)容計(jì)算出對(duì)應(yīng)的高度/寬度。
tableview 中的 cell 自適應(yīng)
要讓 table view 的 cell 自適應(yīng)內(nèi)容,有幾個(gè)要點(diǎn):
設(shè)置的 autolayout 約束必須讓 cell 的 contentview 知道如何自動(dòng)延展。關(guān)鍵點(diǎn)是 contentview 的 4 個(gè)邊都要設(shè)置連接到內(nèi)容的約束,并且內(nèi)容是會(huì)動(dòng)態(tài)改變尺寸的。
uitableview 的 rowheight 的值要設(shè)置為 uitableviewautomaticdimension
和 ios 7 一樣,可以實(shí)現(xiàn) estimatedheightforrowatindexpath 方法提升 table view 的第一次加載速度。
任何時(shí)候 cell 的 intrinsiccontentsize 改變了(比如 table view 的寬度變了),都必須重新加載 table view 以更新 cell。
例子
在 xcode 中新建一個(gè)項(xiàng)目,在 storyboard 中創(chuàng)建一個(gè) uitableviewcontroller 的 ib,創(chuàng)建一個(gè)如下樣子的 cell:
這個(gè) cell 中有 3 個(gè)元素,其中 imageview 的 autolayout 約束為:
-
imageview 左邊離 contentview 左邊 0
-
imageview 上邊離 contentview 上邊 0
-
imageview 的 width 和 height 為 80
-
imageview 下邊離 contentview 下邊大于等于 0(為了防止內(nèi)容太少,導(dǎo)致 cell 高度小于圖片高度)
titlelabel 的 autolayout 約束為:
-
titlelabel 左邊離 imageview 右邊 8
-
titlelabel 上邊和 imageview 上邊在同一只線上
-
titlelabel 右邊離 contentview 右邊 0
-
titlelabel 下邊離 description 上邊 8
-
titlelabel 的高度小于等于 22,優(yōu)先級(jí)為 250
descriptionlabel 的約束為:
-
descriptionlabel 左邊和 titlelabel 左邊在同一直線上
-
descriptionlabel 上邊里 titlelabel 8
-
descriptionlabel 下邊里 contentview 下邊 0
-
descriptionlabel 右邊離 contentview 右邊 0
然后在這個(gè) ib 對(duì)應(yīng)的 uitableviewcontroller 中加載一些數(shù)據(jù)進(jìn)去,顯示效果如圖:
實(shí)現(xiàn)這個(gè)效果,我除了設(shè)置了 autolayout,還設(shè)置了 tableview 的 rowheight = uitableviewautomaticdimension,然后就是這樣了。一點(diǎn)計(jì)算 cell 高度的代碼都沒(méi)有!!我連 heightforrowatindexpath都不用實(shí)現(xiàn),真的是….爽出味啊!所以如果已經(jīng)在開發(fā) ios 8 only 的應(yīng)用了一定要用autolayout,把煩人的計(jì)算交給 autolayout 去吧。
collectionview 中的 cell 自適應(yīng)
在 collection view 中也能讓 cell 自適應(yīng)內(nèi)容大小,如果 uicollectionview 的 layout 是一個(gè) uicollectionviewflowlayout,只需要將 layout.itemsize = ... 改成 layout.estimateditemsize = ...。 只要設(shè)置了 layout 的 estimateditemsize,collection view 就會(huì)根據(jù) cell 里面的 autolayout 約束去確定cell 的大小。
原理:
-
collection view 根據(jù) layout 的 estimateditemsize 算出估計(jì)的 contentsize,有了 contentsize collection view 就開始顯示
-
collection view 在顯示的過(guò)程中,即將被顯示的 cell 根據(jù) autolayout 的約束算出自適應(yīng)內(nèi)容的 size
-
layout 從 collection view 里獲取更新過(guò)的 size attribute
-
layout 返回最終的 size attribute 給 collection view
-
collection 使用這個(gè)最終的 size attribute 展示 cell
uitextview 輸入內(nèi)容實(shí)時(shí)更新 cell 的高度
在一個(gè)動(dòng)態(tài)數(shù)據(jù)的 table view 中,cell 根據(jù) text view 內(nèi)容的輸入實(shí)時(shí)改變 cell 和 table view 的高度。自動(dòng)計(jì)算 cell 高度的功能使用 ios 8 才支持的自適應(yīng) cell,先上圖,我們最終要實(shí)現(xiàn)的效果是這樣的:
實(shí)現(xiàn)上面效果的基本原理是:
-
在 cell 中設(shè)置好 text view 的 autolayout,讓 cell 可以根據(jù)內(nèi)容自適應(yīng)大小
-
text view 中輸入內(nèi)容,根據(jù)內(nèi)容更新 textview 的高度
-
調(diào)用 tableview 的 beginupdates 和 endupdates,重新計(jì)算 cell 的高度
將 text view 更新后的數(shù)據(jù)保存,以免 table view 滾動(dòng)超過(guò)一屏再滾回來(lái) text view 中的數(shù)據(jù)又不刷新成原來(lái)的數(shù)據(jù)了。
功能具體實(shí)現(xiàn)方法:
新建一個(gè)項(xiàng)目,拉出 tableviewcontroller,在 cell 上添加一個(gè) uitextview。
首先設(shè)置 text view 的 autolayout,比較關(guān)鍵的 constraint 是要設(shè)置 textview 的高度大于等于一個(gè)值。如圖:
然后,設(shè)置 uitextview 的 scrollenable 為 no。這一點(diǎn)很關(guān)鍵,如果不設(shè)置為 no,uitextview 在內(nèi)容超出 frame 后,重新設(shè)置 text view 的高度會(huì)失效,并出現(xiàn)滾動(dòng)條。
根據(jù)剛才在 storyboard 中創(chuàng)建的 cell,新建一個(gè) uitableviewcell 類。
#import <uikit/uikit.h>
@interface textviewcell : uitableviewcell
@property (weak, nonatomic) iboutlet uitextview *textview;
@end
創(chuàng)建 tableviewcontroller 并初始化一些數(shù)據(jù)
#import "tableviewcontroller.h"
#import "textviewcell.h"
@interface tableviewcontroller ()
@property (nonatomic, strong) nsarray *data;
@end
@implementation tableviewcontroller
- (void)viewdidload {
[super viewdidload];
// 支持自適應(yīng) cell
self.tableview.estimatedrowheight = 100;
self.tableview.rowheight = uitableviewautomaticdimension;
self.data = @[@"cell 1 ", @"cell 2", @"cell 3", @"cell 4", @"cell 5", @"cell 6", @"cell 7", @"cell 8"];
}
#pragma mark - table view data source
- (nsinteger)tableview:(uitableview *)tableview numberofrowsinsection:(nsinteger)section {
return [self.data count];
}
- (uitableviewcell *)tableview:(uitableview *)tableview cellforrowatindexpath:(nsindexpath *)indexpath {
textviewcell *cell = [tableview dequeuereusablecellwithidentifier:@"textviewcell" forindexpath:indexpath];
cell.textview.text = self.data[indexpath.row];
return cell;
}
使用上面的代碼項(xiàng)目已經(jīng)可以運(yùn)行了,但是 text view 還不能自動(dòng)更新大小,下面來(lái)實(shí)現(xiàn) text view 根據(jù)內(nèi)容計(jì)算高度
先在 storyboard 中,將 uitextview 的 delegate 設(shè)置為 cell
在 textviewcell.m 中實(shí)現(xiàn) - (void)textviewdidchange:(uitextview *)textview,每次 text view 內(nèi)容改變的時(shí)候,就重新計(jì)算一次 text view 的大小,并讓 table view 更新高度。
#import "textviewcell.h"
@implementation textviewcell
- (void)textviewdidchange:(uitextview *)textview
{
cgrect bounds = textview.bounds;
// 計(jì)算 text view 的高度
cgsize maxsize = cgsizemake(bounds.size.width, cgfloat_max);
cgsize newsize = [textview sizethatfits:maxsize];
bounds.size = newsize;
textview.bounds = bounds;
// 讓 table view 重新計(jì)算高度
uitableview *tableview = [self tableview];
[tableview beginupdates];
[tableview endupdates];
}
- (uitableview *)tableview
{
uiview *tableview = self.superview;
while (![tableview iskindofclass:[uitableview class]] && tableview) {
tableview = tableview.superview;
}
return (uitableview *)tableview;
}
@end
這樣就已經(jīng)實(shí)現(xiàn)了 text view 改變內(nèi)容自動(dòng)更新 cell 高度的功能,這篇文章沒(méi)有涉及到計(jì)算 cell 高度的代碼,因?yàn)橛?jì)算 cell 高度的工作全部交給 ios 8 的 autolayout 自動(dòng)計(jì)算了,這讓我們少寫了許多令人痛苦的代碼。
最后:為了防止 table view 過(guò)長(zhǎng),導(dǎo)致滾動(dòng)后重新加載 cell,會(huì)讓 text view 中的內(nèi)容還原的問(wèn)題,我們應(yīng)該在更新了 text view 的內(nèi)容之后保存數(shù)據(jù)。(如果是在編輯狀態(tài)下,還需要考慮取消編輯后的回滾功能。 普通數(shù)組數(shù)據(jù),可以保存一個(gè)原始數(shù)據(jù)的副本,如果用戶取消編輯,就設(shè)置 data 為原始數(shù)據(jù)的副本。如果是 nsmanagedobject 對(duì)象可以使用 nsundomanage,不過(guò)這些已經(jīng)超出本篇文章的內(nèi)容范圍了。)
為了在 text view 更新后能讓 tableviewcontroller 中的 data 更新,需要為 cell 添加一個(gè) delegate,在 text view 更新后調(diào)用 delegate,tableviewcontroller 中收到 delegate 信息后更新 data。
修改后的 textviewcell.h
#import <uikit/uikit.h>
@protocol textviewcelldelegate;
@interface textviewcell : uitableviewcell
@property (weak, nonatomic) iboutlet uitextview *textview;
@property (weak, nonatomic) id<textviewcelldelegate> delegate;
@end
@protocol textviewcelldelegate <nsobject>
- (void)textviewcell:(textviewcell *)cell didchangetext:(nsstring *)text;
@end
在 textview.m的 - (void)textviewdidchange:(uitextview *)textview 中添加 delegate 的調(diào)用
- (void)textviewdidchange:(uitextview *)textview
{
if ([self.delegate respondstoselector:@selector(textviewcell:didchangetext:)]) {
[self.delegate textviewcell:self didchangetext:textview.text];
}
// 計(jì)算 text view 的高度
...
// 讓 table view 重新計(jì)算高度
...
}
最后在 tableviewcontroller.m 的最后實(shí)現(xiàn) textviewcelldelegate 的方法,更新 data
#pragma mark - textviewcelldelegate
- (void)textviewcell:(textviewcell *)cell didchangetext:(nsstring *)text
{
nsindexpath *indexpath = [self.tableview indexpathforcell:cell];
nsmutablearray *data = [self.data mutablecopy];
data[indexpath.row] = text;
self.data = [data copy];
}