Desplazarse programáticamente hasta el final de un ListView

97

Tengo un scroll ListViewdonde el número de elementos puede cambiar dinámicamente. Cada vez que se agrega un nuevo elemento al final de la lista, me gustaría desplazarlo ListViewhasta el final mediante programación. (por ejemplo, algo así como una lista de mensajes de chat donde se pueden agregar nuevos mensajes al final)

Supongo que necesitaría crear un ScrollControlleren mi Stateobjeto y pasarlo manualmente al ListViewconstructor, para luego poder llamar animateTo()/ jumpTo()método en el controlador. Sin embargo, dado que no puedo determinar fácilmente el desplazamiento máximo de desplazamiento, parece imposible simplemente realizar un scrollToEnd()tipo de operación (mientras que puedo pasar fácilmente 0.0para que se desplace a la posición inicial).

¿Existe una manera fácil de lograrlo?

Usar reverse: trueno es una solución perfecta para mí, porque me gustaría que los elementos estuvieran alineados en la parte superior cuando solo hay una pequeña cantidad de elementos que quepan en la ListViewventana gráfica.

yyoon
fuente

Respuestas:

95

Si usa un envoltorio retráctil ListViewcon reverse: true, desplazándolo a 0.0 hará lo que quiera.

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),
          );
        }
      ),
    );
  }
}
Collin Jackson
fuente
1
Esto hizo el truco. En mi caso específico, tuve que usar columnas anidadas para poner ListView en un widget expandido, pero la idea básica es la misma.
yyoon
19
Vine buscando una solución, solo quería mencionar que la otra respuesta puede ser una mejor manera de lograr esto - stackoverflow.com/questions/44141148/…
Animesh Jain
6
Estoy de acuerdo, esa respuesta (que también escribí) definitivamente mejor.
Collin Jackson
1
@Dennis Para que ListView comience en orden inverso, es decir, desde abajo.
CopsOnRoad
1
La respuesta de @CopsOnRoad se ve mejor y esta respuesta parece más un truco que una solución.
Roopak A Nelliat
118

Utilice ScrollController.jumpTo()o ScrollController.animateTo()método para lograrlo.

Ejemplo:

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

Si desea un desplazamiento suave, en lugar de usar el jumpToanterior, use

_controller.animateTo(
  _controller.position.maxScrollExtent,
  duration: Duration(seconds: 1),
  curve: Curves.fastOutSlowIn,
);
CopsOnRoad
fuente
4
Para Firebase Realtime Database, hasta ahora he podido salir con 250ms (que probablemente sea muy largo). Me pregunto qué tan bajo podemos llegar. El problema es que maxScrollExtent necesita esperar a que la compilación del widget termine con el nuevo elemento agregado. ¿Cómo perfilar la duración promedio desde la devolución de llamada hasta la finalización de la construcción?
SacWebDeveloper
2
¿Crees que esto funcionará correctamente con el uso de streambuilder para obtener datos de firebase? ¿También agregar un mensaje de chat en el botón enviar? Además, si la conexión a Internet es lenta, ¿funcionará? algo mejor si puedes sugerir. Gracias.
Jay Mungara
@SacWebDeveloper ¿cuál fue su enfoque con este problema más firebase?
Idris Stack
@IdrisStack En realidad, 250ms evidentemente no es lo suficientemente largo ya que a veces, el jumpTo ocurre antes de que ocurra la actualización. Creo que el mejor enfoque sería comparar la longitud de la lista después de la actualización y saltar al final si la longitud es una mayor.
SacWebDeveloper
Gracias @SacWebDeveloper, su enfoque funciona, tal vez pueda hacerle otra pregunta en el contexto de Firestore, podría desviar un poco el tema. Qn. ¿Por qué cuando le envío un mensaje a alguien, la lista no se desplaza automáticamente al final? ¿Hay alguna forma de escuchar el mensaje y desplazarse al final?
Idris Stack
13

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) es la forma más sencilla.

Jack
fuente
1
Hola Jack, gracias por tu respuesta, creo que escribí lo mismo, así que no hay ninguna ventaja en agregar la misma solución nuevamente en mi humilde opinión.
CopsOnRoad
1
Estaba usando este enfoque, pero debe esperar algunos ms para hacerlo porque la pantalla debe procesarse antes de ejecutar animateTo (...). Tal vez tengamos una buena manera de usar buenas prácticas para hacerlo sin un hack.
Renan Coelho
1
Esa es la respuesta correcta. Desplazará su lista al final si tiene +100 en la línea actual. como listViewScrollController.position.maxScrollExtent + 100;
Rizwan Ansar
1
Con la inspiración de @RenanCoelho, envolví esta línea de código en un temporizador con un retraso de 100 milisegundos.
ymerdrengene
5

Para obtener los resultados perfectos, combiné las respuestas de Colin Jackson y CopsOnRoad de la siguiente manera:

_scrollController.animateTo(
                              _scrollController.position.maxScrollExtent,
                              curve: Curves.easeOut,
                              duration: const Duration(milliseconds: 500),
                            );
arjava
fuente
2

Tengo tantos problemas al intentar usar el controlador de desplazamiento para ir al final de la lista que utilizo otro enfoque.

En lugar de crear un evento para enviar la lista al final, cambio mi lógica para usar una lista invertida.

Entonces, cada vez que tengo un artículo nuevo, simplemente lo hago al insertar en la parte superior de la lista.

// 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))
  ),
),
huellas dactilares
fuente
2
El único problema que tengo con esto es que las barras de desplazamiento ahora funcionan al revés. Cuando me desplazo hacia abajo, se mueven hacia arriba.
ThinkDigital
1

No coloque widgetBinding en initstate, en su lugar, debe colocarlo en el método que obtiene sus datos de la base de datos. por ejemplo, así. Si se pone en initstate, scrollcontrollerno se adjuntará a ninguna vista de lista.

    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');
    }
   }
Firzan
fuente
-2

podría usar esto donde 0.09*heightestá la altura de una fila en la lista y _controllerse define así_controller = ScrollController();

(BuildContext context, int pos) {
    if(pos != 0) {
        _controller.animateTo(0.09 * height * (pos - 1), 
                              curve: Curves.easeInOut,
                              duration: Duration(milliseconds: 1400));
    }
}
redaDk
fuente