Flutter 列表Item可见区域监听

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

前言

类似extended_list插件,viewportBuilder可以拿到当前视口的索引,collectGarbage可以拿到不可见(可回收的)children的索引。

但他需要使用他的组件替代常用的ListViewGridView组件,有侵入性,有没有不需要替换组件,直接套住就能用的方法呢?

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也就是ElementElement可以遍历他的子Element

element.visitChildElements((child) {
});
复制代码

但需要知道,我们的目标Element是什么。

扫描二维码关注公众号,回复: 14291942 查看本文章

观察一下ListViewGridView,会发现它们最终的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,来找到对应的SliverMultiBoxAdaptorWidgetElement

/// 递归找到对应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决定。SliverMultiBoxAdaptorElementparentData则为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即回收的监听要怎么监听到?

猜你喜欢

转载自juejin.im/post/7109782363629944845