quire-io / scroll-to-index

scroll to index with fixed/variable row height inside Flutter scrollable widget
MIT License
518 stars 105 forks source link

Is it possible to scroll to the next index? #19

Closed hicnar closed 4 years ago

hicnar commented 4 years ago

I can see how one can scroll to subsequent indexes, but is it possible using this plugin to implement scrolling to the index of the next element displayed? What I mean here is a scenario where I scroll the view to show some element in the middle of the list I would like to scroll to the element that is positioned below the currently displayed element. Or to rephrase the question, is it possible to get the index of the currently displayed element and scroll to the one directly below/above it?

jerrywell commented 4 years ago

you can try this solution: https://github.com/flutter/flutter/issues/19941#issuecomment-486950990. : )

ref: https://github.com/quire-io/scroll-to-index/issues/4

hicnar commented 4 years ago

Thanks for your suggestion, I tried it but it was throwing exceptions especially when the mobile orientation changed from landscape to portrait (or the other way around), so I did it in a slightly different and I believe simpler way.

I added a global key to every single element held on the ListView and stored the mapping in Map<int, GlobalKey> map where the key (int) is the index of the element of the list that has the corresponding global key passed to it.

Once the view is rendered I can call the following function to get me the index based on the scroll position that can be retrieved from the scroll controller.

The _indexToKeyGetter below is ValueGetter<Map<int, GlobalKey>> that returns the map once it is rendered.

  Tuple2<int, int> getIndexForScrollPosition(double scrollPosition) {
    Map<int, Tuple2<double, double >> aa = _indexToKeyGetter().entries.fold(Map(), (prev, entry) {
      RenderBox box = entry.value.currentContext.findRenderObject();
      if (prev.isEmpty) {
        prev[entry.key] = Tuple2(0, box.size.height);
      } else {
        double previousEntry = prev[entry.key-1].item2;
        prev[entry.key] = Tuple2(previousEntry, previousEntry + box.size.height);
      }
      return prev;
    });

    var found = aa.entries.firstWhere((box) =>
    box.value.item1 <= scrollPosition && scrollPosition < box.value.item2);

    return Tuple2(found.key, scrollPosition == found.value.item1 ? -1 : 0);
  }

In the return value the index of the element is stored in the first element of the tuple, and the second element is equal to -1 if the scrollPosition (retrieved from _scrollController.offset) is placed exactly in between two elements of the list and 0 if it is somewhere in the middle of the element with the index returned in the first element.

PS Code formatting behaves in a bizare way, but you will get the idea :)

jerrywell commented 4 years ago

this is a O(n) algorithm. the suggested solution is extracted from Flutter codebase, which is at least O(Log n) since they are using the layer tree to find the target. however, it depends on your case, for a expected limited list, you can use this way.

BTW, you may cache it after first get and clear it after every build() is called. : )

hicnar commented 4 years ago

Very true, I can cache the results from the fold and since the values in the map it returns are sorted I could use binary search which would result in O(log n). But I do not expect the list to have more than 50 elements ever so for now I will keep it as is :)