Programmgesteuertes Scrollen zum Ende einer ListView

98

Ich habe eine Bildlaufleiste, ListViewin der sich die Anzahl der Elemente dynamisch ändern kann. Immer wenn ein neues Element am Ende der Liste hinzugefügt wird, möchte ich das programmgesteuert ListViewbis zum Ende scrollen . (zB so etwas wie eine Chat-Nachrichtenliste, in der am Ende neue Nachrichten hinzugefügt werden können)

Ich vermute, dass ich ScrollControllerin meinem StateObjekt ein erstellen und es manuell an den ListViewKonstruktor übergeben müsste , damit ich später animateTo()/ jumpTo()method auf dem Controller aufrufen kann . Da ich jedoch den maximalen Bildlaufversatz nicht einfach bestimmen kann, scheint es unmöglich, einfach eine scrollToEnd()Art von Operation auszuführen (während ich leicht passieren kann 0.0, um ihn zur Ausgangsposition zu scrollen).

Gibt es einen einfachen Weg, dies zu erreichen?

Die Verwendung reverse: trueist für mich keine perfekte Lösung, da ich möchte, dass die Elemente oben ausgerichtet werden, wenn nur wenige Elemente in das ListViewAnsichtsfenster passen .

yyoon
quelle

Antworten:

96

Wenn Sie einen eingeschweißten ListViewmit verwenden reverse: true, wird durch Scrollen auf 0.0 das erreicht, was Sie möchten.

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
quelle
1
Das hat den Trick gemacht. In meinem speziellen Fall musste ich verschachtelte Spalten verwenden, um die ListView in ein erweitertes Widget einzufügen, aber die Grundidee ist dieselbe.
JJY
19
Ich war auf der Suche nach einer Lösung und wollte nur erwähnen, dass die andere Antwort ein besserer Weg ist, dies zu erreichen - stackoverflow.com/questions/44141148/…
Animesh Jain
6
Ich stimme zu, diese Antwort (die ich auch geschrieben habe) definitiv besser.
Collin Jackson
1
@Dennis Damit die ListView in umgekehrter Reihenfolge von unten beginnt.
CopsOnRoad
1
Die Antwort von @CopsOnRoad sieht besser aus und diese Antwort ähnelt eher einem Hack als einer Lösung.
Roopak A Nelliat
118

Verwenden ScrollController.jumpTo()oder ScrollController.animateTo()Methode, um dies zu erreichen.

Beispiel:

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

Wenn Sie einen reibungslosen Bildlauf wünschen, verwenden Sie anstelle der jumpTooben genannten Verwendung

_controller.animateTo(
  _controller.position.maxScrollExtent,
  duration: Duration(seconds: 1),
  curve: Curves.fastOutSlowIn,
);
CopsOnRoad
quelle
4
Für die Firebase-Echtzeitdatenbank konnte ich bisher mit 250 ms durchkommen (was wahrscheinlich sehr lang ist). Ich frage mich, wie tief wir gehen können? Das Problem ist, dass maxScrollExtent warten muss, bis der Widget-Build mit dem neuen hinzugefügten Element abgeschlossen ist. Wie kann die durchschnittliche Dauer vom Rückruf bis zur Fertigstellung des Builds ermittelt werden?
SacWebDeveloper
2
Denken Sie, dass dies mit der Verwendung von Streambuilder zum Abrufen von Daten aus Firebase ordnungsgemäß funktioniert? Auch Chat-Nachricht über die Schaltfläche "Senden" hinzufügen? Auch wenn die Internetverbindung langsam ist, funktioniert sie? etwas besseres, wenn Sie vorschlagen können. Vielen Dank.
Jay Mungara
@SacWebDeveloper Was war Ihr Ansatz mit diesem Problem plus Firebase?
Idris Stack
@IdrisStack Tatsächlich sind 250 ms offensichtlich nicht lang genug, da manchmal der Sprung nach vor dem Update erfolgt. Ich denke, der bessere Ansatz wäre, die Listenlänge nach dem Update zu vergleichen und nach unten zu springen, wenn die Länge eins größer ist.
SacWebDeveloper
Vielen Dank an @SacWebDeveloper, Ihr Ansatz funktioniert. Vielleicht kann ich Ihnen im Zusammenhang mit Firestore eine andere Frage stellen, die das Thema möglicherweise ablenkt. Qn. Warum gibt es eine Möglichkeit, die Nachricht anzuhören und nach unten zu scrollen, wenn ich eine Nachricht an jemanden sende, dessen Liste nicht automatisch nach unten scrollt?
Idris Stack
13

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) ist der einfachste Weg.

Jack
quelle
1
Hallo Jack, danke für deine Antwort, ich glaube, ich habe dasselbe geschrieben, daher gibt es keinen Vorteil, IMHO dieselbe Lösung erneut hinzuzufügen.
CopsOnRoad
1
Ich habe diesen Ansatz verwendet, aber Sie müssen einige ms warten, um dies zu tun, da der Bildschirm gerendert werden muss, bevor Sie animateTo (...) ausführen. Vielleicht haben wir eine gute Möglichkeit, gute Praktiken anzuwenden, um dies ohne Hack zu tun.
Renan Coelho
1
Das ist die richtige Antwort. Es wird Ihre Liste nach unten scrollen, wenn Sie +100 in der aktuellen Zeile. wie listViewScrollController.position.maxScrollExtent + 100;
Rizwan Ansar
1
Mit der Inspiration von @RenanCoelho habe ich diese Codezeile mit einer Verzögerung von 100 Millisekunden in einen Timer eingeschlossen.
Ymerdrengene
5

Um die perfekten Ergebnisse zu erzielen, habe ich die Antworten von Colin Jackson und CopsOnRoad wie folgt kombiniert:

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

Ich habe so viele Probleme beim Versuch, mit dem Scroll-Controller zum Ende der Liste zu gelangen, dass ich einen anderen Ansatz verwende.

Anstatt ein Ereignis zu erstellen, um die Liste nach unten zu senden, ändere ich meine Logik, um eine umgekehrte Liste zu verwenden.

Jedes Mal, wenn ich einen neuen Artikel habe, habe ich ihn einfach beim Einfügen oben in der Liste erstellt.

// 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))
  ),
),
Fingerabdrücke
quelle
2
Das einzige Problem, das ich damit habe, ist, dass alle Bildlaufleisten jetzt rückwärts funktionieren. Wenn ich nach unten scrolle, bewegen sie sich nach oben.
ThinkDigital
1

Fügen Sie die WidgetBinding nicht in den Init-Status ein, sondern in die Methode, mit der Ihre Daten aus der Datenbank abgerufen werden. zum Beispiel so. Wenn in initstate gesetzt, scrollcontrollerwird das nicht an eine Listenansicht angehängt.

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

Sie können dies verwenden, wenn 0.09*heightdie Höhe einer Zeile in der Liste ist und _controllerwie folgt definiert ist_controller = ScrollController();

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