前言
这篇文章是最近对UITableViewCell利用AutoLayout自动高度计算和UITableView滑动优化的一个总结。从这篇文章里,你可以读到:
UITableView高度计算和估算的机制
不同iOS系统在高度计算上的差异
iOS8 self-sizing cell
UITableView+FDTemplateLayoutCell如何用一句话解决高度问题
UITableView+FDTemplateLayoutCell中对RunLoop的使用技巧
UITableViewCell高度计算
* rowHeight*
UITableView是我们再熟悉不过的视图了,它的delegate和data source回调不知写了多少次,也不免遇到UITableViewCell高度计算的事。UITableView询问cell高度有两种方式。
- 一种是针对所有Cell具有固定高度的情况,通过:
self.tableView.rowHeight=88;
上面的代码指定了一个所有cell都是88高度的UITableView,对于定高需求的表格,强烈建议使用这种(而非下面的)方式保证不必要的高度计算和调用。rowHeight属性的默认值是44,所以一个空的UITableView显示成那个样子。
- 另一种方式就是实现UITableViewDelegate中的:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// return xxx
}
- 需要注意的是,实现了这个方法后,rowHeight的设置将无效。所以,这个方法适用于具有多种cell高度的
UITableView.estimaTEDRowHeight
这个属性iOS7就出现了,文档是这么描述它的作用的:
If The table contains variable height rows, it might be expensive to calculate all Their heights when The table loads. Using estimation allows you to defer some of The cost of geometry calculation from load time to scrolling time.
- 恩,听上去蛮靠谱的。我们知道,UITableView是个UIScrollView,就像平时使用UIScrollView一样,加载时指定contentSize后它才能根据自己的bounds、contentInset、contentOffset等属性共同决定是否可以滑动以及滚动条的长度。而UITableView在一开始并不知道自己会被填充多少内容,于是询问data source个数和创建cell,同时询问delegate这些cell应该显示的高度,这就造成它在加载的时候浪费了多余的计算在屏幕外边的cell上。和上面的rowHeight很类似,设置这个
估算高度有两种方法:
self.tableView.estimaTEDRowHeight=88;
// or
- (CGFloat)tableView:(UITableView *)tableView estimaTEDHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
// return xxx
}
有所不同的是,即使面对种类不同的cell,我们依然可以使用简单的estimaTEDRowHeight属性赋值,只要整体估算值接近就可以,比如大概有一半cell高度是44, 一半cell高度是88, 那就可以估算一个66,基本符合预期。
说完了估算高度的基本使用,可以开始吐槽了:
- 设置估算高度后,contentSize.height根据“cell估算值 x cell个数”计算,这就导致滚动条的大小处于不稳定的状态,contentSize会随着滚动从估算高度慢慢替换成真实高度,肉眼可见滚动条突然变化甚至“跳跃”。
若是有设计不好的下拉刷新或上拉加载控件,或是KVO了contentSize或contentOffset属性,有可能使表格滑动时跳动。
估算高度设计初衷是好的,让加载速度更快,那凭啥要去侵害滑动的流畅性呢,用户可能对进入页面时多零点几秒加载时间感觉不大,但是滑动时实时计算高度带来的卡顿是明显能体验到的,个人觉得还不如一开始都算好了呢(iOS 8更过分,即使都算好了也会边划边计算)。
iOS8 self-sizing cell
具有动态高度内容的cell一直是个头疼的问题,比如聊天气泡的cell,frame布局时代通常是用数据内容反算高度:
CGFloat height=textHeightWithFont() + imageHeight + topMargin + bottomMargin + ...;
供UITableViewDelegate调用时很可能是个cell的类方法:
@interface BubbleCell : UITableViewCell
+ (CGFloat)heightWithEntity:(id)entity;
@end
各种魔法margin加上耦合了屏幕宽度。
AutoLayout时代好了不少,提供了-systemLayoutSizeFittingSize:的API,在contentView中设置约束后,就能计算出准确的值;缺点是计算速度肯定没有手算快,而且这是个实例方法,需要维护专门为计算高度而生的template layout cell,它还要求使用者对约束设置的比较熟练,要保证contentView内部上下左右所有方向都有约束支撑,设置不合理的话计算的高度就成了0。
这里还不得不提到一个UILabel的蛋疼问题,当UILabel行数大于0时,需要指定preferredMaxLayoutWidth后它才知道自己什么时候该折行。这是个“鸡生蛋蛋生鸡”的问题,因为UILabel需要知道superView的宽度才能折行,而superView的宽度还依仗着子View宽度的累加才能确定。这个问题好像到iOS 8才能够自动解决(不过我们找到了解决方案)。