Flutter: Scrolling to a widget in ListView
Asked Answered
I

23

198

How can I scroll to a special widget in a ListView? For instance I want to scroll automatically to some Container in the ListView if I press a specific button.

ListView(children: <Widget>[
  Container(...),
  Container(...), // scroll for example to this container 
  Container(...)
]);
Incriminate answered 7/3, 2018 at 13:25 Comment(2)
Using SliverList? Here is what I did: #63650698Richma
Use: pub.dev/packages/hidableBulganin
P
262

By far, the easiest solution is to use Scrollable.ensureVisible(context). As it does everything for you and work with any widget size. Fetching the context using GlobalKey.

The problem is that ListView won't render non-visible items. Meaning that your target most likely will not be built at all. Which means your target will have no context ; preventing you from using that method without some more work.

In the end, the easiest solution will be to replace your ListView by a SingleChildScrollView and wrap your children into a Column. Example :

class ScrollView extends StatelessWidget {
  final dataKey = new GlobalKey();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      primary: true,
      appBar: new AppBar(
        title: const Text('Home'),
      ),
      body: new SingleChildScrollView(
        child: new Column(
          children: <Widget>[
            new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
            new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
            new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
            // destination
            new Card(
              key: dataKey,
              child: new Text("data\n\n\n\n\n\ndata"),
            )
          ],
        ),
      ),
      bottomNavigationBar: new RaisedButton(
        onPressed: () => Scrollable.ensureVisible(dataKey.currentContext),
        child: new Text("Scroll to data"),
      ),
    );
  }
}

NOTE : While this allows to scroll to the desired item easily, consider this method only for small predefined lists. As for bigger lists you'll get performance problems.

But it's possible to make Scrollable.ensureVisible work with ListView ; although it will require more work.

Parsee answered 7/3, 2018 at 14:56 Comment(11)
As stated, this solution works fine for a short list, implemented as a Column. How could this be modified to work with a CustomScrollView that contains SliverLists?Revegetate
I'm getting this error type 'List<dynamic>' is not a subtype of type 'GlobalKey<State<StatefulWidget>>' of 'function result'. How should I fix it?Cherokee
ListView and SingleChildScrollView are completely different beasts. If the use case would fit in SingleChildScrollView then this question would not exist in the first place.Sherise
If someone needs to scroll after build then WidgetsBinding.instance.addPostFrameCallback((_) => Scrollable.ensureVisible(dataKey.currentContext)) could be used.Aberration
@SarpBaşaraner they might be but this issue actually helped me with SingleChildScrollView so it is a very useful answer!Ejaculation
@Remi, i think you have best answer of it. Please help me with a solution. #69796003Panda
Null safety: Scrollable.ensureVisible( widgetKey.currentContext ?? context, curve: Curves.ease, duration: const Duration(seconds: 1), );Kerstin
Better approach: use ListView with a large cacheExtent, let's say 1000. This is good enough if you don't need to jump huge distances, and you can fallback gracefully if it fails.Northerly
Since ListView cannot render invisible items that makes related GlobalKey's BuildContext to be null,I thought we'll have to replace ListView with SingleChildScrollView to make it work. Is that meaning of more work here?Emergence
Plz make a full code example of the "more work" required to make Scrollable.ensureVisible work with ListView!Younker
If your SingleChildScrollView is wrapped in another scrollable widget that scrolls on the other axis, ensureVisible will unfortunately (for my usecase) scroll on both axis.Chucho
M
93

Unfortunately, ListView has no built-in approach to a scrollToIndex() function. You’ll have to develop your own way to measure to that element’s offset for animateTo() or jumpTo(), or you can search through these suggested solutions/plugins or from other posts like flutter ListView scroll to index not available

(the general scrollToIndex issue is discussed at flutter/issues/12319 since 2017, but still with no current plans)


But there is a different kind of ListView that does support scrollToIndex:

You set it up exactly like ListView and works the same, except you now have access to a ItemScrollController that does:

  • jumpTo({index, alignment})
  • scrollTo({index, alignment, duration, curve})

Simplified example:

ItemScrollController _scrollController = ItemScrollController();

ScrollablePositionedList.builder(
  itemScrollController: _scrollController,
  itemCount: _myList.length,
  itemBuilder: (context, index) {
    return _myList[index];
  },
)

_scrollController.scrollTo(index: 150, duration: Duration(seconds: 1));

Please not that although the scrollable_positioned_list package is published by google.dev, they explicitly state that their packages are not officially supported Google products. - Source

Mccarthyism answered 18/11, 2019 at 23:4 Comment(11)
This works perfectly. The other answers are OK if all the sizes of the items are the same, if not the scroll will not be precise.Consociate
Nice answer. However, ScrollablePositionedList doesn't have shrinkWrap property and can not be used with Slivers.Pledget
@TehSunnLiu, have you found a way to use it with slivers?Iey
Hi @Daniel, As mentioned in this (https://mcmap.net/q/127624/-flutter-scrolling-to-a-widget-in-listview) answer above. Using Scrollable.ensureVisible(widgetKey.currentContext) does scroll the widgets even in Slivers. All you have to do is set Global Keys for your widgets and call Scrollable.ensureVisible on the key of your widget you want to scroll to. For this to work your ListView should be a finite List of objects. If you are using ListView.builder. I would suggest you to set physics: NeverScrollableScrollPhysics and shrinkWrap to true.Pledget
This package (scrollable_positioned_list) doesn't support scrolling to items of variable (not fixed/constant) height, right? That's why I'd recommend scroll_to_index.Farber
This package doesn't support primary/non-primary scroll views (hard/impossible to use inside sliver scrollable views properly with Cupertino refresh indicator), doesn't have a scrollController (impossible to have scrollbar with), and after intense use (5-10 min of playing and intense scrolling with) occasionally inaccurately display the current number of widgets on the screen (used to check when to pre-load more data - which destroys an infinite feed). I've walked the path with this package and it wasn't good... just a heads up. Edit: package in question is scrollable_positioned_list.Silverpoint
@MatthewTrent what have you decided to go with then? I need a ListView to jump to a specific index, but currently have a primary setup on my page. Any suggestions?Bahrain
@DennisAshford, I ended up going with scrollable_positioned_list, just because of hope in the future (before my product launches), it gets aScrollController (which then can be used to fix some of its problems). However, an alternative package, which houses less, but in my finding more reliable tools, is scrollview_observer. Another (hacky) way of adding a ScrollController is to have a CustomListView and to have the scrollable_positioned_list as a sliver inside of it, with Ignore Pointer set to true, and scroll physics set to never. This works [continued next comment]Silverpoint
because it allows for you to track the outer CustomScrollView except it'll miss the scroll event on the inner scrollable_positioned_list about 1.5 seconds after you've stopped scrolling. It misses it exactly on that one frame, and then on fast concurrent events (same frame a scrollbar disappears if you have one attached to the outer CustomScrollView). I also just added a bounty on a question I have asking for the scrollable_positioned_list to get a ScrollController... hopefully that yields some positive results.Silverpoint
can I pass index parameter to this itemScrollController ? How to ?Spurge
One issue I had with scrollable_positioned_list is that there is often a bounce animation. This happens, for example, when scrolling to the last element in a list, as Flutter attempts to align the top of the element with the top of the view. There currently doesn't seem to be a solution to this yet, and I haven't been able to find a workaround that fixes it. So, for now, the package doesn't fit my needs.Comfy
Q
56

Screenshot (Fixed height content)

enter image description here


If your items have fixed height, then you can use the following approach.

class HomePage extends StatelessWidget {
  final ScrollController _controller = ScrollController();
  final double _height = 100.0;

  void _animateToIndex(int index) {
    _controller.animateTo(
      index * _height,
      duration: Duration(seconds: 2),
      curve: Curves.fastOutSlowIn,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.arrow_downward),
        onPressed: () => _animateToIndex(10),
      ),
      body: ListView.builder(
        controller: _controller,
        itemCount: 20,
        itemBuilder: (_, i) {
          return SizedBox(
            height: _height,
            child: Card(
              color: i == 10 ? Colors.blue : null,
              child: Center(child: Text('Item $i')),
            ),
          );
        },
      ),
    );
  }
}
Qualified answered 17/10, 2019 at 15:14 Comment(11)
I tried this on a list whose items were not of the same height and it wasn't scrolling to the precise position. You should add a not that it works best for a list with items of the same height(or width if its a horizontal scroll). I ended up using @TWL's solution which uses the item's index.Consociate
instead of a button you might want to do the animation as soon as the widget is built. if so, add WidgetsBinding.instance.addPostFrameCallback((_) => yourFunc(context)); to itemBuilderDeflower
@Qualified is there any way to find index when scrolling? I mean scroll up and down find top index?Congener
I have main ListView(vertical) and category ListView(horizonal). same like this youtube.com/watch?v=LrOR5QOCHBI I am trying to implement this kind of two listView.Congener
I want to find the current index when scrolling the main ListView. then I want to point category listView. (I decide to use this plugin for that pub.dev/packages/scroll_to_index). So, is there any way to find out current index?Congener
Or can you suggest to implement this? youtube.com/watch?v=LrOR5QOCHBICongener
@BloodLoss Let me get to my system later today, I'll take a look at it (currently I am on phone).Qualified
Thanks you so much for helping to meCongener
@BloodLoss I saw the video, didn't take a look at the package though. What you can do is, you can get the ListView's offset using ScrollController and you must be aware of dimensions of views you're showing in the ListView or a CustomScrollView.Qualified
@Qualified thanks for try to support.. I implemented this feature same like above videoCongener
Now what's gonna happen when you try to scroll to the latest item? The scrollbar will try to keep scrolling, till the latest gonna be in the top, which is not possible obviously, this causes a redundant "extra scrolling" effect.Vent
C
40

You can use GlobalKey to access buildercontext.

I use GlobalObjectKey with Scrollable.

Define GlobalObjectKey in item of ListView

ListView.builder(
itemCount: category.length,
itemBuilder: (_, int index) {
return Container(
    key: GlobalObjectKey(category[index].id),

You can navigate to item from anywhere

InkWell(
  onTap: () {
Scrollable.ensureVisible(GlobalObjectKey(category?.id).currentContext);

You add scrollable animation changing property of ensureVisible

Scrollable.ensureVisible(
  GlobalObjectKey(category?.id).currentContext,
  duration: Duration(seconds: 1),// duration for scrolling time
  alignment: .5, // 0 mean, scroll to the top, 0.5 mean, half
  curve: Curves.easeInOutCubic);
Congener answered 12/4, 2022 at 4:57 Comment(7)
I tried that to highlight an item in a list of items in an horizontal listview, it works perfectly. I went with a scroll controller for some time, but the best result was clearly with your method.Agathaagathe
I think this is the best solution.Stephanotis
Best answer for me, at least. Thank you, friend!Defrock
It works perfectly for small lists but for large lists, because items are not rendered by listView builder it doesn't work as good, unless the item is rendered.Shirelyshirey
ListView.builder only build it's child when visible in viewport and the outside viewport child don't have context yet. So Scrollable.ensureVisible not work. I got error context is null from outside viewport child. api.flutter.dev/flutter/widgets/ListView/ListView.builder.htmlWingless
For me works perfectly even inside of SingleChildScrollViewOutport
I used this and it worked but i have 2 horizontal list. if i use this approach for my second horizontal listview it is giving me error that globalobjectkey is already use..although i pass 2 different keys to itGradey
B
31

For people are trying to jump to widget in CustomScrollView. First, add this plugin to your project.

Then look at my example code below:

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  AutoScrollController _autoScrollController;
  final scrollDirection = Axis.vertical;

  bool isExpaned = true;
  bool get _isAppBarExpanded {
    return _autoScrollController.hasClients &&
        _autoScrollController.offset > (160 - kToolbarHeight);
  }

  @override
  void initState() {
    _autoScrollController = AutoScrollController(
      viewportBoundaryGetter: () =>
          Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
      axis: scrollDirection,
    )..addListener(
        () => _isAppBarExpanded
            ? isExpaned != false
                ? setState(
                    () {
                      isExpaned = false;
                      print('setState is called');
                    },
                  )
                : {}
            : isExpaned != true
                ? setState(() {
                    print('setState is called');
                    isExpaned = true;
                  })
                : {},
      );
    super.initState();
  }

  Future _scrollToIndex(int index) async {
    await _autoScrollController.scrollToIndex(index,
        preferPosition: AutoScrollPosition.begin);
    _autoScrollController.highlight(index);
  }

  Widget _wrapScrollTag({int index, Widget child}) {
    return AutoScrollTag(
      key: ValueKey(index),
      controller: _autoScrollController,
      index: index,
      child: child,
      highlightColor: Colors.black.withOpacity(0.1),
    );
  }

  _buildSliverAppbar() {
    return SliverAppBar(
      brightness: Brightness.light,
      pinned: true,
      expandedHeight: 200.0,
      backgroundColor: Colors.white,
      flexibleSpace: FlexibleSpaceBar(
        collapseMode: CollapseMode.parallax,
        background: BackgroundSliverAppBar(),
      ),
      bottom: PreferredSize(
        preferredSize: Size.fromHeight(40),
        child: AnimatedOpacity(
          duration: Duration(milliseconds: 500),
          opacity: isExpaned ? 0.0 : 1,
          child: DefaultTabController(
            length: 3,
            child: TabBar(
              onTap: (index) async {
                _scrollToIndex(index);
              },
              tabs: List.generate(
                3,
                (i) {
                  return Tab(
                    text: 'Detail Business',
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        controller: _autoScrollController,
        slivers: <Widget>[
          _buildSliverAppbar(),
          SliverList(
              delegate: SliverChildListDelegate([
            _wrapScrollTag(
                index: 0,
                child: Container(
                  height: 300,
                  color: Colors.red,
                )),
            _wrapScrollTag(
                index: 1,
                child: Container(
                  height: 300,
                  color: Colors.red,
                )),
            _wrapScrollTag(
                index: 2,
                child: Container(
                  height: 300,
                  color: Colors.red,
                )),
          ])),
        ],
      ),
    );
  }
}

Yeah it's just a example, use your brain to make it this idea become true enter image description here

Brayton answered 10/5, 2020 at 9:48 Comment(4)
@AXE, any way of making the app bar reacting to the scroll? I'm trying to make this: twitter.com/maurodibert/status/1366341648343568389 Thanks!Rectum
@AXE here could be the solution: titanwolf.org/Network/Articles/…Rectum
@AXE sorry, but It shows how to achieve the same but no synced.Rectum
Point to note here is that you don't have to wrap every widget with _wrapScrollTag(), wrap only the widgets you want to scroll to and it will work as intended.Subject
T
29

This solution improves upon other answers as it does not require hard-coding each elements' heights. Adding ScrollPosition.viewportDimension and ScrollPosition.maxScrollExtent yields the full content height. This can be used to estimate the position of an element at some index. If all elements are the same height, the estimation is perfect.

// Get the full content height.
final contentSize = controller.position.viewportDimension + controller.position.maxScrollExtent;
// Index to scroll to.
final index = 100;
// Estimate the target scroll position.
final target = contentSize * index / itemCount;
// Scroll to that position.
controller.position.animateTo(
  target,
  duration: const Duration(seconds: 2),
  curve: Curves.easeInOut,
);

And a full example:

user clicks a button to scroll to the one hundredth element of a long list

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter Test",
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = ScrollController();
    final itemCount = 1000;
    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter Test"),
      ),
      body: Column(
        children: [
          ElevatedButton(
            child: Text("Scroll to 100th element"),
            onPressed: () {
              final contentSize = controller.position.viewportDimension + controller.position.maxScrollExtent;
              final index = 100;
              final target = contentSize * index / itemCount;
              controller.position.animateTo(
                target,
                duration: const Duration(seconds: 2),
                curve: Curves.easeInOut,
              );
            },
          ),
          Expanded(
            child: ListView.builder(
              controller: controller,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text("Item at index $index."),
                );
              },
              itemCount: itemCount,
            ),
          )
        ],
      ),
    );
  }
}

Theomachy answered 6/2, 2022 at 1:19 Comment(1)
That is a really cool makeshift way to scroll through listviews with similar or almost similar widgets.Machree
M
10

You can just specify a ScrollController to your listview and call the animateTo method on button click.

A mininmal example to demonstrate animateTo usage :

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => new _ExampleState();
}

class _ExampleState extends State<Example> {
  ScrollController _controller = new ScrollController();

  void _goToElement(int index){
    _controller.animateTo((100.0 * index), // 100 is the height of container and index of 6th element is 5
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOut);
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(),
      body: new Column(
        children: <Widget>[
          new Expanded(
            child: new ListView(
              controller: _controller,
              children: Colors.primaries.map((Color c) {
                return new Container(
                  alignment: Alignment.center,
                  height: 100.0,
                  color: c,
                  child: new Text((Colors.primaries.indexOf(c)+1).toString()),
                );
              }).toList(),
            ),
          ),
          new FlatButton(
            // on press animate to 6 th element
            onPressed: () => _goToElement(6),
            child: new Text("Scroll to 6th element"),
          ),
        ],
      ),
    );
  }
}
Mccleary answered 7/3, 2018 at 13:42 Comment(6)
Too complicated. Especially for unknows sized elements.Directrix
Not sure if there is any way to scroll to a specific widget when size is unknown. If can be implemented with minor changes, feel free to edit my answer. If not do post an answer so that even I can know other ways of doing it. Thanks @DarkyMccleary
Anybody found a way of doing this for items with different heights? This is a total showstopper for flutter :(Noodle
Hi @StanMots, I answered this when flutter was still in its initial alpha releases. There has been a lot of improvements and now we can scroll to a specific child with ensureVisible method on a Scrollable. I'll try change and update the answer to show a right and optimal solution.Mccleary
Thanks, @HemanthRaj this was simple and clear worked for meIndolent
@HemanthRaj not working for scrollDirection: Axis.horizontal listViewCongener
G
7

Here is the solution for StatefulWidget if you want to made widget visible right after building the view tree.

By extending Remi's answer, you can achieve it with this code:

class ScrollView extends StatefulWidget {
  // widget init
}

class _ScrollViewState extends State<ScrollView> {

  final dataKey = new GlobalKey();

  // + init state called

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      primary: true,
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: _renderBody(),
    );
  }

  Widget _renderBody() {
    var widget = SingleChildScrollView(
        child: Column(
          children: <Widget>[
           SizedBox(height: 1160.0, width: double.infinity, child: new Card()),
           SizedBox(height: 420.0, width: double.infinity, child: new Card()),
           SizedBox(height: 760.0, width: double.infinity, child: new Card()),
           // destination
           Card(
              key: dataKey,
              child: Text("data\n\n\n\n\n\ndata"),
            )
          ],
        ),
      );
    setState(() {
        WidgetsBinding.instance!.addPostFrameCallback(
              (_) => Scrollable.ensureVisible(dataKey.currentContext!));
    });
    return widget;
  }
}

Garmon answered 14/3, 2021 at 11:21 Comment(2)
this answer worked for me as a charm. thank you.Ticonderoga
for a good animation you can also add (duration) like this: Scrollable.ensureVisible(dataKey.currentContext!, duration: Duration(seconds: 1))Ticonderoga
T
6

I found a perfect solution to it using ListView.
I forgot where the solution comes from, so I posted my code. This credit belongs to other one.

21/09/22:edit. I posted a complete example here, hope it is clearer.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class CScrollToPositionPage extends StatefulWidget {

CScrollToPositionPage();

@override
State<StatefulWidget> createState() => CScrollToPositionPageState();
}

class CScrollToPositionPageState extends State<CScrollToPositionPage> {
static double TEXT_ITEM_HEIGHT = 80;
final _formKey = GlobalKey<FormState>();
late List _controls;
List<FocusNode> _lstFocusNodes = [];

final __item_count = 30;

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

    _controls = [];
    for (int i = 0; i < __item_count; ++i) {
        _controls.add(TextEditingController(text: 'hello $i'));

        FocusNode fn = FocusNode();
        _lstFocusNodes.add(fn);
        fn.addListener(() {
            if (fn.hasFocus) {
                _ensureVisible(i, fn);
            }
        });
    }
}

@override
void dispose() {
    super.dispose();

    for (int i = 0; i < __item_count; ++i) {
        (_controls[i] as TextEditingController).dispose();
    }
}

@override
Widget build(BuildContext context) {
    List<Widget> widgets = [];
    for (int i = 0; i < __item_count; ++i) {
        widgets.add(TextFormField(focusNode: _lstFocusNodes[i],controller: _controls[i],));
    }

    return Scaffold( body: Container( margin: const EdgeInsets.all(8),
        height: TEXT_ITEM_HEIGHT * __item_count,
        child: Form(key: _formKey, child: ListView( children: widgets)))
    );
}

Future<void> _keyboardToggled() async {
    if (mounted){
        EdgeInsets edgeInsets = MediaQuery.of(context).viewInsets;
        while (mounted && MediaQuery.of(context).viewInsets == edgeInsets) {
            await Future.delayed(const Duration(milliseconds: 10));
        }
    }

    return;
}
Future<void> _ensureVisible(int index,FocusNode focusNode) async {
    if (!focusNode.hasFocus){
        debugPrint("ensureVisible. has not the focus. return");
        return;
    }

    debugPrint("ensureVisible. $index");
    // Wait for the keyboard to come into view
    await Future.any([Future.delayed(const Duration(milliseconds: 300)), _keyboardToggled()]);


    var renderObj = focusNode.context!.findRenderObject();
    if( renderObj == null ) {
      return;
    }
    var vp = RenderAbstractViewport.of(renderObj);
    if (vp == null) {
        debugPrint("ensureVisible. skip. not working in Scrollable");
        return;
    }
    // Get the Scrollable state (in order to retrieve its offset)
    ScrollableState scrollableState = Scrollable.of(focusNode.context!)!;

    // Get its offset
    ScrollPosition position = scrollableState.position;
    double alignment;

    if (position.pixels > vp.getOffsetToReveal(renderObj, 0.0).offset) {
        // Move down to the top of the viewport
        alignment = 0.0;
    } else if (position.pixels < vp.getOffsetToReveal(renderObj, 1.0).offset){
        // Move up to the bottom of the viewport
        alignment = 1.0;
    } else {
        // No scrolling is necessary to reveal the child
        debugPrint("ensureVisible. no scrolling is necessary");
        return;
    }

    position.ensureVisible(
        renderObj,
        alignment: alignment,
        duration: const Duration(milliseconds: 300),
    );

}

}
Thermochemistry answered 15/1, 2020 at 9:10 Comment(4)
Hi. Is it possible to add some explanation, like what does the code do? or a screenshot? It will be of great help. Thanks.Pledget
@Thermochemistry what to initialize mounted with?Enthronement
@SaugatThapa mounted is a variable that already exists on the Stateful Widget as an indicator of whether the widget is attached and operable or not (e.g. already popped). You just need to check whether it's true or false. If it's false, prevent any further code that may lead to this.setState().Moo
this is not perfect .. this is the worst solution herePero
P
5

Output:

Use Dependency:

dependencies:
    scroll_to_index: ^1.0.6

Code: (Scroll will always perform 6th index widget as its added below as hardcoded, try with scroll index which you required for scrolling to specific widget)

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  final scrollDirection = Axis.vertical;

  AutoScrollController controller;
  List<List<int>> randomList;

  @override
  void initState() {
    super.initState();
    controller = AutoScrollController(
        viewportBoundaryGetter: () =>
            Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
        axis: scrollDirection);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView(
        scrollDirection: scrollDirection,
        controller: controller,
        children: <Widget>[
          ...List.generate(20, (index) {
            return AutoScrollTag(
              key: ValueKey(index),
              controller: controller,
              index: index,
              child: Container(
                height: 100,
                color: Colors.red,
                margin: EdgeInsets.all(10),
                child: Center(child: Text('index: $index')),
              ),
              highlightColor: Colors.black.withOpacity(0.1),
            );
          }),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _scrollToIndex,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
  // Scroll listview to the sixth item of list, scrollling is dependent on this number
  Future _scrollToIndex() async {
    await controller.scrollToIndex(6, preferPosition: AutoScrollPosition.begin);
  }
}
Parquet answered 6/8, 2020 at 12:44 Comment(4)
Thanks! this solution works perfectly for my use case as I had some actions related to the scroll controller and the other library mentioned in a previous answer (scrollable_positioned_list) didn't offer a solution for that.Cobbie
How to achieve the same thing without a floating button? Like when a user swipes left and right?Theone
@KamrulHasanJony: Try Listview in horizontal, this should workParquet
tried that. The user can stop between two list items. So to solve this issue i used PageView Builder and it works like a charm.Theone
D
3
  1. To achieve initial scrolling at a particular index in a list of items
  2. on tap of the floating action button you will be scrolled to an index of 10 in a list of items
    class HomePage extends StatelessWidget {
      final _controller = ScrollController();
      final _height = 100.0;
    
      @override
      Widget build(BuildContext context) {
        
        // to achieve initial scrolling at particular index
        SchedulerBinding.instance.addPostFrameCallback((_) {
          _scrollToindex(20);
        });
    
        return Scaffold(
          appBar: AppBar(),
          floatingActionButton: FloatingActionButton(
            onPressed: () => _scrollToindex(10),
            child: Icon(Icons.arrow_downward),
          ),
          body: ListView.builder(
            controller: _controller,
            itemCount: 100,
            itemBuilder: (_, i) => Container(
              height: _height,
              child: Card(child: Center(child: Text("Item $i"))),
            ),
          ),
        );
      }
    // on tap, scroll to particular index
      _scrollToindex(i) => _controller.animateTo(_height * i,
          duration: Duration(seconds: 2), curve: Curves.fastOutSlowIn);
    }
Diphosgene answered 16/5, 2021 at 19:44 Comment(1)
Answers that doesn't contain anything other than code aren't good, please add an explanation of what you have done.Aggrandize
L
2

I am posting a solution here in which List View will scroll 100 pixel right and left . you can change the value according to your requirements. It might be helpful for someone who want to scroll list in both direction

import 'package:flutter/material.dart';

class HorizontalSlider extends StatelessWidget {
 HorizontalSlider({Key? key}) : super(key: key);

// Dummy Month name
List<String> monthName = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"July",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
 ];
ScrollController slideController = new ScrollController();

@override
Widget build(BuildContext context) {
 return Container(
  child: Flex(
    direction: Axis.horizontal,
    crossAxisAlignment: CrossAxisAlignment.center,
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      InkWell(
        onTap: () {
          // Here monthScroller.position.pixels represent current postion 
          // of scroller
           slideController.animateTo(
            slideController.position.pixels - 100, // move slider to left
             duration: Duration(
              seconds: 1,
            ),
            curve: Curves.ease,
          );
        },
        child: Icon(Icons.arrow_left),
      ),
      Container(
        height: 50,
        width: MediaQuery.of(context).size.width * 0.7,
        child: ListView(
          scrollDirection: Axis.horizontal,
          controller: slideController,
          physics: ScrollPhysics(),
          children: monthName
              .map((e) => Padding(
                    padding: const EdgeInsets.all(12.0),
                    child: Text("$e"),
                  ))
              .toList(),
        ),
      ),
      GestureDetector(
        onTap: () {
          slideController.animateTo(
            slideController.position.pixels +
                100, // move slider 100px to right
            duration: Duration(
              seconds: 1,
            ),
            curve: Curves.ease,
          );
        },
        child: Icon(Icons.arrow_right),
      ),
    ],
  ),
);
 }
 }
Loiretcher answered 23/7, 2021 at 17:46 Comment(0)
S
2

The simplest way is to call this method inside your InitState method. (not the build to evict unwanted errors)

WidgetsBinding.instance.addPostFrameCallback((_) => Scrollable.ensureVisible(targetKey.currentContext!))

WidgetsBinding.instance.addPostFrameCallback will guarantee that the list is builded and the this automatic search for your target and move the scroll to it. You can then customize the animation of the scroll effect on the Scrollable.ensureVisible method

Note: Remember to add the targetKey (a GlobalKey) to the widget you want to scroll to.

Sapsago answered 5/10, 2022 at 3:38 Comment(0)
M
2

You perfectly can use ensureVisibility() but need to convert your ListView into a SingleChildScrollView so that you are not affected by the ListView own widget visibility optimization process (not all the widget are actually rendered at a given time).

Here is an example that I'm sure will benefit many of you: An interactive debugger that let you follow a program being ran in live.

Say you have a list of Widgets that materializes the program instructions to be executed and you want to follow the execution by looking at the running instruction no matter where in the program it is located.

  1. Define a unique GlobalKey that designates you running instruction.

  2. In the build, you set the key to the currently running instruction (aka the widget you always want to be visible) while let null to the others.

  3. Then simply add a WidgetsBinding.instance.addPostFrameCallback within which you call the Scrollable.ensureVisible passing the running instruction widget context.

And voilà.

Here is a working DartPad

class Debugger extends StatelessWidget {

  
  const Debugger(Simulator simulator);

  /// The key that designates the visible widget
  final currentIpKey = GlobalKey();

  List<Widget> get steps {
    final l = <Widgets>[];

    for(int i=0; i<simulator.program.length; i++) {

      // == Assign the key to the widget we wanna see, while
      // leaving null to others 
      final key = i == simulator.ip ? currentIpKey : null;

      l.add(Text(key:key, simulator.program[i]);
    }

    return l;
  }

  @override Widget build(BuildContext context) {

  return Scaffold(
      appBar: AppBar(
          title: Text("Battery BMS simulator"),
          
      body: Card(
        child: AnimatedBuilder(
            animation: widget.simulator,
            builder: (context, child) {

              final allSteps = steps;

              // == Add the post build call to scroll to the visible
              // widget.

              WidgetsBinding.instance.addPostFrameCallback((_) {
                if (currentIpKey.currentContext != null)
                  Scrollable.ensureVisible(
                    currentIpKey.currentContext!,
                    duration: Duration(seconds: 1),
                  );
              });

              // New code:
              return SingleChildScrollView(
                physics: const BouncingScrollPhysics(),
                child: Column(children: allSteps),
              );

              // Former code
              //return ListView(
              //  physics: const BouncingScrollPhysics(),
              //  children: allSteps,
              //);
            }),
      ),
    );
  }

}
Merrifield answered 26/9, 2023 at 16:4 Comment(1)
Thanks a lot, WidgetsBinding.instance.addPostFrameCallback((_) {...} helped me.Denunciate
B
0

Adding with Rémi Rousselet's answer,

If there is a case you need to scroll past to end scroll position with addition of keyboard pop up, this might be hided by the keyboard. Also you might notice the scroll animation is a bit inconsistent when keyboard pops up(there is addition animation when keyboard pops up), and sometimes acts weird. In that case wait till the keyboard finishes animation(500ms for ios).

BuildContext context = key.currentContext;
  Future.delayed(const Duration(milliseconds: 650), () {
    Scrollable.of(context).position.ensureVisible(
        context.findRenderObject(),
        duration: const Duration(milliseconds: 600));
  });
Bifoliolate answered 25/2, 2021 at 22:41 Comment(0)
C
0

You can also simply use the FixedExtentScrollController for same size items with the index of your initialItem :

controller: FixedExtentScrollController(initialItem: itemIndex);

The documentation : Creates a scroll controller for scrollables whose items have the same size.

Chequer answered 17/2, 2022 at 23:2 Comment(1)
FixedExtentScrollController can only be used with ListWheelScrollViews.Shluh
B
0

Using flutter_scrollview_observer package can help you resolve this

ListObserverController observerController = ListObserverController(controller: scrollController);

ListViewObserver(
  controller: observerController,
  child: _buildListView(),
  ...
)

Now you can scroll to the specified index position.

// Jump to the specified index position without animation.
observerController.jumpTo(index: 1)

// Jump to the specified index position with animation.
observerController.animateTo(
  index: 1,
  duration: const Duration(milliseconds: 250),
  curve: Curves.ease,
);
Boltzmann answered 11/4, 2023 at 10:31 Comment(0)
F
0

Well this same answer https://mcmap.net/q/127624/-flutter-scrolling-to-a-widget-in-listview

But I came up with a solution to fix this problem. this is a trick. If you want to scroll to unbuilt widget. You can use 2 or 3 times Scrollable.ensureVisible

Example:

final _datakey = GlobalKey();
final _datakey2 = GlobalKey();

============================
await Scrollable.ensureVisible(dataKey.currentContext!,
                    curve: Curves.easeIn,
                    duration: const Duration(milliseconds: 250)),
await Future.delayed(const Duration(milliseconds: 200)),
await Scrollable.ensureVisible(dataKey2.currentContext!,
                    curve: Curves.easeOut,
                    duration: const Duration(milliseconds: 250)),

===============

SingleChildScrollView(
  child: Column(
    children: [
      Container(
        key: dataKey,
        width: 500,
        height: 500,
        color: Colors.yellow,
      ),
      Container(
        width: 500,
        height: 500,
        color: Colors.red,
      ),
      Container( // this is a widget, which u want to scroll
        key: dataKey2,
        width: 500,
        height: 500,
        color: Colors.blue,
      ),
    ],
    
  ),
);
Fruiterer answered 21/6, 2023 at 7:45 Comment(0)
C
0

Studying some of the other approaches I came up to a solution with the use of ListView and trying to get the height of each list item through its context.

First, I wrap each list item with this widget:

class _ListItem extends StatelessWidget {
  const _ListItem(
      {Key? key,
      required this.onItemBuilt,
      required this.index,
      required this.child})
      : super(key: key);

  final int index;
  final Widget child;
  final void Function(double) onItemBuilt;

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (context.mounted && context.size != null) {
        onItemBuilt(context.size!.height);
      }
    });
    return child;
  }
}

addPostFrameCallback is used to obtain item's height after the item is built. Then the item's height is exposed through the onItemBuilt callback.

Second, I get each item's height when ListView builds the items.

ListView(
    children: items.map((e) {
        final index = widget.children.indexOf(e);
        return _ListItem(
            index: index,
            child: e,
            onItemBuilt: (height) => _setItemHeights(index, height));
      }).toList(),
    )

Then I need to store the heights of the items through the method _setItemHeights:

final _itemHeights = <double>[];

void _setItemHeights(int index, double height) {
    // Assure that item's height is not already in _itemHeights list
    if (index >= _itemHeights.length) {
      _itemHeights.add(height);
    }
  }

Third, to scroll to a specific item we need to calculate the offset. Since we have the heights of the items stored to _itemHeights, we call the method:

double _getOffset(int index) {
    double offset = 0;
    for (int i = 0; i < index; i++) {
      if (i < _itemHeights.length) {
        offset = offset + _itemHeights[i];
      }
    }
    return offset;
  }

and then call:

_scrollController.animateTo(_getOffset(index),
            duration: const Duration(milliseconds: 160),
            curve: Curves.linear)

BUT, ListView does not build all the items from the beggining (ListView builts the items that are visible from Viewport and some of the next items and when user scrolls, then ListView builds the following items that are going to be visible from Viewport).

So, when we want to scroll to an item that has not been built yet, then we should wait for the ListView to build all the preceding items. That is achieved by scrolling to the last item of our _itemHeights list in order to trigger ListView to build more items until the desired item is built. To implement that we create the method:

int _tempItemHeightsLength = 0;

Future<void> _animateToItem(int index) async {
    final itemHeightsLength = _itemHeights.length;
    if (_scrollController.hasClients) {
      if (index <= itemHeightsLength) {
        await _scrollController.animateTo(
            _getOffset(index),
            duration: const Duration(milliseconds: 160),
            curve: Curves.linear);
      } else if (_tempItemHeightsLength < itemHeightsLength) {
        _tempItemHeightsLength = itemHeightsLength;
        await _scrollController.animateTo(
            _getOffset(itemHeightsLength - 1),
            duration: const Duration(milliseconds: 160),
            curve: Curves.linear);
        await _animateToItem(index);
      }
    }
  }

Finally, I created a custom controller (it is shown below) to manipulate scrolling with an extra property extraPixels in order to position the item some pixels lower or higher regarding Viewport's top position.

That's it. For me this solution works very well. I also added a property padding for ListView.

The entire code is below:

import 'package:flutter/material.dart';

/// A wrapper around ListView with a controller (CustomListViewController)
/// which features method animateToItem(int index) that scrolls the ListView
/// to the item through its index
class CustomListView extends StatefulWidget {
  const CustomListView(
      {Key? key,
      required this.controller,
      required this.children,
      this.scrollAnimationDuration, this.padding})
      : super(key: key);

  final CustomListViewController controller;
  final List<Widget> children;
  final Duration? scrollAnimationDuration;
  final EdgeInsets? padding;

  @override
  State<CustomListView> createState() => _CustomListViewState();
}

class _CustomListViewState extends State<CustomListView> {
  late final ScrollController _scrollController;

  final _itemHeights = <double>[];

  int _tempItemHeightsLength = 0;

  Future<void> _initialize(int index, double? extraPixels) async {
    assert(index >= 0 && index < widget.children.length,
        'Index of item to animate to is out of list\'s range');
    if (_itemHeights.isNotEmpty) {
      await _animateToItem(index, extraPixels);
    } else {
      WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
        if (_itemHeights.isNotEmpty) {
          _animateToItem(index, extraPixels);
        }
      });
    }
  }

  Future<void> _animateToItem(int index, double? extraPixels) async {
    final itemHeightsLength = _itemHeights.length;
    if (_scrollController.hasClients) {
      if (index <= itemHeightsLength) {
            await _scrollController.animateTo(
            _getOffset(index) - (extraPixels ??= 0),
            duration: widget.scrollAnimationDuration ??
                const Duration(milliseconds: 160),
            curve: Curves.linear);
      } else if (_tempItemHeightsLength < itemHeightsLength &&
          itemHeightsLength > 0) {
        _tempItemHeightsLength = itemHeightsLength;
        await _scrollController.animateTo(_getOffset(itemHeightsLength - 1),
            duration: widget.scrollAnimationDuration ??
                const Duration(milliseconds: 160),
            curve: Curves.linear);
        await _animateToItem(index, extraPixels);
      }
    }
  }

  void _setItemHeights(int index, double height) {
    if (index >= _itemHeights.length) {
      _itemHeights.add(height);
    }
  }

  double _getOffset(int index) {
    double offset = 0;
    for (int i = 0; i < index; i++) {
      if (i < _itemHeights.length) {
        offset = offset + _itemHeights[i];
      }
    }
    return offset + (widget.padding?.top?? 0);
  }

  @override
  void initState() {
    _scrollController = widget.controller.scrollController;
    widget.controller._state = this;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      controller: _scrollController,
      padding: widget.padding,
      children: widget.children.map((e) {
        final index = widget.children.indexOf(e);
        return _ListItem(
            index: index,
            child: e,
            onItemBuilt: (height) => _setItemHeights(index, height));
      }).toList(),
    );
  }
}

/// Wrapper around the items of ListView.
/// The CallBack onItemBuilt exposes the height of the item to the CutomListView,
/// so that the offset of the scroll position can be calculated later on.
class _ListItem extends StatelessWidget {
  const _ListItem(
      {Key? key,
      required this.onItemBuilt,
      required this.index,
      required this.child})
      : super(key: key);

  final int index;
  final Widget child;
  final void Function(double) onItemBuilt;

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (context.mounted && context.size != null) {
        onItemBuilt(context.size!.height);
      }
    });
    return child;
  }
}

/// Controller of CustomListView.
/// It includes a ScrollController that is attached to the ListView and
/// can be used at will (e.g. to add a ScrollBar)
/// and the method animateToItem to scroll the ListView to a specific item.
class CustomListViewController {

  _CustomListViewState? _state;

  /// ScrollController that is attached to ListView
  final scrollController = ScrollController();

  /// Method to scroll ListView to specific item given the item's index.
  /// The item appears first in ListView's Viewport.
  /// With extraPixels, pixels can be added/subtracted in order to position the
  /// item lower or higher in Viewport.
  /// If ListView is built before calling this method, the Future of this
  /// method is returned when ListView completes the scrolling to specific item.
  /// Otherwise this method is scheduled for next Frame,
  /// therefore the Future is returned earlier and it is not bound to the
  /// completion of the scrolling
  Future<void> animateToItem(int index, {double? extraPixels}) async {
    if (_state != null) {
      await _state!._initialize(index, extraPixels);
    } else {
      WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
        _state?._initialize(index, extraPixels);
      });
    }
  }
}

I hope this solution is helpful :)

Clementius answered 10/8, 2023 at 12:33 Comment(0)
L
0

My Solution

class SitePage extends StatelessWidget {

const SitePage({super.key});

@override Widget build(BuildContext context) {

GlobalKey<State<StatefulWidget>> dataKey = new GlobalKey();

return Scaffold(
  backgroundColor: SiteColors.backgroundColor,
  body: SingleChildScrollView(
    child: Column(
      children: [
        HeaderNavigation(dataKey: dataKey),
        HeaderModule(),
        CategoriesWidget(dataKey: dataKey),
        58.verticalSpaceFromWidth,            
      ],
    ),
  ),
);

}

}

Limpkin answered 4/9, 2023 at 19:45 Comment(0)
C
0

you can refer following code to the scroll-specific position in the listview builder that is perfectly working for me.

   ListView.builder(
              controller: controller,
              itemCount: 15,
              itemBuilder: (BuildContext context, int index) {
               return children[index);
              },
            ),
             ElevatedButton(
                    onPressed: () {
    oneScrollController .animateTo(10 * 10, 
       duration: const Duration(milliseconds: 10),
        curve: Curves.bounceIn);                                                              
    },
                    child: Text(
                      "Contact me",);

);

Cyperaceous answered 31/10, 2023 at 13:51 Comment(0)
L
-2

Simply use page view controller. Example:

   var controller = PageController();  
     
    ListView.builder(
      controller: controller,
      itemCount: 15,
      itemBuilder: (BuildContext context, int index) {
       return children[index);
      },
    ),
     ElevatedButton(
            onPressed: () {
              controller.animateToPage(5,   //any index that you want to go
    duration: Duration(milliseconds: 700), curve: Curves.linear);
              },
            child: Text(
              "Contact me",), 

       
Lawrenson answered 5/6, 2021 at 10:5 Comment(1)
PageController can't assign to ScrollControllerPerfectionist
Y
-5

You can use the controller.jumpTo(100) after the loading finish

Yuma answered 16/12, 2018 at 3:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.