Lập trình cuộn đến cuối ListView


98

Tôi có một thanh cuộn ListViewtrong đó số lượng các mục có thể thay đổi động. Bất cứ khi nào một mục mới được thêm vào cuối danh sách, tôi muốn cuộn ListViewđến cuối theo chương trình . (ví dụ: một cái gì đó giống như danh sách tin nhắn trò chuyện, nơi có thể thêm tin nhắn mới vào cuối)

Tôi đoán là tôi sẽ cần tạo một đối tượng ScrollControllertrong Stateđối tượng của mình và chuyển nó theo cách thủ công cho hàm ListViewtạo, để sau này tôi có thể gọi animateTo()/ jumpTo()phương thức trên bộ điều khiển. Tuy nhiên, vì tôi không thể dễ dàng xác định độ lệch cuộn tối đa, nên dường như không thể thực hiện một cách đơn giản một scrollToEnd()loại thao tác (trong khi tôi có thể dễ dàng vượt qua 0.0để làm cho nó cuộn về vị trí ban đầu).

Có cách nào dễ dàng để đạt được điều này không?

Sử dụng reverse: truekhông phải là giải pháp hoàn hảo đối với tôi, bởi vì tôi muốn các mục được căn chỉnh ở trên cùng khi chỉ có một số lượng nhỏ các mục vừa với ListViewkhung nhìn.

Câu trả lời:


97

Nếu bạn sử dụng một co-bọc ListViewvới reverse: true, di chuyển nó đến 0.0 sẽ làm những gì bạn muốn.

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

1
Điều này đã thực hiện thủ thuật. Trong trường hợp cụ thể của tôi, tôi phải sử dụng các Cột lồng nhau để đặt ListView trong tiện ích con Mở rộng, nhưng ý tưởng cơ bản là giống nhau.
yyoon

20
Tôi đến tìm kiếm một giải pháp, chỉ muốn đề cập đến rằng câu trả lời khác có thể là một cách tốt hơn để đạt được điều này - stackoverflow.com/questions/44141148/...
Animesh Jain

6
Tôi đồng ý, câu trả lời đó (mà tôi cũng đã viết) chắc chắn tốt hơn.
Collin Jackson

1
@Dennis Để ListView bắt đầu theo thứ tự ngược lại từ dưới lên.
CopsOnRoad,

1
Câu trả lời từ @CopsOnRoad trông đẹp hơn và câu trả lời này trông giống một bản hack hơn là một giải pháp.
Roopak A Nelliat

118

Sử dụng ScrollController.jumpTo()hoặc ScrollController.animateTo()phương pháp để đạt được điều này.

Thí dụ:

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

Nếu bạn muốn cuộn trơn tru, thì thay vì sử dụng jumpToở trên, hãy sử dụng

_controller.animateTo(
  _controller.position.maxScrollExtent,
  duration: Duration(seconds: 1),
  curve: Curves.fastOutSlowIn,
);

4
Đối với Cơ sở dữ liệu thời gian thực của Firebase, cho đến nay tôi đã có thể thực hiện được với 250ms (có lẽ là rất lâu). Tôi tự hỏi chúng ta có thể xuống thấp đến mức nào? Vấn đề là maxScrollExtent cần đợi quá trình xây dựng tiện ích con hoàn tất với mục mới được thêm vào. Làm cách nào để lập hồ sơ thời lượng trung bình từ khi gọi lại đến khi hoàn thành quá trình xây dựng?
SacWebDeveloper 19/08/19

2
Bạn có nghĩ rằng điều này sẽ hoạt động bình thường khi sử dụng streambuilder để tìm nạp dữ liệu từ firebase không? Cũng thêm tin nhắn trò chuyện trên nút gửi? Ngoài ra nếu kết nối internet chậm nó sẽ hoạt động? bất cứ điều gì tốt hơn nếu bạn có thể đề xuất. Cảm ơn.
Jay Mungara

@SacWebDeveloper cách tiếp cận của bạn với vấn đề này cộng với firebase là gì?
Idris Stack

@IdrisStack Trên thực tế, 250ms rõ ràng là không đủ dài vì đôi khi, jumpTo xảy ra trước khi cập nhật xảy ra. Tôi nghĩ rằng cách tiếp cận tốt hơn sẽ là so sánh độ dài danh sách sau khi cập nhật và jumpT xuống dưới cùng nếu độ dài lớn hơn một.
SacWebDeveloper

Cảm ơn @SacWebDeveloper, cách tiếp cận của bạn hiệu quả, có lẽ tôi có thể hỏi bạn một câu hỏi khác trong bối cảnh của Firestore, nó có thể chuyển hướng chủ đề. Qn. Tại sao khi tôi gửi tin nhắn cho ai đó danh sách không tự động cuộn xuống cuối, có cách nào để nghe tin nhắn và cuộn xuống dưới cùng không?
Idris Stack

13

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) là cách đơn giản nhất.


1
Chào Jack, cảm ơn câu trả lời của bạn, tôi tin rằng tôi đã viết như vậy, vì vậy không có lợi ích gì khi thêm lại cùng một giải pháp IMHO.
CopsOnRoad vào

1
Tôi đang sử dụng phương pháp này nhưng bạn cần đợi vài giây để thực hiện vì màn hình cần được hiển thị trước khi chạy animateTo (...). Có lẽ chúng tôi có một cách hay khi sử dụng các phương pháp hay để làm điều đó mà không bị hack.
Renan Coelho

1
Đó là câu trả lời đúng. Nó sẽ cuộn danh sách của bạn xuống cuối nếu bạn +100 ở dòng hiện tại. như listViewScrollController.position.maxScrollExtent + 100;
Rizwan Ansar

1
Với nguồn cảm hứng từ @RenanCoelho, tôi đã gói dòng mã này trong Bộ hẹn giờ với độ trễ 100 mili giây.
ymerdrengene

5

để có được kết quả hoàn hảo, tôi đã kết hợp các câu trả lời của Colin Jackson và CopsOnRoad như sau:

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

2

Tôi gặp rất nhiều vấn đề khi cố gắng sử dụng bộ điều khiển cuộn để đi đến cuối danh sách mà tôi sử dụng một cách tiếp cận khác.

Thay vì tạo sự kiện để gửi danh sách xuống dưới cùng, tôi thay đổi logic của mình để sử dụng danh sách đảo ngược.

Vì vậy, mỗi khi tôi có một mục mới, tôi chỉ đơn giản là chèn vào đầu danh sách.

// 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))
  ),
),

2
Vấn đề duy nhất mà tôi gặp phải là bất kỳ thanh cuộn nào hiện hoạt động ngược lại. Khi tôi cuộn xuống, chúng di chuyển lên trên.
ThinkDigital

1

Không đặt widgetBinding trong initstate, thay vào đó, bạn cần đặt nó trong phương thức tìm nạp dữ liệu của bạn từ cơ sở dữ liệu. ví dụ, như thế này. Nếu được đặt trong initstate, scrollcontrollersẽ không đính kèm vào bất kỳ listview nào.

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

-2

bạn có thể sử dụng điều này, đâu 0.09*heightlà chiều cao của một hàng trong danh sách và _controllerđược xác định như thế này_controller = ScrollController();

(BuildContext context, int pos) {
    if(pos != 0) {
        _controller.animateTo(0.09 * height * (pos - 1), 
                              curve: Curves.easeInOut,
                              duration: Duration(milliseconds: 1400));
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.