Skip to content
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

Provide method and/or config to programmatically scroll to ListView index #12319

Closed
ariejdl opened this issue Sep 29, 2017 · 140 comments
Closed

Provide method and/or config to programmatically scroll to ListView index #12319

ariejdl opened this issue Sep 29, 2017 · 140 comments
Assignees
Labels
a: annoyance Repeatedly frustrating issues with non-experimental functionality c: new feature Nothing broken; request for a new capability f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.
Milestone

Comments

@ariejdl
Copy link

ariejdl commented Sep 29, 2017

The ListView is successful at displaying large lists, however in some circumstances, such as a reader application it is useful to be able to focus to a certain list item offset, the iOS UITableView has - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated and on Android the ListView has either setSelection(index); or smoothScrollToPosition(index). Please can you add this feature?

@ariejdl ariejdl changed the title Provide method and or config to programmatically scroll to ListView index Provide method and/or config to programmatically scroll to ListView index Sep 29, 2017
@ariejdl
Copy link
Author

ariejdl commented Oct 3, 2017

@Hixie is this a reasonable feature request?

@Hixie
Copy link
Contributor

Hixie commented Oct 3, 2017

It's certainly a reasonable request... how easy it is to implement is a different matter. :-)

For fixed-item-height list views it's easy to work around (just jump to the index * the height). For variable-height list views, it's really difficult in the current setup (because we don't know how far to scroll). It's the same thing that's blocking #10826.

@ariejdl
Copy link
Author

ariejdl commented Oct 3, 2017

@Hixie please forgive my ignorance, but does it have to do with endScrollOffset < scrollOffset in performLayout in https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/rendering/sliver_list.dart#L213 ?

@Hixie
Copy link
Contributor

Hixie commented Oct 3, 2017

That's part of the code that makes sure that widgets that aren't visible aren't laid out, yes. And that's the reason we don't know the offset for the widgets that aren't visible.

@sroddy
Copy link
Contributor

sroddy commented Nov 25, 2017

I have to add that I spent quite a few time in the past to figure out the fact that this feature is missing as I consider it being a critical requirement for a lot of common scenarios (chats, readers, contact lists just to name a few...) and for sure at least a couple of our apps.

While I understand the complexity of an animated version of this feature, I thought that a non-animated jumpToIndex(int index, {double position}) method shouldn't be that hard to implement, as it would require to just trigger a brand new render/layout of the visible area of the list starting from the element at the index we need to make visible (instead of index 0) and then walk up and down to fill the holes.

@sroddy
Copy link
Contributor

sroddy commented Nov 25, 2017

another possible approach would be to be able to correctly estimate the height of the rows upfront and provide a correct offset to the jumpTo method. In one my specific use-cases the only variable part of the equation was to figure out how many rows a text label would have occupied when being rendered on the specific container. iOS has a method to achieve this (https://developer.apple.com/documentation/foundation/nsattributedstring/1529154-boundingrectwithsize), I''m wondering how to achieve the same goal with Flutter.

@GRouslan
Copy link

In case of smooth scrolling, if it is 1 element visible and 1001 is requested, no need to scroll through all 1000 elements.

@Hixie Hixie added framework flutter/packages/flutter repository. See also f: labels. f: scrolling Viewports, list views, slivers, etc. c: new feature Nothing broken; request for a new capability labels Nov 28, 2017
@Hixie Hixie added this to the 4: Next milestone milestone Nov 28, 2017
@Hixie
Copy link
Contributor

Hixie commented Nov 28, 2017

This is definitely something we want to offer, we just haven't implemented it yet. The general case (animating to an item selected by index) is non-trivial, but we sort of need to know what our approach for that will be before we can design the API for the simpler cases as well.

@DmitryIvanitskiy
Copy link

DmitryIvanitskiy commented Mar 2, 2018

I made a small test app implementing ScrollToLine functionality, it should work also with mixed (different lineHeight) lists. The source: https://github.com/DmitryIvanitskiy/Animation/blob/master/main.dart
My approach is: ScrollToLine renders list twice:

  1. Render list truncated from start to target line (so ScrollController.position.maxScrollExtent is offset of the last line - save it).
  2. Render full list and animateTo offset we get on step 1.
    I encountered the following problem - ScrollController.position.maxScrollExtent didn't changes instantly after setState, but only after some (random, but short) time, solution made code extremely ugly...
    Is there a way to set a listener/callback directly to ScrollController.position.maxScrollExtent ?

@eseidelGoogle
Copy link
Contributor

@volgin
Copy link

volgin commented Apr 11, 2018

In many use cases jumpTo(_index) is critical, while scrollTo(_index) is unnecessary or even undesirable.

If a user is in the middle of a long list, tapping to see the details and then coming back, scrolling all the way from the top to the desired position is a bad UX, whether it's manual or programmatic. I believe that jumpTo() is a major UX improvement in 100% of "coming back" use cases, while scrollTo is applicable only to a small subset of use cases.

@rodydavis
Copy link
Contributor

I am currently running into this. Working with Lists from a REST API and up to 500-100 items in each list and every time more are added future builder gets called and it scrolls to the top. then you have to scroll all the way to the bottom.

@seenickcode
Copy link

This would be great for PageView as well.

@marica27
Copy link

I'm implementing gallery of photos and have a button "scroll to chosen date when photo was made". So scrollToPosition also important for me.

Calculating offset is not very accurate and on the long amount of data (2-4k photos) it's not right. And I could change the layout, so I would have to rewrite logic for calculating offset, especially to support changing orientation.

@CyanBlob
Copy link

I would also love to have this feature. As it stands, I'm not really sure of a great way to implement a chat app where the message list starts scrolled to the latest message, especially since not all rows will have the same height

@zoechi
Copy link
Contributor

zoechi commented Jun 15, 2018

@zoechi
Copy link
Contributor

zoechi commented Jun 15, 2018

@marica27 does the scroll position really need to reflect the exact position over all photos?
You can query for a date in the database and just show the returned data with some extra length in front and after the shown images to make the ListView (or whatever you are using) scrollable.
You can show some short blurry scroll effect (up or down) without caring how far exactly the list would need to scroll. I don't think the users care if the scroll effect matches exactly 300 or 3000 images that might have scrolled by.

@marica27
Copy link

@zoechi Along with datePicker feature I have a draggable scrollbar showing date next to it. Have you seen Google Photos app or in Samsung Gallery App? So I want the same.
And now I'm realising that for this anyway I need to know the exact offset of all item. So scroll to position will not help me with it. And as i know offset for all items I can use it to scroll or jump to selected date.

@zoechi
Copy link
Contributor

zoechi commented Jun 15, 2018

I need to know the exact offset of all item. So scroll to position will not help me with it.

I don't follow. If you know the offset, then you can just scroll there like _scrollController.animateTo(1500.0, duration: const Duration(milliseconds: 100), curve: Curves.easeOut);

@marica27
Copy link

sorry for pure explanation. My list has 2 features. One is scroll to item with selected date and other is draggable scrollbar with label showing date for current position (as in google photos app).
When I wrote comment here first time I did not implement yet a draggable scrollbar so I did not know I need to know anyway offset for every item.
So, I'm going to calculate height of every item and it will resolve 2 my features.

@CyanBlob
Copy link

@zoechi I had tried setting reverse: true, but that caused the newest messages to be on the top, which isn't what you want for a messaging window. As it turns out, I just wasn't thinking about the problem properly. The simple solution was to reverse the order in which I pull data from the server, along with reversing the render direction of the ListView

@marcglasberg
Copy link

marcglasberg commented Jun 30, 2018

@Hixie @zoechi The core of this problem is that ListViews use a pixel offset, only. It will all be solved if you could use a pixelOffset+startIndex. At present, startIndex is implicitly always 0. So, if you want to start displaying item 1000, it has to calculate the size of all items from 0 to 999, just to know the pixel offset. Instead, to move to item 1000, some navigateToItem(1000) method could set pixelOffset to 0, and startIndex to 1000. Then, if you scroll to list to previous items you would have a negative pixelOffset. In other words, you just move the "origin" around.

@gaaclarke
Copy link
Member

@Hixie thanks for the response.

implementing itemHeight is essentially impossible (edit: or at least prohibitively expensive)

Why? I did it just fine in the sample code. If you have a data model, it is the definition of itemHeight, no calculation needed.

viewport might contain more than just a sliver list

That should be represented in your data model so you can query it as well.

A more efficient implementation for the simple case

Why is that more efficient? What is the inefficiency in the proposed data model approach?

@tarobins
Copy link
Contributor

@Hixie thanks for the response.

implementing itemHeight is essentially impossible (edit: or at least prohibitively expensive)

Why? I did it just fine in the sample code. If you have a data model, it is the definition of itemHeight, no calculation needed.

In some cases you can't know what the height of an item is until you perform a layout of the item and take into account issues such as the space used for a particular font, line breaking of text, and the size of images, the latter of which might need to be downloaded from some remote source. Having your data source return the height of a item would mean the data source will need to do this layout (and would consequently need to know something about the dimensions of the viewport, such as needing to know the width of the viewport to work out line breaking). In the case of a long list, you could need to do the layout for thousands of items to compute the position of some items, leading to the expensive computations @Hixie refers to.

@gaaclarke
Copy link
Member

gaaclarke commented Oct 25, 2019

@tarobins That would only be the case if you are displaying content that isn't known at build time and is in a form factor that isn't defined at build time and can't be spatially compressed. For example, showing user submitted images but you refuse to display them resized.

Even in your text situation, the height of a widget is deterministic and should be able to be calculated. I don't know if we've surfaced the logic to the framework but there is definitely code in the engine that given a string and a font and a width, it will tell you the height of the space needed to render it completely. It isn't the slickest solution, but I don't think the problem is all that common either. A chat room app might be the only thing I could think of. Most people can determine the height of their widgets or design their widgets to render in a specified height.

I know this because the original post asks why we can't do something that UITableView does, and that's because UITableView has a data model that looks similar to what I'm proposing. Without a data model, I dare say it's impossible to efficiently jump to a given item in a situation with heterogeneous children heights. The only solution there is to build and throw away all the proceeding widgets, which is almost in all cases going to be unacceptable.

@tarobins
Copy link
Contributor

Without a data model, I dare say it's impossible to efficiently jump to a given item in a situation with heterogeneous children heights. The only solution there is to build and throw away all the proceeding widgets, which is almost in all cases going to be unacceptable.

I'm not quite sure exactly what you mean by data model in this context. Do you mean a model that provides heights for the items or just a model of the data to be displayed?

We've solved the jumping case without knowing the heights of the items in a data model and without building all the widgets by using the method @Hixie described. And example can be found here #12319 (comment)

Smooth scrolling is a bit more complicated. There's one solution here https://github.com/google/flutter.widgets/tree/master/lib/src/scrollable_positioned_list

@gaaclarke
Copy link
Member

I'm not quite sure exactly what you mean by data model in this context. Do you mean a model that provides heights for the items or just a model of the data to be displayed?

For a ListView you want to display some state s[n]. s[n] is the data model. There also has to be a function f(s[x]) -> itemHeight. I used data model to mean s and f.

We've solved the jumping case without knowing the heights of the items in a data model and without building all the widgets by using the method @Hixie described. And example can be found here #12319 (comment)

That's an interesting approach but I think a solution that doesn't involve ScrollerController is kind of hacky in my opinion. Controlling where you are scrolled is raison-d-etre of ScrollerController.

Smooth scrolling is a bit more complicated. There's one solution here https://github.com/google/flutter.widgets/tree/master/lib/src/scrollable_positioned_list

It looks like this is caching the size of widgets, introspecting into the offset as widgets are created? It's a lot of code to accomplish something similar to what I did in 100 lines of code, just to avoid defining f(s[x]). It would not perform as well in non-scrolling case but I can see how it is easier to use for some data. I'll give it a more thorough read tomorrow.

@tarobins
Copy link
Contributor

That's an interesting approach but I think a solution that doesn't involve ScrollerController is kind of hacky in my opinion. Controlling where you are scrolled is raison-d-etre of ScrollerController.

Even if you know what pixel offset to start the ScrollController at, or jump to, the current implementation of ListView will build and layout everything between the 0 offset and your desired offset in order to know what to put at the scroll offset your asked for, so, for long lists, using the ScrollController to position the list can be inefficient.

@knuesel
Copy link

knuesel commented Oct 25, 2019

@gaaclarke for an example where the ListView items are generated in such a way that the dimensions cannot be known statically: I'm working on an application that shows a list of tasks in a ListView. Each task has dynamic elements (mainly text widgets) that the user can configure, so I rely on Flutter's layout engine to determine the dimensions of the ListView items. I'm using a horizontal list view, where dynamic dimensions along the main axis are probably more common, but the same problem could arise for a vertical list of user-editable tasks.

@tarobins
Copy link
Contributor

google/flutter.widgets#31
and google/flutter.widgets#23 should be fixed now.

@oseiasmribeiro
Copy link

Tarobins, ScrollablePositionedList worked beautifully. Help me now add a header that appears when scrolling down and disappears when scrolling up. This is very useful for showing details of a certain book, as in the Bible.

@tarobins
Copy link
Contributor

tarobins commented Nov 29, 2019 via email

@oseiasmribeiro
Copy link

Thanks a lot already Tarobins. ScrollablePositionedList is working very well. Congratulations!

@oseiasmribeiro
Copy link

Captura de Tela 2019-11-29 às 14 31 21

Tarobins, with NestedScrollView I have managed to add the header, but Pinned and Floating don't seem to work. I can't make the Header it disappear by rolling up and appear by rolling down.

@tarobins
Copy link
Contributor

I'm not super surprised. I'm just creating my experiment app, so I'll play around with it for a bit and see what's going on.

@oseiasmribeiro
Copy link

oseiasmribeiro commented Nov 29, 2019

Ok thanks. Please let me know if I succeed!

@tarobins
Copy link
Contributor

So, first, I noticed that in general floating doesn't seem to work as I expected for NestedScrollView. I tried setting in the example from https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html and the SliverAppBar didn't float as shown in SliverAppBar's docs.

I didn't have high hopes to ScrollablePositionedList to work out of the box within a NestedScrollView, and in particular because in the example for NestedScrollView it is noted for its CustomScrollView

// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.

and that's not true of ScrollablePositionedList's internal CustomScrollView. Furthermore, ScrollablePositionedList does some magic with switching what CustomScrollView is displayed sometimes, which is likely to confuse NestedScrollView.

Some work will be needed to support SliverAppBar within ScrollablePositionedList

However, if all you care about is positioning an item at the top of the list when you display it, this worked surprisingly well:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

final topKey = ValueKey('top');

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Material(
        child: SafeArea(
          child: body,
        ),
      ),
    );
  }
}

Widget get body => NestedScrollView(
      headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
        return <Widget>[
          SliverAppBar(
            title: const Text('Books'),
            // This is the title in the app bar.
            pinned: true,
            expandedHeight: 200.0,
            forceElevated: true,
          ),
        ];
      },
      body: SafeArea(
        top: false,
        bottom: false,
        child: CustomScrollView(
          center: topKey,
          slivers: <Widget>[
            SliverFixedExtentList(
              itemExtent: 48.0,
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return ListTile(
                    title: Text('Item ${14 - index}'),
                  );
                },
                childCount: 15,
              ),
            ),
            SliverFixedExtentList(
              key: topKey,
              itemExtent: 48.0,
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return ListTile(
                    title: Text('Item 15'),
                  );
                },
                childCount: 1,
              ),
            ),
            SliverFixedExtentList(
              itemExtent: 48.0,
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return ListTile(
                    title: Text('Item ${index + 16}'),
                  );
                },
                childCount: 15,
              ),
            ),
          ],
        ),
      ),
    );

@oseiasmribeiro
Copy link

Great! Thanks. This way I how get the index of scroll? I save him to back read the book where he left off.

@tarobins
Copy link
Contributor

tarobins commented Nov 29, 2019

oh. that's can't be done with the above. hmmm.

If the size of your content and screen never changes, you could just store the scroll offset https://api.flutter.dev/flutter/widgets/ScrollController/offset.html

If you can do without a collapsing app bar, then you can just have a statically sized widget with your header above the ScrollablePositionedList, all laid out in a column. Then use the ScrollablePositionedList's itemPositionsListener to get the position of items in the list.

otherwise, it's trickier.

I really should try to support a SliverAppBar on ScrollablePositionedList. I've had that request a couple times. I'll try to find some time to work on it. There's a few non-obvious behaviours we need to spec out though, such as does using ItemScrollController.scrollTo and ItemScrollController.jumpTo affect the size of the app bar and how so.

@oseiasmribeiro
Copy link

Ok. For now I go use Fixed Header, until you implement something similar in ScrollablePositionedList. Thank you so much for everything.

@tarobins
Copy link
Contributor

I'm going to close this issue with ScrollablePositionedList https://github.com/google/flutter.widgets/tree/master/lib/src/scrollable_positioned_list available. I am still working on the library and there are some fixes already submitted to the internal google repo that should be pushed to the github repo soon. Please file additional bugs and feature requests here: https://github.com/google/flutter.widgets/issues

@duzenko

This comment has been minimized.

@tarobins
Copy link
Contributor

tarobins commented Mar 5, 2020

I don't see a solution that can be used on ListView itself without making a mess of the API because there's a conflict between pixel-based and item-based scrolling. When we jump or scroll to a item that's far down a list, if we want to keep the pixel-based and item-based indices consistent, we'd need to layout every item before the desired item, which can be prohibitively expensive.

@duzenko

This comment has been minimized.

@tarobins
Copy link
Contributor

tarobins commented Mar 6, 2020

I can understand that one would want a solution integrated with the existing ListView; however, I wasn't able to think of one that I thought would be workable and at the same time that I thought would be an acceptable change to the core ListView. I did create a solution that I thought got to the core of what was originally asked for in this issue.

I know there are still some desired features. For example, I have seen the requests to have the ScrollablePositionedList work with a collapsible appbar, unfortunately I haven't had time to explore a solution to that use case. (More requests can be seen or added at https://github.com/google/flutter.widgets/issues, and people are encouraged to fork my solution and submit PRs, or just create new solutions based on mine or from scratch). If you have use cases not covered, please open a new issue (perhaps with a linking comment from here so people interested see the new issue), with an exact explanation of not-yet-covered needs and use cases. Doing so will help someone provide solutions that meet the use cases you have. Also, if more documentation is needed for the solution I provided, please let me know, and I’ll work on providing updates, as I do want people to be able to use it.

As for this issue, given that I am the current assignee of the issue, I didn’t want to leave the issue open, and give the impression that I was still working on the issue past the solution I was able to come up with. However, if someone does want to continue to work on integrating a solution with ListView, I'm happy to hand over this issue and they can reopen it. However, if no one has a feasible change to ListView in mind, I'm not sure leaving this issue open is useful, as doing so might give people an incorrect impression that a change to ListView is being worked on, or eventually will be worked on at some point in the reasonable future. If such a change is not foreseeable, it might be best to direct energies to coming up with new widgets that meet peoples’ use cases, which, personally, I think is the more obtainable path.

@marcglasberg
Copy link

marcglasberg commented Mar 6, 2020

@tarobins However, it would be great if this widget could have its own package in pub/dev, instead of being bundled with other widgets. That's totally non-standard. I am guilty of doing that myself (https://pub.dev/packages/assorted_layout_widgets) but I think ScrollablePositionedList is just too important to be buried into a package called flutter.widgets.

@tarobins
Copy link
Contributor

tarobins commented Apr 1, 2020

exciting news! ScrollablePositionedList is now available in its own package on pub: https://pub.dev/packages/scrollable_positioned_list

Please start pointing to that package.

@tarobins
Copy link
Contributor

tarobins commented Apr 1, 2020

Thanks particularly to @mehmetf

@lock
Copy link

lock bot commented Apr 15, 2020

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

@lock lock bot locked and limited conversation to collaborators Apr 15, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
a: annoyance Repeatedly frustrating issues with non-experimental functionality c: new feature Nothing broken; request for a new capability f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.
Projects
Scrolling Refactor
Awaiting triage
Development

No branches or pull requests