181

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(...)
]);
GreenTigerEye
  • 5,917
  • 9
  • 22
  • 33

20 Answers20

235

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.

Syed
  • 15,657
  • 13
  • 120
  • 154
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
  • 7
    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? – Amit Kotlovski Jun 28 '20 at 06:34
  • I'm getting this error `type 'List' is not a subtype of type 'GlobalKey>' of 'function result'`. How should I fix it? – Nehal Jaisalmeria Nov 08 '20 at 20:48
  • 1
    `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. – Sarp Başaraner Dec 29 '20 at 08:36
  • 14
    If someone needs to scroll after build then `WidgetsBinding.instance.addPostFrameCallback((_) => Scrollable.ensureVisible(dataKey.currentContext))` could be used. – ych Jan 05 '21 at 17:41
  • 1
    @SarpBaşaraner they might be but this issue actually helped me with SingleChildScrollView so it is a very useful answer! – cs guy Jan 12 '21 at 19:59
  • @Remi, i think you have best answer of it. Please help me with a solution. https://stackoverflow.com/questions/69795503/flutter-nested-list-scroll-parent-when-reach-to-end-start-of-inner-list – Jamshed Alam Nov 08 '21 at 02:17
  • Null safety: Scrollable.ensureVisible( widgetKey.currentContext ?? context, curve: Curves.ease, duration: const Duration(seconds: 1), ); – Aziz Feb 01 '22 at 17:12
  • 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. – szotp Sep 23 '22 at 13:28
  • 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? – zionpi Oct 27 '22 at 07:26
  • 1
    Plz make a full code example of the "more work" required to make `Scrollable.ensureVisible` work with `ListView`! – Karolina Hagegård Nov 07 '22 at 14:43
  • 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. – spydon Jan 04 '23 at 14:24
86

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

Tom
  • 4,070
  • 4
  • 22
  • 50
TWL
  • 6,228
  • 29
  • 65
  • 1
    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. – Paul Kitatta Feb 18 '20 at 23:36
  • 3
    Nice answer. However, `ScrollablePositionedList` doesn't have shrinkWrap property and can not be used with Slivers. – Teh Sunn Liu Feb 20 '21 at 12:48
  • @TehSunnLiu, have you found a way to use it with slivers? – Daniel Jun 15 '21 at 02:38
  • 1
    Hi @Daniel, As mentioned in this (https://stackoverflow.com/a/49154882/5361492) 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. – Teh Sunn Liu Jun 22 '21 at 06:20
  • This package ([scrollable_positioned_list](https://pub.dev/packages/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](https://pub.dev/packages/scroll_to_index). – Aleksandar Oct 20 '21 at 09:20
  • 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`. – Matthew Trent Aug 17 '22 at 23:03
  • @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? – Dennis Ashford Aug 21 '22 at 15:40
  • 1
    @DennisAshford, I ended up going with `scrollable_positioned_list`, just because of hope in the future (before my product launches), it gets a`ScrollController` (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] – Matthew Trent Aug 22 '22 at 00:25
  • 1
    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. – Matthew Trent Aug 22 '22 at 00:29
  • can I pass index parameter to this itemScrollController ? How to ? – Noor Hossain Sep 28 '22 at 13:20
  • 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. – Omar Sharaki Jan 03 '23 at 10:36
54

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')),
            ),
          );
        },
      ),
    );
  }
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • 7
    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. – Paul Kitatta Feb 18 '20 at 23:42
  • 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 itemBuilder – Tiago Santos Jul 16 '20 at 11:26
  • @CopsOnRoad is there any way to find index when scrolling? I mean scroll up and down find top index? – BIS Tech Aug 17 '20 at 09:00
  • I have main ListView(vertical) and category ListView(horizonal). same like this https://www.youtube.com/watch?v=LrOR5QOCHBI I am trying to implement this kind of two listView. – BIS Tech Aug 17 '20 at 11:15
  • 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 https://pub.dev/packages/scroll_to_index). So, is there any way to find out current index? – BIS Tech Aug 17 '20 at 11:18
  • Or can you suggest to implement this? https://www.youtube.com/watch?v=LrOR5QOCHBI – BIS Tech Aug 17 '20 at 11:19
  • @BloodLoss Let me get to my system later today, I'll take a look at it (currently I am on phone). – CopsOnRoad Aug 17 '20 at 11:36
  • Thanks you so much for helping to me – BIS Tech Aug 17 '20 at 12:39
  • @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`. – CopsOnRoad Aug 18 '20 at 17:15
  • @CopsOnRoad thanks for try to support.. I implemented this feature same like above video – BIS Tech Aug 18 '20 at 17:29
  • 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. – idish Jun 30 '22 at 13:03
30

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

iDecode
  • 22,623
  • 19
  • 99
  • 186
Nhật Trần
  • 2,522
  • 1
  • 20
  • 20
  • @AXE, any way of making the app bar reacting to the scroll? I'm trying to make this: https://twitter.com/maurodibert/status/1366341648343568389 Thanks! – Mau Di Bert Mar 01 '21 at 11:26
  • @AXE here could be the solution: https://titanwolf.org/Network/Articles/Article?AID=f85aaeb2-8233-45d0-899a-25464e35fba5#gsc.tab=0 – Mau Di Bert Mar 01 '21 at 11:45
  • @AXE sorry, but It shows how to achieve the same but no synced. – Mau Di Bert Mar 01 '21 at 12:44
  • 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. – Lalit Fauzdar Aug 14 '21 at 21:31
27

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);
BIS Tech
  • 17,000
  • 12
  • 99
  • 148
  • 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. – beauchette Apr 22 '22 at 08:41
  • 1
    I think this is the best solution. – Onur Kağan Aldemir Apr 29 '22 at 13:12
  • 1
    Best answer for me, at least. Thank you, friend! – Raphael Souza Oct 06 '22 at 17:50
  • 3
    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. – Abdullah Qasemi Mar 21 '23 at 20:17
  • 1
    `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. https://api.flutter.dev/flutter/widgets/ListView/ListView.builder.html – omega_mi May 30 '23 at 18:26
22

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

nathanfranke
  • 775
  • 1
  • 9
  • 19
  • That is a really cool makeshift way to scroll through listviews with similar or almost similar widgets. – TheCoder May 04 '22 at 13:27
11

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"),
          ),
        ],
      ),
    );
  }
}
Tom Stein
  • 345
  • 2
  • 15
Hemanth Raj
  • 32,555
  • 10
  • 92
  • 82
  • 34
    Too complicated. Especially for unknows sized elements. – Rémi Rousselet Mar 07 '18 at 13:49
  • 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 @Darky – Hemanth Raj Mar 07 '18 at 13:52
  • 9
    Anybody found a way of doing this for items with different heights? This is a total showstopper for flutter :( – Christine Aug 01 '18 at 04:21
  • 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. – Hemanth Raj Feb 26 '19 at 12:47
  • Thanks, @HemanthRaj this was simple and clear worked for me – irzum shahid Jul 27 '20 at 06:42
  • @HemanthRaj not working for `scrollDirection: Axis.horizontal` listView – BIS Tech Aug 05 '21 at 10:04
8

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

Michał Dobi Dobrzański
  • 1,449
  • 1
  • 20
  • 19
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),
    );

}

}
anakin.jin
  • 61
  • 1
  • 3
  • Hi. Is it possible to add some explanation, like what does the code do? or a screenshot? It will be of great help. Thanks. – Teh Sunn Liu Feb 20 '21 at 12:53
  • @anakin.jin what to initialize `mounted` with? – Saugat Thapa Mar 18 '21 at 04:44
  • @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()`. – Chen Li Yong May 17 '21 at 01:52
  • 1
    this is not perfect .. this is the worst solution here – Mohammed Hamdan May 31 '23 at 18:33
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);
  }
}
alireza easazade
  • 3,324
  • 4
  • 27
  • 35
Jitesh Mohite
  • 31,138
  • 12
  • 157
  • 147
  • 1
    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. – pgzm29 Aug 25 '20 at 23:29
  • How to achieve the same thing without a floating button? Like when a user swipes left and right? – Md. Kamrul Amin Dec 01 '21 at 17:47
  • @KamrulHasanJony: Try Listview in horizontal, this should work – Jitesh Mohite Dec 02 '21 at 13:35
  • 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. – Md. Kamrul Amin Dec 08 '21 at 04:29
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);
    }
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),
      ),
    ],
  ),
);
 }
 }
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.

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));
  });
Saikat halder
  • 548
  • 4
  • 11
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.

F Perroch
  • 1,988
  • 2
  • 11
  • 23
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,
);
LinXunFeng
  • 41
  • 3
0

Well this same answer https://stackoverflow.com/a/49154882/8422048

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,
      ),
    ],
    
  ),
);
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 :)

-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",), 

       
Vega
  • 27,856
  • 27
  • 95
  • 103
-5

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

Maddy Leo
  • 1
  • 1