Программная прокрутка до конца ListView

98

У меня есть прокручиваемый, ListViewгде количество элементов может меняться динамически. Всякий раз, когда в конец списка добавляется новый элемент, я хотел бы программно прокрутить его ListViewдо конца. (например, что-то вроде списка сообщений чата, в который можно добавлять новые сообщения в конце)

Я предполагаю, что мне нужно будет создать ScrollControllerв моем Stateобъекте и передать его вручную ListViewконструктору, чтобы позже я мог вызвать animateTo()/ jumpTo()метод на контроллере. Однако, поскольку я не могу легко определить максимальное смещение прокрутки, кажется невозможным просто выполнить какой-либо scrollToEnd()тип операции (тогда как я могу легко перейти, 0.0чтобы заставить его прокрутить до начальной позиции).

Есть ли простой способ добиться этого?

Использование reverse: true- не идеальное решение для меня, потому что я хотел бы, чтобы элементы были выровнены вверху, когда есть только небольшое количество элементов, которые помещаются в область ListViewпросмотра.

юн
источник

Ответы:

96

Если вы используете упаковали ListViewс reverse: true, скроллинг его в 0.0 будет делать то , что вы хотите.

import 'dart:collection';

import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Example',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<Widget> _messages = <Widget>[new Text('hello'), new Text('world')];
  ScrollController _scrollController = new ScrollController();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Center(
        child: new Container(
          decoration: new BoxDecoration(backgroundColor: Colors.blueGrey.shade100),
          width: 100.0,
          height: 100.0,
          child: new Column(
            children: [
              new Flexible(
                child: new ListView(
                  controller: _scrollController,
                  reverse: true,
                  shrinkWrap: true,
                  children: new UnmodifiableListView(_messages),
                ),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        child: new Icon(Icons.add),
        onPressed: () {
          setState(() {
            _messages.insert(0, new Text("message ${_messages.length}"));
          });
          _scrollController.animateTo(
            0.0,
            curve: Curves.easeOut,
            duration: const Duration(milliseconds: 300),
          );
        }
      ),
    );
  }
}
Коллин Джексон
источник
1
Это сработало. В моем конкретном случае мне пришлось использовать вложенные столбцы, чтобы поместить ListView в расширенный виджет, но основная идея та же.
yyoon
19
Я пришел в поисках решения, просто хотел упомянуть, что другой ответ может быть лучшим способом добиться этого - stackoverflow.com/questions/44141148/…
Animesh Jain
6
Согласен, этот ответ (который я тоже написал) определенно лучше.
Коллин Джексон
1
@Dennis Чтобы ListView запускался в обратном порядке снизу.
CopsOnRoad
1
Ответ от @CopsOnRoad выглядит лучше, и этот ответ больше похож на взлом, чем на решение.
Roopak A Nelliat
118

Используйте ScrollController.jumpTo()или ScrollController.animateTo()метод для достижения этого.

Пример:

final _controller = ScrollController();

@override
Widget build(BuildContext context) {
  
  // After 1 second, it takes you to the bottom of the ListView
  Timer(
    Duration(seconds: 1),
    () => _controller.jumpTo(_controller.position.maxScrollExtent),
  );

  return ListView.builder(
    controller: _controller,
    itemCount: 50,
    itemBuilder: (_, __) => ListTile(title: Text('ListTile')),
  );
}

Если вам нужна плавная прокрутка, вместо использования jumpToвыше используйте

_controller.animateTo(
  _controller.position.maxScrollExtent,
  duration: Duration(seconds: 1),
  curve: Curves.fastOutSlowIn,
);
CopsOnRoad
источник
4
Для базы данных Firebase Realtime мне до сих пор удавалось обойтись 250 мс (что, вероятно, очень долго). Интересно, как низко мы можем спуститься? Проблема в том, что maxScrollExtent необходимо дождаться завершения сборки виджета с добавлением нового элемента. Как профилировать среднюю продолжительность от обратного вызова до завершения сборки?
SacWebDeveloper
2
Как вы думаете, это будет правильно работать с использованием streambuilder для извлечения данных из firebase? Также добавляете сообщение чата на кнопку отправки? Также, если интернет-соединение медленное, будет ли оно работать? что-нибудь лучше, если вы можете предложить. Спасибо.
Джей Мунгара
@SacWebDeveloper, каков был ваш подход к этой проблеме плюс firebase?
Идрис Стэк
@IdrisStack На самом деле 250 мс явно недостаточно, так как иногда jumpTo происходит до обновления. Я думаю, что лучше было бы сравнить длину списка после обновления и перейти вниз, если длина на единицу больше.
SacWebDeveloper
Спасибо @SacWebDeveloper, ваш подход работает, может быть, я могу задать вам еще один вопрос в контексте Firestore, это может отвлечь тему abit. Qn. Почему, когда я отправляю кому-то сообщение, список не прокручивается автоматически вниз, есть ли способ прослушать сообщение и прокрутить вниз?
Идрис Стэк
13

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) это самый простой способ.

Джек
источник
1
Привет, Джек, спасибо за ваш ответ, я считаю, что написал то же самое, поэтому нет никакого преимущества в добавлении того же решения снова ИМХО.
CopsOnRoad
1
Я использовал этот подход, но вам нужно подождать несколько мс, потому что экран необходимо отрендерить перед запуском animateTo (...). Может быть, у нас есть хороший способ, используя передовой опыт, сделать это без взлома.
Ренан Коэльо
1
Это правильный ответ. Он прокрутит ваш список вниз, если вы поставите +100 в текущей строке. как listViewScrollController.position.maxScrollExtent + 100;
Ризван Ансар,
1
Вдохновленный @RenanCoelho, я заключил эту строку кода в таймер с задержкой в ​​100 миллисекунд.
ymerdrengene
5

Чтобы получить идеальные результаты, я объединил ответы Колина Джексона и CopsOnRoad следующим образом:

_scrollController.animateTo(
                              _scrollController.position.maxScrollExtent,
                              curve: Curves.easeOut,
                              duration: const Duration(milliseconds: 500),
                            );
арджава
источник
2

У меня так много проблем с попыткой использовать контроллер прокрутки для перехода в конец списка, что я использую другой подход.

Вместо создания события для отправки списка в конец я меняю свою логику на использование перевернутого списка.

Итак, каждый раз, когда у меня появляется новый элемент, я просто вставляю его в начало списка.

// add new message at the begin of the list 
list.insert(0, message);
// ...

// pull items from the database
list = await bean.getAllReversed(); // basically a method that applies a descendent order

// I remove the scroll controller
new Flexible(
  child: new ListView.builder(
    reverse: true, 
    key: new Key(model.count().toString()),
    itemCount: model.count(),
    itemBuilder: (context, i) => ChatItem.displayMessage(model.getItem(i))
  ),
),
отпечатки пальцев
источник
2
Единственная проблема, с которой я столкнулся, заключается в том, что теперь все полосы прокрутки работают в обратном направлении. Когда я прокручиваю вниз, они движутся вверх.
ThinkDigital
1

Не помещайте widgetBinding в initstate, вместо этого вам нужно поместить его в метод, который извлекает ваши данные из базы данных. например, вот так. Если поместить в initstate, scrollcontrollerне будет прикрепляться ни к какому списку.

    Future<List<Message>> fetchMessage() async {

    var res = await Api().getData("message");
    var body = json.decode(res.body);
    if (res.statusCode == 200) {
      List<Message> messages = [];
      var count=0;
      for (var u in body) {
        count++;
        Message message = Message.fromJson(u);
        messages.add(message);
      }
      WidgetsBinding.instance
          .addPostFrameCallback((_){
        if (_scrollController.hasClients) {
          _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
        }
      });
      return messages;
    } else {
      throw Exception('Failed to load album');
    }
   }
Фирзан
источник
-2

вы можете использовать это, где 0.09*heightэто высота строки в списке и _controllerопределяется следующим образом_controller = ScrollController();

(BuildContext context, int pos) {
    if(pos != 0) {
        _controller.animateTo(0.09 * height * (pos - 1), 
                              curve: Curves.easeInOut,
                              duration: Duration(milliseconds: 1400));
    }
}
redaDk
источник