Flutter 列表Item可见区域监听
前言
类似extended_list插件,viewportBuilder
可以拿到当前视口的索引,collectGarbage
可以拿到不可见(可回收的)children的索引。
但他需要使用他的组件替代常用的ListView
、GridView
组件,有侵入性,有没有不需要替换组件,直接套住就能用的方法呢?
NotificationListener
对于滚动列表的监听,立马就能想到的就是NotificationListener
。那我们能不能够通过它来监听得到我们想要的东西呢。
NotificationListener(
onNotification: onNotification,
child: child,
)
滚动列表我们取ScrollNotification
。
bool onNotification(ScrollNotification notice) {
...
}
从ScrollNotification
中我们可以得到什么?
/// A description of a [Scrollable]'s contents, useful for modeling the state
/// of its viewport.
final ScrollMetrics metrics;
/// The build context of the widget that fired this notification.
///
/// This can be used to find the scrollable's render objects to determine the
/// size of the viewport, for instance.
final BuildContext? context;
一个是context
,一个是ScrollMetrics
ScrollMetrics
是视口状态的信息,主要可以得到minScrollExtent
(最小滚动距离)、maxScrollExtent
(最大滚动距离)、pixels
(当前滚动位置)、viewportDimension
(视口范围)、axisDirection
(滚动方向)。
那也就是说,假如我们可以获取到Item的位置,就可以根据大于pixels
,小于pixels
+ viewportDimension
来判断Item是否在可视范围中。
Item的element
既然可以得到滚动列表的context
,而context
也就是Element,Element可以遍历他的子Element
element.visitChildElements((child) {
});
但需要知道,我们的目标Element是什么。
观察一下ListView
、GridView
,会发现它们最终的Layout构建,都是Sliver,且都继承自SliverMultiBoxAdaptorWidget
。
@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent!,
);
} else if (prototypeItem != null) {
return SliverPrototypeExtentList(
delegate: childrenDelegate,
prototypeItem: prototypeItem!,
);
}
return SliverList(delegate: childrenDelegate);
}
class SliverFixedExtentList extends SliverMultiBoxAdaptorWidget
class SliverList extends SliverMultiBoxAdaptorWidget
class SliverGrid extends SliverMultiBoxAdaptorWidget
那可以尝试通过遍历child,来找到对应的SliverMultiBoxAdaptorWidget
的Element。
/// 递归找到对应SliverMultiBoxAdaptorElement返回
SliverMultiBoxAdaptorElement? findSliverMultiBoxAdaptorElement(
Element element) {
if (element is SliverMultiBoxAdaptorElement) {
return element;
}
SliverMultiBoxAdaptorElement? target;
element.visitChildElements((child) {
target ??= findSliverMultiBoxAdaptorElement(child);
});
return target;
}
用的递归的方法,不断遍历直到遇到SliverMultiBoxAdaptorElement
。
然后访问他的children,也就是Item列表
sliverMultiBoxAdaptorElement.visitChildren((Element element) {});
是一个回调的形式,不断返回访问的child(element)。
element可以访问RenderObject,而RenderObject布局属性则有parentData决定。SliverMultiBoxAdaptorElement
的parentData则为SliverMultiBoxAdaptorParentData
,所以也可以拿到了Item的布局数据。
final SliverMultiBoxAdaptorParentData oldParentData =
element.renderObject?.parentData as SliverMultiBoxAdaptorParentData;
滚动列表的Item的parentData,可以取到layoutOffset,基于滚动列表的Item的位置。
位置信息得到了,然后就可以判断他是否在可视范围内
/// 取得Item的element数据
final SliverMultiBoxAdaptorElement? sliverMultiBoxAdaptorElement =
findSliverMultiBoxAdaptorElement(notice.context! as Element);
if (sliverMultiBoxAdaptorElement == null) return false;
/// 可见区域的距离
final viewportDimension = notice.metrics.viewportDimension;
/// 第一个可见Item 坐标
int firstIndex = 0;
/// 最后一个可见Item 坐标
int endIndex = 0;
/// 访问Children时判断Child是否可见,并计算相应的first和end
void onVisitChildren(Element element) {
final SliverMultiBoxAdaptorParentData oldParentData =
element.renderObject?.parentData as SliverMultiBoxAdaptorParentData;
double layoutOffset = oldParentData.layoutOffset!;
double pixels = notice.metrics.pixels;
double all = pixels + viewportDimension;
/// 判断是否在可见区域内
if (layoutOffset >= pixels) {
firstIndex = min(firstIndex, oldParentData.index!);
if (layoutOffset <= all) {
endIndex = max(endIndex, oldParentData.index!);
}
firstIndex = max(firstIndex, 0);
} else {
endIndex = firstIndex = oldParentData.index!;
}
}
sliverMultiBoxAdaptorElement.visitChildren(onVisitChildren);
封装使用
然后再简单的封装一下
class ItemCollectListener extends StatefulWidget {
const ItemCollectListener({
Key? key,
required this.child,
this.indexCallBack,
this.scrollDirection = Axis.vertical,
}) : super(key: key);
scrollDirection
可以限制只处理对应方向上的Notification。
ItemCollectListener(
indexCallBack: (firstIndex, endIndex, notification) {
print("firstIndex: $firstIndex, endIndex: $endIndex");
},
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Container(
width: double.maxFinite,
height: 50,
color: Colors.blue.withOpacity(index / 100),
);
},
),
)
这样就可以监听到哪些Item进入到了可视窗口中了,这里是将首个和末尾的下标返回。
不需要修改ListView,只需要套在需要监听的滚动列表上,就可以监听到他的可视状态,测试大部分都有效。
可以思考一下,移出的Item即回收的监听要怎么监听到?
转载自:https://juejin.cn/post/7109782363629944845