Flutter-ListView重用机制分析和实现jumpTo(index)功能

Flutter官方SDK目前还没有支持ListView.jumpTo(index)的功能,但是这个功能是很多App都需要的.想要实现这个功能需要先要了解ListView的item"重用"机制.

重用机制

先介绍一下iOS的cell重用机制,然后对比ListView的item"重用"机制.

iOS的TableViewCell重用机制

  • 通过对每一个类型的cell绑定重用id标志
  • 根据重用id去取出重用池里面的cell对象,池子里没有或者数量不够,tableView会new一个新的出来.
  • 去更新该cell,调整frame并移动到可视区域.
/// 注册cell
- (void)registerClass:(nullable Class)cellClass forCellReuseIdentifier:(NSString *)identifier;

/// 取出cell
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;

/// 更新cell
cell.data = data;

ListView的item"重用"机制

ListView因为没有item的重用id,所以每次滑动ListView,它会重新创建、布局、绘制可见区域内的item,一般会多绘制可见区域以外2-4个item,即预加载机制,这点跟iOS有点类似.当item不在屏幕显示的时候,会执行dispose.
Flutter整个框架对UI进行了优化,所以不必担心重复创建item的内存消耗问题.ListView的重用机制就是Flutter对UI的重用机制,优化更加彻底,会重用item对应的element和renderObject对象,因为item对象每次都会重新创建.item对象是轻量级的,它关联的renderObject和element才是正在消耗内存的,只要这两个有缓存机制就没什么大问题.而且ListView必须滚动到指定位置之后才会触发相关区域item的创建、布局等操作.

实现jumpTo(index)功能

ScrollController提供jumpTo(double value)方法,所以我们只要知道index对应的offset即可,对于item高度一样的ListView,比较简单.

等高的item

var offset = itemIndex * itemHeight;
scrollController.jumpTo(offset);

非等高的item

难点在于item高度不一样的时候,有几种可用方案:

提供item的高度的回调方法
这样的方式其实跟iOS类似.iOS的方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
flutter需要实现double itemHeight(int index)方法,这样就可以计算offset.

double itemHeight(int index){
    return 不同高度;    
  }
  
  double offsetOnIndex(int index) {
    double offset = 0.0;
    for(int i = 0; i < index; i ++){
      offset += itemHeight(i);
    }
    return offset;
  }
    _scrollController.jumpTo(offsetOnIndex(index));

itemHeight方法实现起来比较麻烦,你需要给item增加height一个计算方法,尤其是碰到复杂的item.写过iOS的都知道,这玩意不好写,但iOS至少有一个自动计算cell高度的三方框架.而且flutter中的高度计算更加不好写,因为flutter的布局体系更复杂,实现难度更大.

创建一个SingleChildScrollView,并把ListView的所有item同样创建一份给SingleChildScrollView

利用SingleChildScrollView全部加载child的机制,可以很方便的计算出所有的item的height,然后就可以累加之前的所有item并计算出任意item的offset,但是缺点是如果item很多,会消耗大量的内存,而且SingleChildScrollView自身必须要显示出来才会layout它的child.

利用ListView的预加载机制,逐步加载未显示的item

需要自定义item,使用SizeChangedLayoutNotifier,它可以监听到item布局完成的通知,但是需要自定义修改一下它的实现,因为它的第一次布局完成不会发通知.每次布局完成把布局结果放入一个Map<int,double>缓存起来.当你需要滚动到某一个index的时候,取出<index的所有item缓存的高度累加即可.但是,并没有想象的那么简单.如果是你jumpTo到已经显示过的item,这样是可以,因为显示过的item已经有高度缓存了.没有显示过的是没有缓存过高度的.这里就出现了一个矛盾.

  • 想滚到到指定index,前提是index之前的item都必须已经布局完成
  • index之前的item都已经布局完成,才能缓存他们的高度,然后才能滚到指定的index.

所以存在滚动<=>布局相互等待问题了.那该如何解决?

可以利用ListView的预加载机制来做,每次我们可以使offset+=1,触发预加载,等待预加载出来的item布局完成之后直接滚动到最后的item的offset,一直循环这个逻辑就可以滚动到目标index,但要注意判断边界条件.
伪代码:

var tryOffset = 1;
var totalOffset = 0;
var startIndex = 0;
while(true) {
  scrollController.offset += tryOffset;
  // 边界条件判断
  // 1.超出所有item数量了
  // 2.滚动到底了
  // 3.到达目标index了
  // 等待scrollController.position.moveTo完成
  // 等待新的item布局完成
  totalOffset += 新item的高度(从缓存取);
  startIndex ++;
}

// 结束:
scrollController.position.moveTo(totalOffset);

最后

我写的flutter库list_view_item_builder的解决方案就是利用预加载来实现ListView.jumpTo(index)的.目前也没有发现什么问题,不管是还未布局过的,还是布局过的item都是可以正常滚动到指定index.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。