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.