TableView 动态 cell 高度自适应方案
相比安卓 ListView/RecylerView,iOS 中 TableViewCell 高度自适应是需要开发者自己想办法解决的,原因在于 tableview 的渲染机制默认是先获取 cell 高度,然后再去绘制 cell 体。由于 label/textview 等通常是高度不定的,cell 高度动态化是个很常见的需求。
方案一:高度固定
针对所有 Cell 具有固定高度的情况:
self.tableView.rowHeight = 88;
- 实现协议方法
heightForRowAtIndexPath:
。
缺点:不支持动态高度。
方案二:cell 预估高度(iOS 7+)
设计思路:
加载 tableview 时一次性计算所有 cell 高度太耗性能。所以把计算 height 的任务从 load time 转移到 scrolling time。只有滑动到的 cell 会计算,屏幕外边的不会计算。
方法:tableView: estimatedHeightForRowAtIndexPath:
这是 iOS 7.0 出现的 UITableViewDelegate 中的方法,表示返回某行 cell 的预估高度。这个方法改变了 TableView 代理方法的调用顺序。
未调用estimatedHeightForRow...
方法时:
调用estimatedHeightForRow...
后:
此时 tableView 工作原理:
- tableView 先向代理拿得到每个 cell 的预估高度(
estimatedHeightForRow...
方法),并且拿这个高度去计算整个 tableView 应该显示的范围。 - 根据每行预估的高度,算出一屏显示的 cell 的个数,并先对这些 cell(调用
cellForRow...
方法)进行绘制。 - 绘制时拿到 cell 的真实高度,然后放在
heightForRow...
方法里面拿给 tabelView 去用。 - 屏幕滚动(有 cell 进入屏幕)的时候,仍然会调用绘制以及获取真实高度的方法。
简单点说,预估高度用来让 tableView 心里有个底,把 cell 先绘制出来,但最后实际的 cell 高度还是会从heightForRow...
方法中获取。
缺点:
- 设置估算高度后,
tableview.contentSize.height
根据“cell估算值 x cell个数”计算,这就导致滚动条的大小处于不稳定的状态,contentSize 会随着滚动从估算高度慢慢替换成真实高度,肉眼可见滚动条突然变化甚至“跳跃”。 - 估算高度使加载速度更快,但侵害滑动流畅性,cell较多情况下滑动时实时计算高度带来的卡顿是明显能体验到的。
方案三:self-sizing cell (iOS 8+)
设计思路:
如图,row123 是已经在屏幕上被展示的cell, 而 row4 是下一个会被展示的cell, 这时 row4 这个 cell 的 rowHeight 是预先为其设置的 estimated height, 又或者是 UITableViewDelegate 中返回的 height。
当用户滚动的时候,首先 cell 会被创建或重用,然后 cell 会被调用调整 size 的方法, 接着 cell 会根据 tableView 的size去调整自身的 contentSize,最后cell被展示出来。
如何让一个cell去调整自己的高度?
- cell 要使用 Autolayout 布局;
- 在 tableView 中启动动态布局, 告诉 tableView 用新的方法来布局行高.而不是 rowHeight 或者 delegate 方法。
tableView.rowHeight = UITableViewAutomaticDimension;
缺点:
- 仅支持iOS 8+;
- 高度没有缓存。iOS 7 计算高度后有”缓存“机制,不会重复计算;iOS 8 不论何时都会重新计算 cell 高度。
方案四:FDTemplateLayoutCell(iOS 6+)
好处:既有 iOS8 self-sizing 功能简单的 API,又可以达到 iOS7 流畅的滑动效果,还保持了最低支持 iOS6。
- 根据 autolayout 约束自动计算高度。使用了系统在 iOS6 提供的 API:
-systemLayoutSizeFittingSize:
- 根据 indexPath 的一套高度缓存机制。
计算出的高度会自动进行缓存,所以滑动时每个 cell 真正的高度计算只会发生一次,后面的高度询问都会命中缓存,减少了多余计算。 - 自动的缓存失效机制。
UITableView 刷新时,已有的高度缓存将以最小的代价失效。如删除一个 indexPath 为 [0:5] 的 cell,[0:0] ~ [0:4] 的高度缓存不受影响,而 [0:5] 后面所有的缓存值都向前移动一个位置。自动缓存失效机制对 UITableView 的 9 个公有 API 都进行了分别的处理,以保证没有一次多余的高度计算。 - 预缓存机制(通过 runloop 实现)。
UITableView 没有滑动的空闲时刻会计算和缓存那些还没有显示到屏幕中的 cell,整个缓存过程完全没有感知,这使得完整列表的高度计算既没有发生在加载时,又没有发生在滑动时,同时保证了加载速度和滑动流畅性。
缺点:
要求约束设置完整准确,保证 contentView 内部上下左右所有方向都有约束支撑,设置不合理的话计算的高度就成了0。(这个实际上是systemLayoutSizeFittingSize:
的要求)。
从易用性、性能、高度自适应效果、适配 iOS 最低版本综合来看,方案四是最佳的。但需要引入第三方库,同时要保证 autolayout 约束完整准确才行,关于这点《UITableViewCell高度自适应的关键点》总结得很好:
Cell 内部的 Constraints 一定要有一条从 Cell 顶部到底部的一条可联通线。