Rolar programaticamente para o final de um ListView

108

Eu tenho um rolável ListViewonde o número de itens pode mudar dinamicamente. Sempre que um novo item é adicionado ao final da lista, gostaria de rolar programaticamente o ListViewaté o final. (por exemplo, algo como uma lista de mensagens de bate-papo onde novas mensagens podem ser adicionadas no final)

Meu palpite é que eu precisaria criar um ScrollControllerno meu Stateobjeto e passá-lo manualmente para o ListViewconstrutor, para que posteriormente eu pudesse chamar animateTo()/ jumpTo()método no controlador. No entanto, como não posso determinar facilmente o deslocamento máximo de rolagem, parece impossível simplesmente realizar um scrollToEnd()tipo de operação (ao passo que posso facilmente passar 0.0para fazê-lo rolar para a posição inicial).

Existe uma maneira fácil de conseguir isso?

Usar reverse: truenão é uma solução perfeita para mim, porque eu gostaria que os itens fossem alinhados na parte superior quando houver apenas um pequeno número de itens que cabem na ListViewjanela de visualização.

yyoon
fonte

Respostas:

106

Se você usar uma embalagem termoencolhível ListViewcom reverse: true, rolar para 0,0 fará o que você quiser.

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
fonte
1
Isso funcionou. No meu caso específico, tive que usar colunas aninhadas para colocar o ListView em um widget expandido, mas a ideia básica é a mesma.
yyoon,
21
Eu vim em busca de uma solução, só queria mencionar que a outra resposta pode ser a melhor maneira de conseguir isso - stackoverflow.com/questions/44141148/…
Animesh Jain
6
Eu concordo, essa resposta (que também escrevi) definitivamente melhor.
Collin Jackson
1
@Dennis Para que o ListView comece na ordem inversa, ou seja, de baixo para cima.
CopsOnRoad
1
A resposta de @CopsOnRoad parece melhor e essa resposta parece mais um hack do que uma solução.
Roopak A Nelliat
128

Use ScrollController.jumpTo()ou ScrollController.animateTo()método para conseguir isso.

Exemplo:

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

Se você quiser uma rolagem suave, em vez de usar o jumpToacima, use

_controller.animateTo(
  _controller.position.maxScrollExtent,
  duration: Duration(seconds: 1),
  curve: Curves.fastOutSlowIn,
);
CopsOnRoad
fonte
4
Para o Firebase Realtime Database, até agora consegui fazer 250 ms (o que provavelmente é muito longo). Eu me pergunto o quão baixo podemos ir? O problema é que maxScrollExtent precisa esperar que a construção do widget termine com o novo item adicionado. Como traçar o perfil da duração média do retorno à conclusão da construção?
SacWebDeveloper
2
Você acha que isso funcionará corretamente com o uso do streambuilder para buscar dados do Firebase? Também está adicionando mensagem de bate-papo no botão enviar? Além disso, se a conexão com a Internet for lenta, funcionará? qualquer coisa melhor se você puder sugerir. Obrigado.
Jay Mungara
@SacWebDeveloper, qual foi sua abordagem com relação a esse problema mais o firebase?
Idris Stack
@IdrisStack Na verdade, 250ms evidentemente não é longo o suficiente, pois às vezes, o jumpTo acontece antes da atualização ocorrer. Acho que a melhor abordagem seria comparar o comprimento da lista após a atualização e pular para a parte inferior se o comprimento for um maior.
SacWebDeveloper
Obrigado @SacWebDeveloper, sua abordagem funciona, talvez eu possa fazer outra pergunta no contexto do Firestore, isso pode desviar o assunto um pouco. Qn. Por que, quando envio uma mensagem para alguém, a lista não rola automaticamente para o final, há uma maneira de ouvir a mensagem e rolar para o final?
Idris Stack
14

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) é a maneira mais simples.

macaco
fonte
2
Oi Jack, obrigado pela sua resposta, acredito que escrevi o mesmo, então não há nenhuma vantagem em adicionar a mesma solução novamente IMHO.
CopsOnRoad
1
Eu estava usando essa abordagem, mas você precisa esperar alguns ms para fazê-lo porque a tela precisa ser renderizada antes de executar o animateTo (...). Talvez tenhamos uma boa maneira de usar boas práticas para fazer isso sem um hack.
Renan Coelho
1
Essa é a resposta certa. Ele rolará sua lista para baixo se você +100 na linha atual. como listViewScrollController.position.maxScrollExtent + 100;
Rizwan Ansar,
1
Inspirado em @RenanCoelho, envolvi essa linha de código em um Timer com atraso de 100 milissegundos.
ymerdrengene,
5

para obter os resultados perfeitos, combinei as respostas de Colin Jackson e CopsOnRoad da seguinte forma:

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

Tenho tantos problemas ao tentar usar o controlador de rolagem para ir para o final da lista que uso outra abordagem.

Em vez de criar um evento para enviar a lista para o final, mudo minha lógica para usar uma lista reversa.

Assim, cada vez que tenho um novo item, eu simplesmente, insiro no topo da 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))
  ),
),
impressões digitais
fonte
2
O único problema que estou tendo com isso é que todas as barras de rolagem funcionam para trás agora. Quando eu rolo para baixo, eles sobem.
ThinkDigital
2

Não coloque widgetBinding no initstate; em vez disso, você precisa colocá-lo no método que busca seus dados no banco de dados. por exemplo, assim. Se colocado em initstate, o scrollcontrollernão será anexado a nenhum listview.

    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
fonte
-2

você pode usar isto onde 0.09*heighté a altura de uma linha na lista e _controlleré definido assim_controller = ScrollController();

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