Programowe przewijanie do końca ListView

108

Mam przewijaną listę, w ListViewktórej liczba elementów może zmieniać się dynamicznie. Za każdym razem, gdy na końcu listy zostanie dodany nowy element, chciałbym programowo przewinąć ListViewdo końca. (np. coś w rodzaju listy wiadomości na czacie, gdzie na końcu można dodawać nowe wiadomości)

Domyślam się, że musiałbym utworzyć ScrollControllerw moim Stateobiekcie i przekazać go ręcznie do ListViewkonstruktora, aby móc później wywołać animateTo()/ jumpTo()metodę na kontrolerze. Ponieważ jednak nie mogę łatwo określić maksymalnego przesunięcia przewijania, wydaje się niemożliwe wykonanie po prostu scrollToEnd()rodzaju operacji (podczas gdy mogę łatwo przejść, 0.0aby przewinąć do pozycji początkowej).

Czy jest na to łatwy sposób?

Używanie reverse: truenie jest dla mnie idealnym rozwiązaniem, ponieważ chciałbym, aby elementy były wyrównane u góry, gdy jest tylko niewielka liczba elementów, które mieszczą się w ListViewwidoku.

yyoon
źródło

Odpowiedzi:

106

Jeśli używasz ofoliowana ListViewz reverse: true, przewijanie go na 0.0 będzie robić to, co chcesz.

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
źródło
1
To załatwiło sprawę. W moim konkretnym przypadku musiałem użyć zagnieżdżonych kolumn, aby umieścić ListView w widżecie rozszerzonym, ale podstawowa idea jest taka sama.
yyoon
21
Przyszedłem szukać rozwiązania, chciałem tylko wspomnieć, że inna odpowiedź może być lepszym sposobem na osiągnięcie tego - stackoverflow.com/questions/44141148/…
Animesh Jain
6
Zgadzam się, że odpowiedź (którą też napisałem) zdecydowanie lepsza.
Collin Jackson,
1
@Dennis Aby ListView rozpoczynał się w odwrotnej kolejności, czyli od dołu.
CopsOnRoad
1
Odpowiedź od @CopsOnRoad wygląda lepiej, a ta odpowiedź bardziej przypomina hack niż rozwiązanie.
Roopak A Nelliat
128

Użyj ScrollController.jumpTo()lub ScrollController.animateTo()metody, aby to osiągnąć.

Przykład:

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

Jeśli chcesz płynne przewijanie, zamiast używać jumpTopowyższego, użyj

_controller.animateTo(
  _controller.position.maxScrollExtent,
  duration: Duration(seconds: 1),
  curve: Curves.fastOutSlowIn,
);
CopsOnRoad
źródło
4
W przypadku bazy danych czasu rzeczywistego Firebase udało mi się jak dotąd uciec z 250 ms (co prawdopodobnie jest naprawdę długie). Zastanawiam się, jak nisko możemy zejść? Problem polega na tym, że maxScrollExtent musi czekać na zakończenie tworzenia widżetu z dodaniem nowego elementu. Jak profilować średni czas od wywołania zwrotnego do zakończenia kompilacji?
SacWebDeveloper
2
Czy myślisz, że to zadziała poprawnie z użyciem Streambuildera do pobierania danych z Firebase? Dodajesz również wiadomość czatu na przycisku wysyłania? Czy będzie działać, jeśli połączenie internetowe jest wolne? coś lepszego, jeśli możesz zasugerować. Dzięki.
Jay Mungara
@SacWebDeveloper Jakie było Twoje podejście do tego problemu i Firebase?
Idris Stack
@IdrisStack Właściwie 250 ms to ewidentnie za mało, ponieważ czasami skokTo ma miejsce przed wystąpieniem aktualizacji. Myślę, że lepszym podejściem byłoby porównanie długości listy po aktualizacji i skoku na dół, jeśli długość jest o jeden większa.
SacWebDeveloper
Dzięki @SacWebDeveloper, twoje podejście działa, może mogę zadać ci inne pytanie w kontekście Firestore, może to zmienić temat. Qn. Dlaczego jest tak, że kiedy wysyłam wiadomość do kogoś, lista nie przewija się automatycznie w dół, czy istnieje sposób na odsłuchanie wiadomości i przewinięcie do dołu?
Idris Stack
14

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) to najprostszy sposób.

Jacek
źródło
2
Cześć Jack, dzięki za odpowiedź, myślę, że napisałem to samo, więc nie ma żadnej korzyści z dodania tego samego rozwiązania ponownie IMHO.
CopsOnRoad
1
Używałem tego podejścia, ale musisz poczekać, aż zrobi to kilka ms, ponieważ ekran musi zostać wyrenderowany przed uruchomieniem animateTo (...). Może mamy dobry sposób, wykorzystując dobre praktyki, aby zrobić to bez włamania.
Renan Coelho
1
To jest właściwa odpowiedź. Lista przewinie się na dół, jeśli w bieżącej linii +100. jak listViewScrollController.position.maxScrollExtent + 100;
Rizwan Ansar
1
Z inspiracji @RenanCoelho zawinąłem ten wiersz kodu w Timer z opóźnieniem 100 milisekund.
ymerdrengene
5

aby uzyskać doskonałe rezultaty, połączyłem odpowiedzi Colina Jacksona i CopsOnRoad w następujący sposób:

_scrollController.animateTo(
                              _scrollController.position.maxScrollExtent,
                              curve: Curves.easeOut,
                              duration: const Duration(milliseconds: 500),
                            );
arjava
źródło
2

Mam tak duży problem, że próbuję użyć kontrolera przewijania, aby przejść na koniec listy, że używam innego podejścia.

Zamiast tworzyć zdarzenie w celu wysłania listy na sam dół, zmieniam logikę na odwróconą listę.

Tak więc za każdym razem, gdy mam nową pozycję, po prostu wstawiłem ją na górze listy.

// 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))
  ),
),
odciski palców
źródło
2
Jedynym problemem, który mam z tym, jest to, że wszystkie paski przewijania działają teraz wstecz. Kiedy przewijam w dół, przesuwają się w górę.
ThinkDigital
2

Nie umieszczaj widgetBinding w initstate, zamiast tego musisz umieścić go w metodzie, która pobiera dane z bazy danych. na przykład w ten sposób. Jeśli ustawisz initstate, scrollcontrollernie zostanie dołączony do żadnego widoku listy.

    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
źródło
0

Natknąłem się na ten problem, gdy korzystałem z StreamBuilderwidżetu do pobierania danych z mojej bazy danych. Położyłem WidgetsBinding.instance.addPostFrameCallbackna górze buildmetodę widżetu i nie przewijałbym go do końca. Naprawiłem to, robiąc to:

...
StreamBuilder(
  stream: ...,
  builder: (BuildContext context, AsyncSnapshot snapshot) {
    // Like this:
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_controller.hasClients) {
        _controller.jumpTo(_controller.position.maxScrollExtent);
      } else {
        setState(() => null);
      }
     });

     return PutYourListViewHere
}),
...

Też próbowałem, _controller.animateToale nie działało.

Andrej
źródło
-2

możesz użyć tego, gdzie 0.09*heightjest wysokością wiersza na liście i _controllerjest zdefiniowany w ten sposób_controller = ScrollController();

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