查找ListView中可见的项

28

如何查找当前在 ListView 中哪些项目是“可见的”或“不可见的”?
例如,我有100个项目在 ListView 中,当我滚动到屏幕或列表的顶部时,我想检测哪些项目出现或从视口中消失。

示意图:

enter image description here


你为什么想知道这个?如果你要创建一个有100个项目的列表,你应该使用ListView.builder,它可以传递一个itemBuilder,用于构建listView中一行的“widget”。这样Flutter就能够管理哪些行是可见的并进行渲染,并清除不可见的行以释放内存... - LgFranco
是的,我知道Flutter可以管理它。我想使用这个功能来使用其他监听器来使用它。 - DolDurma
4个回答

15

这并不是一件容易的事情。这里有同样的问题,但没有答案。

GitHub上有一个活跃的讨论。

对于该问题,有多种解决方案。其中一个方案需要使用rect_getter,可以在此处找到这个方案的示例代码。
另外,你也可以看看这个提案

简而言之

如果你正在寻找一种简单的方法来解决这个问题,那么目前还没有实现。然而,有一些解决方案,比如我上面提到的方案,还有其他一些包中的解决方案,比如VisibilityDetector这个类来自于flutter_widgets包。


8
现在有一种简单的方法来做到这一点,请查看此答案:https://dev59.com/w1UK5IYBdhLWcg3w9jnb#57252652 - Mamo1234
@LukasSchneider 谢谢你提到它! - creativecreatorormaybenot
谢谢你们两个,卢卡斯的答案也看起来是一个好的解决方案! - leylekseven
visibilityDetector 有一些限制吗?我无法从他们的官方文档中理解。 - Rishabh Agrawal

3

有一个可以实现此目的。

VisibilityDetector小部件包装现有的Flutter小部件,并在小部件的可见性更改时触发回调。

用法:

VisibilityDetector(
    key: Key('my-widget-key'),
    onVisibilityChanged: (visibilityInfo) {
      var visiblePercentage = visibilityInfo.visibleFraction * 100;
      debugPrint(
          'Widget ${visibilityInfo.key} is ${visiblePercentage}% visible');
    },
    child: someOtherWidget,
  )

2

我分享一下如何通常检测小部件位置的方法,以提高可见性。

我很好奇如何访问小部件的位置数据,并且还想能够控制ListView子元素的动画状态。

看起来访问小部件的大小和位置的主要方法是通过BuildContextcontext.findRenderObject()

然而,这只有在组件构建完成并挂载小部件后才能使用。

通过在一个名为WidgetsBinding.instance.addPostFrameCallback((_) => calculatePosition(context));的函数中使用context.findRenderObject()来解决这个问题。

这里有一个包装器组件,您可以在ListView.itemBuilder()代码中使用。

import 'package:flutter/cupertino.dart';
import 'dart:developer' as developer;

enum POCInViewDirection { up, down, static }

class POCInView extends StatefulWidget {
  final Widget child;
  final double scrollHeight;

  const POCInView({super.key, required this.child, required this.scrollHeight});

  @override
  POCInState createState() => POCInState();
}

class POCInState extends State<POCInView> {
  bool inView = false; // are you in view or not.
  double lastPositionY = 0; // used to determine which direction your widget is moving.
  POCInViewDirection direction = POCInViewDirection.static; // Set based on direction your moving.
  RenderBox? renderBoxRef;
  bool skip = true;

  @override
  void initState() {
    super.initState();
    developer.log('InitState', name: 'POCInView');
    lastPositionY = 0;
    renderBoxRef = null;
    direction = POCInViewDirection.static;
    skip = true;
  }
  /// Calculate if this widget is in view.
  /// uses BuildContext.findRenderObject() to get the RenderBox.
  /// RenderBox has localToGlobal which will give you the objects offset(position)
  /// Do some math to workout if you object is in view.
  /// i.e. take into account widget height and position.
  ///
  /// I only do Y coordinates.
  ///
  void calculatePosition(BuildContext context) {
    // findRenderObject() will fail if the widget has been unmounted. so leave if not mounted.
    if (!mounted) {
      renderBoxRef = null;
      return;
    }

    // It says this can be quite expensive as it will hunt through the view tree to find a RenderBox.
    // probably worth timing or seeing if its too much for you view.
    // I've put a rough cache in, deleting the ref when its unmounted. mmmmm.
    renderBoxRef ??= context.findRenderObject() as RenderBox;
    //
    inView = false;
    if (renderBoxRef is RenderBox) {
      Offset childOffset = renderBoxRef!.localToGlobal(Offset.zero);
      final double y = childOffset.dy;
      final double componentHeight = context.size!.height;
      final double screenHeight = widget.scrollHeight;
      if (y < screenHeight) {
        if (y + componentHeight < -20) {
          inView = false;
        } else {
          inView = true;
        }
      } else {
        inView = false;
      }

      // work out which direction we're moving. Not quite working right yet.
      direction = y > lastPositionY ? POCInViewDirection.down : POCInViewDirection.up;
      lastPositionY = y;

     //developer.log('In View: $inView, childOffset: ${childOffset.dy.toString()}', name: 'POCInView');
    }
    skip = false;
  }


  @override
  Widget build(BuildContext context) {
    // calculate position after build is complete. this is required to use context.findRenderObject().
    WidgetsBinding.instance.addPostFrameCallback((_) => calculatePosition(context));

    // fade in when in view.
    final oChild = AnimatedOpacity(opacity: inView ? 1 : 0, duration: const Duration(seconds: 1), child: widget.child);

    // slide in when in view, and adjust slide direction based on scroll direction.
    return AnimatedSlide(
      duration: Duration(seconds: inView ? 1 : 0),
      offset: Offset(0, inView ? 0.0 : 0.25 * (skip == true ? 0 : (direction == POCInViewDirection.up ? 1 : -1))),
      child: oChild,
    );
  }
}


2
你也可以使用 inview_notifier_list。它基本上是一个普通的 ListView,定义了一个可见区域,当子项在该区域内时会得到通知。

enter image description here


这个例子甚至都无法运行...我正在使用Flutter v2.8。 - Besnik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接