以编程方式滚动到ListView的末尾


108

我有一个可滚动ListView的项目的数量可以动态更改。每当将新项目添加到列表的末尾时,我都希望以编程方式将其滚动ListView到末尾。(例如,像聊天消息列表之类的,可以在末尾添加新消息)

我的猜测是,我需要ScrollController在我的State对象中创建一个并将其手动传递给ListView构造函数,以便以后可以在控制器上调用animateTo()/jumpTo()方法。但是,由于我无法轻松确定最大滚动偏移量,因此似乎不可能简单地执行某种scrollToEnd()类型的操作(而我可以轻松通过0.0以使其滚动至初始位置)。

有没有简单的方法可以做到这一点?

使用reverse: true对我来说不是一个完美的解决方案,因为当ListView视口内只有少量项目时,我希望这些项目在顶部对齐。

Answers:


106

如果将收缩包装ListView与一起使用reverse: true,则将其滚动到0.0即可完成所需的操作。

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
这成功了。在我的特定情况下,我必须使用嵌套的Columns才能将ListView放在Expanded小部件中,但是基本思想是相同的。
yyoon '17

21
我来寻找解决的办法,只是想提的是,对方的回答可能是一个更好的方式来实现这一目标- stackoverflow.com/questions/44141148/...
的Animesh耆那教

6
我同意,这个答案(我也写过)肯定更好。
科林·杰克逊

1
@Dennis以便ListView以相反的顺序从底部开始。
CopsOnRoad '18

1
@CopsOnRoad的答案看起来更好,这个答案看起来更像是骇客,而不是解决方案。
Roopak A Nelliat

128

使用ScrollController.jumpTo()ScrollController.animateTo()方法来实现这一目标。

例:

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

如果要平滑滚动,则不要使用jumpTo上面的用法

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

4
对于Firebase Realtime Database,到目前为止,我已经能够摆脱250ms(这可能真的很长)的问题。我想知道我们能走多低?问题是maxScrollExtent需要等待小部件构建完成并添加新项。如何分析从回调到构建完成的平均持续时间?
SacWebDeveloper

2
您认为使用streambuilder从firebase提取数据是否可以正常工作?还可以在“发送”按钮上添加聊天消息吗?另外,如果互联网连接速度慢,是否可以使用?如果您可以提出建议,那就更好了。谢谢。
杰伊·蒙加拉

@SacWebDeveloper对于此问题以及firebase,您采取什么方法?
Idris Stack

@IdrisStack实际上,250ms显然不够长,因为有时候,jumpTo发生在更新发生之前。我认为更好的方法是将更新后的列表长度与跳转到底部的列表长度进行比较(如果长度大一个)。
SacWebDeveloper

感谢@SacWebDeveloper,您的方法行得通,也许我可以在Firestore的上下文中问另一个问题,它可能会使话题有些偏离。Qn。为什么当我向某人发送消息时,列表不会自动滚动到底部,有没有办法收听消息并滚动到底部?
Idris Stack

14

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) 是最简单的方法。


2
嗨,杰克,谢谢您的回答,我相信我写的也一样,因此再次添加恕我直言相同的解决方案没有任何好处。
CopsOnRoad

1
我正在使用这种方法,但是您需要等待一些毫秒才能完成此操作,因为在运行animateTo(...)之前需要渲染屏幕。也许我们有一个很好的方法,可以使用良好的实践来做到这一点而不会被黑客入侵。
Renan Coelho

1
那是正确的答案。如果您在当前行中+100,它将把您的列表滚动到底部。像listViewScrollController.position.maxScrollExtent + 100;
Rizwan Ansar

1
受到@RenanCoelho的启发,我将这行代码包装在一个延迟为100毫秒的Timer中。
ymerdrengene

5

为了获得最佳结果,我结合了Colin Jackson和CopsOnRoad的答案,如下所示:

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

2

我在尝试使用滚动控制器进入列表底部时遇到了很多问题,因此我使用了另一种方法。

我没有创建将列表发送到底部的事件,而是更改了使用反向列表的逻辑。

因此,每次我有一个新商品时,我都会简单地在列表顶部的插入处制作。

// 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
我唯一遇到的问题是任何滚动条现在都可以向后工作。当我向下滚动时,它们会向上移动。
ThinkDigital

2

不要将widgetBinding放在initstate中,而是需要将其放在从数据库中获取数据的方法中。例如,像这样。如果设为initstate,scrollcontroller则不会附加到任何列表视图。

    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

您可以使用where0.09*height在列表中一行的高度,并且_controller像这样定义_controller = ScrollController();

(BuildContext context, int pos) {
    if(pos != 0) {
        _controller.animateTo(0.09 * height * (pos - 1), 
                              curve: Curves.easeInOut,
                              duration: Duration(milliseconds: 1400));
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.