阅读视图

发现新文章,点击刷新页面。

UITableView 预估行高 跳转不准问题修复

最近遇到一个问题, 需要定位到 TableView 的某一行, 历史代码是使用

1
- (CGRect)rectForRowAtIndexPath:(NSIndexPath *)indexPath;

获取到对应的cell 在 TableView 上的位置, 然后再根据业务做了一些简单的计算, 最后使用

1
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;

跳转到最终的位置.

但是由于这个业务比较复杂, 为了性能, 开启了预估行高(estimatedHeightForRowAtIndexPath), 实际上, TableViewCell 的高度也根据业务各不一样, 预估值也与实际情况出入较大, 导致上面的 setContentOffset 跳到了不正确的行高.

预估行高是什么, 为什么会导致定位不准

UITableView 在行数非常多的时候, 比如 1万行, 一次reloadData会计算数万次行高(heightForRowAtIndexPath), 往往heightForRowAtIndexPath 中还会有一些业务逻辑, 哪怕单次执行耗时 0.1ms, 一次刷新也会卡主主线程数秒钟, 所以引入了预估行高(estimatedHeightForRowAtIndexPath)来优化性能.

  1. 问题: 为什么要大量执行 heightForRowAtIndexPath, 不是有复用机制吗?
    • TableView 需要计算自己的contentSize, 所以需要知道每一个数据对应的 cell 有多高.
  2. 预估行高(estimatedHeightForRowAtIndexPath) 是什么?
    • 有预估行高的时候, TableView 遍历每一个数据时, 不会调用 heightForRowAtIndexPath, 而是调用 estimatedHeightForRowAtIndexPath,
    • 我们的estimatedHeightForRowAtIndexPath 只是简单返回一个预测值, 比如是各种高度的平均数(直接写死数值即可), 这样避免了数万次行高的计算, 减少了主线程阻塞, 达到流畅度优化的作用
  3. 预估行高, 那什么时候计算真实高度?
    • cell 上屏之前(cellFor)会计算真实高度(heightForRowAtIndexPath)
  4. 使用预估行高, 为什么跳转会定位不准?
    • 使用预估行高意味着, 某一个 cell 如果没上屏, 高度就是一个预估值, 所有的位移(offset)都是建立在当前数据 index 前面的数据对应行高的累加结果之上的
    • 比如我要定位到第 1000 行, 快速滑动到 1000 行时候, 可能有 100 个真实高度 + 900 个预估高度; 缓慢滑动到 1000 行时候, 则有 1000 个真实高度; 上面两个 case 我同样滑动到了第 1000 行, 但是此时 TableView 的 offsetY 值是不一样的, 这取决于前面有多少个cell 的高度是预估出来的;
    • 基于上面的结论, 定位终点之前有预估行高, 定位就会有误差

怎么解决预估行高定位不准的问题

上面说了 TableView 的 offsetY 在有预估 cell 高度时候不准, 那我们怎么定位到某一行呢?

1
- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated;

scrollToRowAtIndexPath 可以帮助我们定位到正确的位置, scrollPosition 最常用的是 UITableViewScrollPositionTop, 指的是把我们要定位的那一行尽量放在 TableView 的最上面(如果是最后一个 cell, 下面又没有 footer, 就只能留在TableView 最底部, 所以说是尽量).

所以问题就这么简单解决了? - 导航栏问题

但是实际情况比较复杂, 我们的 app 页面顶部一般有导航栏, 上面说的”放在 TableView 的最上面” 这个位置, 一般都正好跟导航栏覆盖了, 导致虽然定位准了, 但是视觉上还是歪了一点, 需要向下挪一点(offsetY -= naviHeader.height);

PS. 简单 case 可以直接设置 contenInsets.top = naviHeader.height 解决, 但是这里我们的业务比较复杂, 这么做会产生别的交互问题

那挪一下, 就会产生一次位移, 会闪一下, 当然我们也可以用动画(setContentOffset:(offsetY - naviHeader.height)point animated:YES).
这个动画的视觉效果是固定的, 但是我们做定位的方向是可能向上, 也可能向下, 这取决于我们初始位置与定位终点的相对位置, 所以一些 case 下视觉上会很奇怪, scrollToRowAtIndexPath 产生一次动画, setContentOffset 又产生一次动画, 两次动画方向可能还是反的; 这时候比较尴尬

  • 定位是准的, 没问题
  • 会产生两次位移动画, 动画方向还可能是反的
  • 不做动画, 页面会闪动, 只做一次动画, 视觉效果仍然会有点奇怪(系统有隐性动画)

最终解决方案 - UIView animateWithDuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 最终打包动画
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^(void) {
// 1. 先定位准确, 不做动画
[tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO];
point = tableView.contentOffset;
// 2. 根据 各种 case 计算下需要便宜的高低, 再定位一次, 不做动画
[tableView setContentOffset:point animated:NO];
}
completion:^(BOOL finished) {

}];
❌