Flutter - how to scroll to the bottom of a listview?

Viewed 1751

I use this code to scroll:

WidgetsBinding.instance?.addPostFrameCallback((_) => _scrollToEnd());

_scrollToEnd() method is:

_scrollController.animateTo(
  _scrollController.position.maxScrollExtent,
  duration: const Duration(
    milliseconds: 200,
  ),
  curve: Curves.easeInOut,
);

Imagine this as a normal chat screen. It scrolls to the bare bottom if the messages are in 1 line. But as soon as a message gets to 2+ lines it struggles to scroll to the bare bottom. The more rows of a message the less it scrolls to the bottom.

This is how it looks like when i enter the chat:

But if i scroll down further this is the bottom of the chat:

I noticed there's also a case when:

  1. I enter the chat.
  2. It scrolls down like on the first image.
  3. If i tap anywhere on the screen, it continues to scroll to the bare bottom of the listview like on the second image.

Why does this happen and how do i fix this?

6 Answers

what i did, use a listView and reverse true and in children use the list of map.reversed, i am giving you my code example below.

ListView(
          reverse: true,
          children: controller.listMessageData.reversed
           .map((e) => Container(child: Text(e.title));

Just a workaround but it should work as expected. Use clamping physics in the list view. Add an extra number to the max extent

 _scrollController.animateTo(
 _scrollController.position.maxScrollExtent+300,
 duration: const Duration(
   milliseconds: 200,
  ),
  curve: Curves.easeInOut,
  );

Use ScrollController for that it works smooth and simple to use

ScrollController _scrollController = new ScrollController();

Add controller to LisView like this :

 ListView.builder(controller: _scrollController,)

In your initState to scroll to bottom when navigating to this screen

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

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          curve: Curves.easeOut,
          duration: const Duration(milliseconds: 500),
        );
      }
    });
}

I finally found a solution. None of the answers worked for me, or worked partially, but I appreciate your attempts to answer.

For me all i had to do was put

reverse: true

in the ListView builder and then whenever you return the message you need to return it as:

return messages[messages.length - 1 - index];

This is the most important part.

Use should use the reverse property.

ListView.builder(
  reverse: true,
  itemCount: 10,
  itemBuilder: (_, index) {
    return ListTile(title: Text(index.toString()));
  },
)

i had a similar problem when i had to add scroll to item on its expand. my guess is that you call animate to in the same build as when adding new elements to list, so the maximum scroll extent changes after the render of the element, and you scroll to previous one, or smth similar. what i did was to call scrollExtent few consecutive times with updated position value.

the code goes like this:

  static const int tickCount = 8;
  int i = 0;

  Future<void> _scrollToEnd() async {
    if (mounted && tickCount > i) {
      final ScrollPosition position = Scrollable.of(context).position;

      final double target = position.pixels +
          (position.maxScrollExtent - position.pixels) / (tickCount - i);

      position.moveTo(
        target,
        duration: duration,
        curve: Curves.linear,
      );
      await Future.delayed(duration);

      i++;
      WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToItem());
    }
  }

and on first launch reset i state:

   WidgetsBinding.instance.addPostFrameCallback((_) {
                  i = 0;
                  _scrollToItem();
   });

code is a bit messy but i hope it helps

Related