Flutter 获取ListView的第一个可见的Item
Flutter的ListView并没有直接提供方法和属性来告知我们当前列表的第一个Item的index,因此查看ListView的源码,可以发现其Item的创建与SliverChildDelegate相关。这里拿ListView.custom举例。
const ListView.custom({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
@required this.childrenDelegate,
double cacheExtent,
int semanticChildCount,
}) : assert(childrenDelegate != null),
super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
);
其中有个参数是childrenDelegate,该参数的定义如下。
/// A delegate that provides the children for the [ListView].
///
/// The [ListView.custom] constructor lets you specify this delegate
/// explicitly. The [ListView] and [ListView.builder] constructors create a
/// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
/// respectively.
final SliverChildDelegate childrenDelegate;
可知,列表的Item的创建工作是由该类完成的。继续深入,发现SliverChildDelegate中存在一个方法,
/// Called at the end of layout to indicate that layout is now complete.
///
/// The `firstIndex` argument is the index of the first child that was
/// included in the current layout. The `lastIndex` argument is the index of
/// the last child that was included in the current layout.
///
/// Useful for subclasses that which to track which children are included in
/// the underlying render tree.
void didFinishLayout(int firstIndex, int lastIndex) { }
震惊!!!这不就是我们所需要的第一个可见的Item吗!!!于是自定义MyDelegate,继承SliverChildDelegate并重写didFinishLayout方法。
class MyDelegate extends SliverChildDelegate {
final int childCount;
final IndexedWidgetBuilder builder;
MyDelegate({this.childCount, this.builder});
@override
Widget build(BuildContext context, int index) {
return builder(context, index);
}
@override
bool shouldRebuild(SliverChildDelegate oldDelegate) => true;
@override
int get estimatedChildCount => childCount;
@override
void didFinishLayout(int firstIndex, int lastIndex) {
print('第一个Item是$firstIndex,最后一个Item是$lastIndex');
}
}
但是程序运行起来,发现Item不能保存状态了,所以不能在build方法中直接返回builder(context, index),必须得重写该方法。这时候发现,flutter已经帮我们做好了这个工作,就是SliverChildBuilderDelegate,直接把里面的build方法以及相关的变量复制过来就好了。
@override
Widget build(BuildContext context, int index) {
assert(builder != null);
if (index < 0 || (childCount != null && index >= childCount))
return null;
Widget child;
try {
child = builder(context, index);
} catch (exception, stackTrace) {
child = _createErrorWidget(exception, stackTrace);
}
if (child == null)
return null;
final Key key = child.key != null ? _SaltedValueKey(child.key) : null;
if (addRepaintBoundaries)
child = RepaintBoundary(child: child);
if (addSemanticIndexes) {
final int semanticIndex = semanticIndexCallback(child, index);
if (semanticIndex != null)
child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
}
if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child);
return KeyedSubtree(child: child, key: key);
}
搞定!!!
等等。。。当我某一个Item进行状态保存以后,firstIndex的值就一直是该Item的index,并且lastIndex也指向列表中最后一个保存状态的Item。所以这个firstIndex应该是指向列表维护的Item中的第一个,跟我们想要的第一个可见的Item完全不是一回事嘛。这个时候,我在SliverChildDelegate中发现了另外一个方法。
/// Returns an estimate of the max scroll extent for all the children.
///
/// Subclasses should override this function if they have additional
/// information about their max scroll extent.
///
/// The default implementation returns null, which causes the caller to
/// extrapolate the max scroll offset from the given parameters.
double estimateMaxScrollOffset(
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
) => null;
嗯。。。。看起来也没啥特别的啊,注释里都没解释参数的意义,而且从名字来看,这个firstIndex跟didFinishLayout里的firstIndex应该是同一个东西吧。不死心的我在MyDelegate中又重写了这个方法并输出了firstIndex的值,结果证明,这就是我们想要的东西!!!同时lastIndex指向当前列表可见的最后一个Item。终于找到你,还好我没放弃~~(不过flutter也太坑了吧,啥也不说明,鬼知道是干嘛用的),完结,撒花。