7

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 does not scroll all the way to the 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?

David Soroko
  • 8,521
  • 2
  • 39
  • 51
busuu
  • 586
  • 9
  • 22

7 Answers7

11

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));
Alwayss Bijoy
  • 778
  • 9
  • 15
9

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,
  );
Kaushik Chandru
  • 15,510
  • 2
  • 12
  • 30
  • This is the closest answer so far. It worked and now it scrolls to the bare bottom. However I noticed that there's no "drag beyond bottom" effect on ios, because of the clamping physics. Is it possible to preserve this effect? – busuu Nov 02 '21 at 11:27
  • You can change the physics to bouncephysics or a normal scrollphysics but you may see an extra bounce to the end and then it will snap in its place which may look a bit odd – Kaushik Chandru Nov 02 '21 at 11:39
  • Yes I noticed there's a bounce effect now. Is there a way to fix this too? – busuu Nov 02 '21 at 12:02
  • Try approximate value to add. For example if the last message is 4 lined. Add a value as 120 (30x4). That way you can use any physics of your choice and it wouldn't bounce. – Kaushik Chandru Nov 04 '21 at 11:22
8

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.

busuu
  • 586
  • 9
  • 22
4

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),
        );
      }
    });
}
Faizan Darwesh
  • 301
  • 1
  • 5
3

Use should use the reverse property.

ListView.builder(
  reverse: true,
  itemCount: 10,
  itemBuilder: (_, index) {
    return ListTile(title: Text(index.toString()));
  },
)
Marius Atasiei
  • 275
  • 1
  • 10
2

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

A. Tratseuski
  • 661
  • 6
  • 9
1

Here is a solution that will scroll down to the end all the time without adding any extra pixels by hand. The only downside is that you can only use Curves.linear as animation.

    Future.doWhile(() {
      if (scrollController.position.extentAfter == 0)
        return Future.value(false);
      return scrollController
          .animateTo(scrollController.position.maxScrollExtent,
              duration: Duration(milliseconds: 100), curve: Curves.linear)
          .then((value) => true);
    });
Taras Mazepa
  • 604
  • 8
  • 24