Find out which items in a ListView are visible
Asked Answered
Z

4

32

How can I find out which items are currently visible or invisible in a ListView?
For example, I have 100 items in ListView and when i scroll to top of screen or list, I want to detect which items appear or disappear from the viewport.

Illustration:

enter image description here

Ziegler answered 12/7, 2019 at 17:41 Comment(2)
Why do you wanna know that? If you are going to make a list with 100 itens you should use ListView.builder witch you can pass a itemBuilder, that will build the 'widget' of a line in your listView. So the flutter will manage to you witch line is visible and render it, and clear from memory the invisible ones...Bath
yes i know that as flutter can manage that. i want to use this feature for using another listeners to use thatZiegler
G
2

You can also use inview_notifier_list. It's basically a normal ListView which defines a visible region and it's children get notified when they are in that region.

enter image description here

Greig answered 5/12, 2021 at 15:7 Comment(1)
There example is not even working... I'm using flutter v2.8Cernuous
D
16

There is no easy way to do this. Here is the same question, however, it does not have an answer.

There is an active GitHub issue about this.

There are multiple solutions for the problem in that issue. This Gist features one that requires the rect_getter package.
Alternatively, you could take a look at this proposal.

TL;DR

This is not yet implemented if you are searching for an easy way to find it out. However, there are solutions, like the ones I mentioned above and from other packages, say VisibilityDetector from flutter_widgets.

Disparate answered 12/7, 2019 at 19:8 Comment(4)
Now there is an easy way to do it, check this answer: https://mcmap.net/q/327256/-how-to-know-if-a-widget-is-visible-within-a-viewportUndersheriff
@LukasSchneider Thanks for mentioning it!Disparate
thank u both u guys, also lukas answer seems to be good solution too!Heiress
does visibilityDetector has some limitations ? i couldn’t understand that from their pub documentationStogner
K
3

There is a package for this purpose.

A VisibilityDetector widget wraps an existing Flutter widget and fires a callback when the widget's visibility changes.

Usage:

VisibilityDetector(
    key: Key('my-widget-key'),
    onVisibilityChanged: (visibilityInfo) {
      var visiblePercentage = visibilityInfo.visibleFraction * 100;
      debugPrint(
          'Widget ${visibilityInfo.key} is ${visiblePercentage}% visible');
    },
    child: someOtherWidget,
  )
Krone answered 8/11, 2021 at 2:59 Comment(0)
G
2

You can also use inview_notifier_list. It's basically a normal ListView which defines a visible region and it's children get notified when they are in that region.

enter image description here

Greig answered 5/12, 2021 at 15:7 Comment(1)
There example is not even working... I'm using flutter v2.8Cernuous
L
2

I'm Sharing for visibility on how to approach detecting position of widget in general.

I was curious as to how you access positional data of widgets, and also wanted to be able to control the animated state of a ListView child element.

Looks like the main point of access to a widgets, size and position is via the BuildContext's context.findRenderObject()

However, this is only usable after the component has been built and the widget is mounted.

This is addressed by using context.findRenderObject() in a function called using WidgetsBinding.instance.addPostFrameCallback((_) => calculatePosition(context));

Here's a wrapper component you can use in your ListView.itemBuilder() code

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,
    );
  }
}

Laburnum answered 31/1, 2023 at 16:5 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.