Skip to content

Return index of the first visible item in ListView #19941

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
sangmin-kim opened this issue Jul 28, 2018 · 28 comments
Open

Return index of the first visible item in ListView #19941

sangmin-kim opened this issue Jul 28, 2018 · 28 comments
Labels
c: new feature Nothing broken; request for a new capability customer: crowd Affects or could affect many people, though not necessarily a specific customer. f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. P3 Issues that are less important to the Flutter project team-framework Owned by Framework team triaged-framework Triaged by Framework team

Comments

@sangmin-kim
Copy link

In the current implementation of ListView, there is no easy way to find which item is the first one in a visible part of the screen. There could be many use cases if we know the index of the first visible item. For instance, playing audio of the first visible item, etc.
Could you please add this feature to the ListView? Thank you.

@zoechi zoechi added the framework flutter/packages/flutter repository. See also f: labels. label Jul 29, 2018
@fly512
Copy link

fly512 commented Jul 30, 2018

You can add a key to each ListView of item, and then traverse the collection of key to know which is displayed.

@sangmin-kim
Copy link
Author

Could you please add a code snippet how to find the only visible items? Thank you for your response. I appreciate it.

@sangmin-kim
Copy link
Author

Any update on this? How to get only items displayed on the screen?

@bluemix
Copy link

bluemix commented Sep 5, 2018

I hope they provide a code of it

@zoechi zoechi added the c: new feature Nothing broken; request for a new capability label Sep 6, 2018
@debuggerx01
Copy link
Contributor

My solution here: Visible Items of ListView Demo
need add a dependency first: RectGetter
164fc95dbf4a5483

@zoechi
Copy link
Contributor

zoechi commented Sep 13, 2018

The work for #10595 might provide a solution for this issue as well.

@farconada
Copy link

are there news about this point? For example I want to change TextStyle of the first visible item and apply the actions of a FloatingActionBottom over that element

@pedromorgan
Copy link

Probably also related is #12319 scrolltoIndex(idx)

@tje3d
Copy link
Contributor

tje3d commented Feb 15, 2019

@debuggerx01 RectGetter solution has bad performance and is a bit buggy ( if you wrap a TextField inside it, keyboard will disappear automatic and some more bugs ).

@marcglasberg
Copy link

#21764

@divyanshub024
Copy link

divyanshub024 commented Apr 3, 2019

Is there any update in this issue or any work around?

@jerrywell
Copy link

jerrywell commented Apr 26, 2019

Following example is used inside our app, you can use the pseudo code to get your visible widget state in any position. I assume we wanna get the position of the first row.

just wrap your children with MetaData

MetaData(
    // [this] can be a custom class, so you can locate it using the class type
    // assume this is a statefull class named DefaultRow and its state called DefaultRowState
    metaData: this,
    behavior: HitTestBehavior.translucent,
    child: row//a row inside the ListView
)

so the widget in the concept will become

ListView(
  children: [
    MetaData(),
    MetaData(),
    MetaData(),
    MetaData(),
    MetaData()
  ]
)

when we need to fetch the first visible row inside the viewport of ListView, just call as following:

final box = listViewState?.context?.findRenderObject() as RenderBox;
if (box != null) {
  // adjust it according to your list padding, 
  // e.g. you have 20's points for your top padding, 
  // just send Offset(0, 20) into [localToGlobal] or maybe 25 just in case of any other border.
  final offset = box.localToGlobal(Offset(0, 0));

  final targetState = getTargetState<DefaultRowState>(offset);
  print('offset: $offset, name: $targetState');
}

T getTargetState<T>(Offset globalPosition) {
  final HitTestResult result = new HitTestResult();
  WidgetsBinding.instance.hitTest(result, globalPosition);
  // Look for the RenderBoxes that corresponds to the hit target (the hit target
  // widgets build RenderMetaData boxes for us for this purpose).
  for (HitTestEntry entry in result.path) {
    if (entry.target is RenderMetaData) {
      final renderMetaData = entry.target as RenderMetaData;
      if (renderMetaData.metaData is T)
        return renderMetaData.metaData as T;
    }
  }

  return null;
}

@iptton
Copy link

iptton commented Apr 28, 2019

@jerrywell Thanks ! It works!

And some additional info: If you running getTargetState on some callback, it must wrap with Future.microtask ( I don't know why, can somebody tell me?)

class _VideoListState extends State<VideoList> {

  @override
  void initState() {
    super.initState();
  }

  ListView _listView;
  @override
  Widget build(BuildContext context) {
    _listView = ListView.builder(
      itemBuilder: (context,index) {
        var item = widget.list[index];
        return _getChild(item,);
      },
      itemCount: widget.list.length,
    );

    var notificationListener = NotificationListener(
      onNotification: (noti){
        if (noti is ScrollStartNotification) {
          // stop playing
        }else if(noti is ScrollEndNotification){
          // resume playing
          print("end");
          Future.microtask((){
            VideoInfo info = getMeta(0, 10);
            print("scrolling to ${info.title}");
          });
        }
      },
      child: _listView,
    );

    return notificationListener;
  }


  T getMeta<T>(double x,double y){
    var renderBox = context.findRenderObject() as RenderBox;
    var offset = renderBox.localToGlobal(Offset(x,y));

    HitTestResult result = HitTestResult();
    WidgetsBinding.instance.hitTest(result, offset);

    for(var i in result.path){
      if(i.target is RenderMetaData){
        var d = i.target as RenderMetaData;
        if(d.metaData is T) {
          return d.metaData as T;
        }
      }
    }
    return null;
  }


  Widget _getChild(VideoInfo info){
    return MetaData(
      metaData: info,
      child: VideoCard(info:info),
    );
  }
}

class VideoCard extends StatefulWidget{

  final VideoInfo info;

  const VideoCard({Key key, this.info}) : super(key: key);

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

class VideoCardState extends State<VideoCard> {
  @override
  Widget build(BuildContext context) {
    return Container(child: Text("hello world ${widget.info.title}"));
  }
}


//-------------data---------------

class VideoInfo {
  final String title;
  VideoInfo(this.title);
}

@ghost
Copy link

ghost commented Aug 19, 2019

Try adding a ScrollController to the list, then add a listener to it as shown below.

class _NumberListState extends State<NumberList> {
  ScrollController scrollController;

  // use this one if the listItem's height is known
  // or width in case of a horizontal list
  void scrollListenerWithItemHeight() {
    int itemHeight = 60; // including padding above and below the list item
    double scrollOffset = scrollController.offset;
    int firstVisibleItemIndex = scrollOffset < itemHeight
        ? 0
        : ((scrollOffset - itemHeight) / itemHeight).ceil();
    print(firstVisibleItemIndex);
  }

  // use this if total item count is known
  void scrollListenerWithItemCount() {
    int itemCount = 100;
    double scrollOffset = scrollController.position.pixels;
    double viewportHeight = scrollController.position.viewportDimension;
    double scrollRange = scrollController.position.maxScrollExtent -
        scrollController.position.minScrollExtent;
    int firstVisibleItemIndex =
        (scrollOffset / (scrollRange + viewportHeight) * itemCount).floor();
    print(firstVisibleItemIndex);
  }

  @override
  void initState() {
    super.initState();
    scrollController = ScrollController();
    scrollController.addListener(scrollListenerWithItemHeight);
  }

  @override
  void dispose() {
    super.dispose();
    scrollController.removeListener(scrollListenerWithItemHeight);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.blueGrey[200],
        appBar: AppBar(
          backgroundColor: Colors.blueGrey[500],
        ),
        body: ListView.builder(
            itemCount: 100,
            controller: scrollController,
            itemBuilder: (context, index) {
              return Padding(
                padding: const EdgeInsets.all(5.0),
                child: Container(
                    height: 50,
                    decoration: BoxDecoration(
                      color: Colors.white,
                    ),
                    child: Center(child: Text('$index'))),
              );
            }));
  }
}

@scofieldpeng
Copy link

Try adding a ScrollController to the list, then add a listener to it as shown below.

class _NumberListState extends State<NumberList> {
  ScrollController scrollController;

  // use this one if the listItem's height is known
  // or width in case of a horizontal list
  void scrollListenerWithItemHeight() {
    int itemHeight = 60; // including padding above and below the list item
    double scrollOffset = scrollController.offset;
    int firstVisibleItemIndex = scrollOffset < itemHeight
        ? 0
        : ((scrollOffset - itemHeight) / itemHeight).ceil();
    print(firstVisibleItemIndex);
  }

  // use this if total item count is known
  void scrollListenerWithItemCount() {
    int itemCount = 100;
    double scrollOffset = scrollController.position.pixels;
    double viewportHeight = scrollController.position.viewportDimension;
    double scrollRange = scrollController.position.maxScrollExtent -
        scrollController.position.minScrollExtent;
    int firstVisibleItemIndex =
        (scrollOffset / (scrollRange + viewportHeight) * itemCount).floor();
    print(firstVisibleItemIndex);
  }

  @override
  void initState() {
    super.initState();
    scrollController = ScrollController();
    scrollController.addListener(scrollListenerWithItemHeight);
  }

  @override
  void dispose() {
    super.dispose();
    scrollController.removeListener(scrollListenerWithItemHeight);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.blueGrey[200],
        appBar: AppBar(
          backgroundColor: Colors.blueGrey[500],
        ),
        body: ListView.builder(
            itemCount: 100,
            controller: scrollController,
            itemBuilder: (context, index) {
              return Padding(
                padding: const EdgeInsets.all(5.0),
                child: Container(
                    height: 50,
                    decoration: BoxDecoration(
                      color: Colors.white,
                    ),
                    child: Center(child: Text('$index'))),
              );
            }));
  }
}

this way is limit to the item height is be known, for flexible height, this is not work.

@sooxt98
Copy link

sooxt98 commented Oct 6, 2019

SOLUTION IS HERE!!! TAKE IT! https://github.com/google/flutter.widgets

ezgif-1-4c83b691065f

@andreyeurope
Copy link

I have searched a lot for something similar in Flutter, like a list that would tell you where is which element, if it is visible etc., but I've only found a few results.
One of them is the above library.
I would personally say that it is not production ready. You are given these features, but you have your flexibility taken. I tried to use the library within a CustomScrollView, but it is one itself. I'm not a Flutter expert by any means, but how do you integrate a CustomScrollView inside a CustomScrollView?

@musthafa1996
Copy link

https://pub.dev/packages/inview_notifier_list. Has anyone tried this package? It builds a ListView and notifies when the widgets are visible on the viewport.

1 similar comment
@musthafa1996
Copy link

https://pub.dev/packages/inview_notifier_list. Has anyone tried this package? It builds a ListView and notifies when the widgets are visible on the viewport.

@zoeyfan zoeyfan changed the title Returning index of the first visible item in ListView Return index of the first visible item in ListView Jan 3, 2020
@YeungKC
Copy link
Member

YeungKC commented Jun 28, 2020

I made a library: https://pub.dev/packages/widgets_visibility_provider
It can be used in customScrollVIew, ListView and other places that contain scrollview

It is very simple and flexible to use

@Mohammed3194
Copy link

Try adding a ScrollController to the list, then add a listener to it as shown below.

class _NumberListState extends State<NumberList> {
  ScrollController scrollController;

  // use this one if the listItem's height is known
  // or width in case of a horizontal list
  void scrollListenerWithItemHeight() {
    int itemHeight = 60; // including padding above and below the list item
    double scrollOffset = scrollController.offset;
    int firstVisibleItemIndex = scrollOffset < itemHeight
        ? 0
        : ((scrollOffset - itemHeight) / itemHeight).ceil();
    print(firstVisibleItemIndex);
  }

  // use this if total item count is known
  void scrollListenerWithItemCount() {
    int itemCount = 100;
    double scrollOffset = scrollController.position.pixels;
    double viewportHeight = scrollController.position.viewportDimension;
    double scrollRange = scrollController.position.maxScrollExtent -
        scrollController.position.minScrollExtent;
    int firstVisibleItemIndex =
        (scrollOffset / (scrollRange + viewportHeight) * itemCount).floor();
    print(firstVisibleItemIndex);
  }

  @override
  void initState() {
    super.initState();
    scrollController = ScrollController();
    scrollController.addListener(scrollListenerWithItemHeight);
  }

  @override
  void dispose() {
    super.dispose();
    scrollController.removeListener(scrollListenerWithItemHeight);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.blueGrey[200],
        appBar: AppBar(
          backgroundColor: Colors.blueGrey[500],
        ),
        body: ListView.builder(
            itemCount: 100,
            controller: scrollController,
            itemBuilder: (context, index) {
              return Padding(
                padding: const EdgeInsets.all(5.0),
                child: Container(
                    height: 50,
                    decoration: BoxDecoration(
                      color: Colors.white,
                    ),
                    child: Center(child: Text('$index'))),
              );
            }));
  }
}

@scofieldpeng - Here, we are able to get firstVisibleItemIndex.

But can you please help me out with getting lastVisibleItemIndex ? Help will be appreciated.

@ohhfreddypleaseno
Copy link

My solution here: Visible Items of ListView Demo
need add a dependency first: RectGetter
164fc95dbf4a5483

Your answer save my time, thanks.
BUT, your code works only with positive indexes (ex.: 0 -> 999...)
If you need to work with negative indexes (ex.: -1 -> -999...) you need to change getVisible() function as shown below:

List<int> getVisible() {
    var rect = RectGetter.getRectFromKey(listViewKey);
    var _items = <int>[];
    _keys.forEach((index, key) {
      var itemRect = RectGetter.getRectFromKey(key);
      if (itemRect != null && !(itemRect.top > rect.bottom || itemRect.bottom < rect.top)) _items.add(index);
    });

    /// Add this line, to sort negative indexes
    _items.sort();

    return _items;
  }

@ChiHwe
Copy link

ChiHwe commented May 25, 2021

My solution here: Visible Items of ListView Demo
need add a dependency first: RectGetter
164fc95dbf4a5483

U SAVED MY LIFE !!!!!!! THANK YOU !

@houchenll
Copy link

My solution here: Visible Items of ListView Demo
need add a dependency first: RectGetter
164fc95dbf4a5483

thanks, solved my problem!

@JarvanMo

This comment was marked as off-topic.

@pedromorgan

This comment was marked as off-topic.

@JarvanMo

This comment was marked as off-topic.

@Piinks Piinks added the P3 Issues that are less important to the Flutter project label Feb 21, 2023
@flutter-triage-bot flutter-triage-bot bot added team-framework Owned by Framework team triaged-framework Triaged by Framework team labels Jul 8, 2023
@SAGARSURI
Copy link

Any update on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: new feature Nothing broken; request for a new capability customer: crowd Affects or could affect many people, though not necessarily a specific customer. f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. P3 Issues that are less important to the Flutter project team-framework Owned by Framework team triaged-framework Triaged by Framework team
Projects
None yet
Development

No branches or pull requests