Menggulir secara terprogram ke akhir ListView

98

Saya memiliki scrollable di ListViewmana jumlah item dapat berubah secara dinamis. Setiap kali item baru ditambahkan ke akhir daftar, saya ingin menggulir secara terprogram ListViewke akhir. (misalnya, sesuatu seperti daftar pesan obrolan di mana pesan baru dapat ditambahkan di akhir)

Dugaan saya adalah bahwa saya perlu membuat ScrollControllerdi Stateobjek saya dan meneruskannya secara manual ke ListViewkonstruktor, jadi saya nanti dapat memanggil animateTo()/ jumpTo()metode pada pengontrol. Namun, karena saya tidak dapat dengan mudah menentukan offset gulir maksimum, tampaknya tidak mungkin untuk hanya melakukan suatu scrollToEnd()jenis operasi (sedangkan saya dapat dengan mudah meneruskan 0.0untuk membuatnya menggulir ke posisi awal).

Adakah cara mudah untuk mencapai ini?

Menggunakan reverse: truebukanlah solusi yang tepat bagi saya, karena saya ingin item disejajarkan di atas saat hanya ada sejumlah kecil item yang muat dalam ListViewviewport.

yyoon
sumber

Jawaban:

96

Jika Anda menggunakan shrink-dibungkus ListViewdengan reverse: true, menggulirnya ke 0,0 akan melakukan apa yang Anda inginkan.

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
sumber
1
Ini berhasil. Dalam kasus khusus saya, saya harus menggunakan Kolom bersarang untuk meletakkan ListView dalam widget yang Diperluas, tetapi ide dasarnya sama.
yyoon
19
Saya datang mencari solusi, hanya ingin menyebutkan bahwa jawaban lain mungkin cara yang lebih baik untuk mencapai ini - stackoverflow.com/questions/44141148/…
Animesh Jain
6
Saya setuju, jawaban itu (yang juga saya tulis) pasti lebih baik.
Collin Jackson
1
@ Dennis Sehingga ListView dimulai dalam urutan terbalik yaitu dari bawah.
CopsOnRoad
1
Jawaban dari @CopsOnRoad terlihat lebih baik dan jawaban ini lebih terlihat seperti peretasan daripada solusi.
Roopak A Nelliat
118

Gunakan ScrollController.jumpTo()atau ScrollController.animateTo()metode untuk mencapai ini.

Contoh:

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

Jika Anda ingin pengguliran halus, alih-alih menggunakan jumpTopenggunaan di atas

_controller.animateTo(
  _controller.position.maxScrollExtent,
  duration: Duration(seconds: 1),
  curve: Curves.fastOutSlowIn,
);
CopsOnRoad
sumber
4
Untuk Firebase Realtime Database, sejauh ini saya bisa mendapatkan 250ms (yang mungkin sangat lama). Aku bertanya-tanya seberapa rendah kita bisa pergi? Masalahnya adalah maxScrollExtent perlu menunggu hingga pembuatan widget selesai dengan item baru ditambahkan. Bagaimana cara membuat profil durasi rata-rata dari callback hingga penyelesaian build?
SacWebDeveloper
2
Apakah menurut Anda ini akan berfungsi dengan baik dengan menggunakan streambuilder untuk mengambil data dari firebase? Juga menambahkan pesan obrolan pada tombol kirim juga? Juga jika koneksi internet lambat apakah akan berhasil? ada yang lebih baik jika Anda bisa menyarankan. Terima kasih.
Jay Mungara
@SacWebDeveloper, apa pendekatan Anda dengan masalah ini ditambah firebase?
Idris Stack
@IdrisStack Sebenarnya, 250ms ternyata tidak cukup lama karena kadang-kadang jumpTo terjadi sebelum update terjadi. Saya pikir pendekatan yang lebih baik adalah membandingkan panjang daftar setelah pembaruan dan jumpTo ke bawah jika panjangnya lebih besar.
SacWebDeveloper
Terima kasih @SacWebDeveloper, pendekatan Anda berhasil, mungkin saya dapat mengajukan pertanyaan lain dalam konteks Firestore, ini mungkin mengalihkan topik sedikit. Qn. Mengapa ketika saya mengirim pesan ke seseorang daftar tidak secara otomatis bergulir ke bawah, apakah ada cara untuk mendengarkan pesan dan menggulir ke bawah?
Idris Stack
13

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) adalah cara paling sederhana.

mendongkrak
sumber
1
Hai Jack, terima kasih atas jawaban Anda, saya yakin saya menulis hal yang sama, jadi tidak ada keuntungan menambahkan solusi yang sama lagi IMHO.
CopsOnRoad
1
Saya menggunakan pendekatan ini tetapi Anda perlu menunggu beberapa ms untuk melakukannya karena layar perlu dirender sebelum menjalankan animateTo (...). Mungkin kami memiliki cara yang bagus menggunakan praktik yang baik untuk melakukannya tanpa peretasan.
Renan Coelho
1
Itu jawaban yang benar. Ini akan menggulir daftar Anda ke bawah jika Anda +100 di baris saat ini. seperti listViewScrollController.position.maxScrollExtent + 100;
Rizwan Ansar
1
Dengan inspirasi dari @RenanCoelho, saya membungkus baris kode ini dalam Timer dengan jeda 100 milidetik.
ymerdrengene
5

untuk mendapatkan hasil yang sempurna saya menggabungkan jawaban Colin Jackson dan CopsOnRoad sebagai berikut:

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

Saya memiliki begitu banyak masalah saat mencoba menggunakan pengontrol gulir untuk pergi ke bagian bawah daftar sehingga saya menggunakan pendekatan lain.

Alih-alih membuat acara untuk mengirim daftar ke bawah, saya mengubah logika saya untuk menggunakan daftar terbalik.

Jadi, setiap kali saya punya item baru, saya cukup, buat di sisipkan di bagian atas daftar.

// 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))
  ),
),
sidik jari
sumber
2
Satu-satunya masalah yang saya alami dengan ini adalah bahwa setiap bilah gulir bekerja mundur sekarang. Saat saya gulir ke bawah, mereka akan naik.
ThinkDigital
1

Jangan letakkan widgetBinding di initstate, Anda harus meletakkannya di metode yang mengambil data Anda dari database. misalnya seperti ini. Jika diletakkan di initstate, scrollcontrollertidak akan dilampirkan ke listview apa pun.

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

Anda bisa menggunakan ini di mana 0.09*heighttinggi baris dalam daftar dan _controllerdidefinisikan seperti ini_controller = ScrollController();

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