美文网首页
Flutter-ListView重用机制分析和实现jumpTo(

Flutter-ListView重用机制分析和实现jumpTo(

作者: _时光念你 | 来源:发表于2019-07-10 19:02 被阅读0次

    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.

    相关文章

      网友评论

          本文标题:Flutter-ListView重用机制分析和实现jumpTo(

          本文链接:https://www.haomeiwen.com/subject/rkhhkctx.html