【问题标题】:Programmatically scrolling to the end of a ListView以编程方式滚动到 ListView 的末尾
【发布时间】:2017-09-15 01:56:46
【问题描述】:

我有一个可滚动的ListView,其中项目的数量可以动态变化。每当将新项目添加到列表末尾时,我想以编程方式将 ListView 滚动到末尾。 (例如,可以在末尾添加新消息的聊天消息列表)

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

有没有简单的方法来实现这一点?

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

【问题讨论】:

    标签: flutter flutter-layout flutter-animation


    【解决方案1】:

    截图:

    1. 用动画滚动:

      final ScrollController _controller = ScrollController();
      
      // This is what you're looking for!
      void _scrollDown() {
        _controller.animateTo(
          _controller.position.maxScrollExtent,
          duration: Duration(seconds: 2),
          curve: Curves.fastOutSlowIn,
        );
      }
      
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          floatingActionButton: FloatingActionButton.small(
            onPressed: _scrollDown,
            child: Icon(Icons.arrow_downward),
          ),
          body: ListView.builder(
            controller: _controller,
            itemCount: 21,
            itemBuilder: (_, i) => ListTile(title: Text('Item $i')),          
          ),
        );
      }
      
    2. 无动画滚动:

      用这个替换上面的_scrollDown方法:

      void _scrollDown() {
        _controller.jumpTo(_controller.position.maxScrollExtent);
      }
      

    【讨论】:

    • 对于 Firebase 实时数据库,到目前为止,我已经能够摆脱 250 毫秒(这可能真的很长)。我想知道我们能走多低?问题是 maxScrollExtent 需要等待小部件构建完成并添加新项目。如何分析从回调到构建完成的平均持续时间?
    • 您认为这可以与使用 streambuilder 从 firebase 获取数据一起正常工作吗?还在发送按钮上添加聊天消息吗?另外,如果互联网连接很慢,它会起作用吗?如果您能提出建议,那就更好了。谢谢。
    • @SacWebDeveloper 你对这个问题的处理方法是什么?
    • @IdrisStack 实际上,250 毫秒显然不够长,因为有时,跳转发生在更新发生之前。我认为更好的方法是比较更新后的列表长度,如果长度大一则跳转到底部。
    • 希望不是一种策略:系统可能会延迟你的应用程序的执行,只要它愿意。您应该在后帧回调中执行此操作,如另一个答案中所述:stackoverflow.com/a/64086128/1220560
    【解决方案2】:

    如果您使用收缩包装的 ListViewreverse: 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),
              );
            }
          ),
        );
      }
    }
    

    【讨论】:

    • 这成功了。在我的特定情况下,我必须使用嵌套列将 ListView 放入 Expanded 小部件,但基本思想是相同的。
    • 我来寻找解决方案,只是想提一下,其他答案可能是实现此目标的更好方法 - stackoverflow.com/questions/44141148/…
    • 我同意,这个答案(我也写过)肯定更好。
    • 为什么我们必须启用反向为真?
    • @Dennis 这样 ListView 从底部以相反的顺序开始。
    【解决方案3】:

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

    【讨论】:

    • 嗨杰克,谢谢你的回答,我相信我写的一样,所以再次添加相同的解决方案恕我直言。
    • 我正在使用这种方法,但您需要等待一些毫秒才能执行此操作,因为在运行 animateTo(...) 之前需要渲染屏幕。也许我们有一个很好的方法,使用良好的实践来做到这一点而无需 hack。
    • 这是正确的答案。如果您在当前行中 +100,它会将您的列表滚动到底部。像 listViewScrollController.position.maxScrollExtent+100;
    • 在@RenanCoelho 的启发下,我将这行代码封装在一个延迟为 100 毫秒的 Timer 中。
    【解决方案4】:

    为了获得完美的结果,我将 Colin Jackson 和 CopsOnRoad 的答案组合如下:

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

    【讨论】:

      【解决方案5】:

      虽然所有答案都产生了预期的效果,但我们应该在这里做一些改进。

      • 首先在大多数情况下(谈到自动滚动)使用 postFrameCallbacks 是无用的,因为可以在 ScrollController 附件(由attach 方法产生)之后呈现一些东西,控制器将滚动到他知道的最后一个位置在您看来,该职位不可能是最新的。

      • 使用reverse:true 应该是“尾随”内容的一个好技巧,但物理会颠倒,所以当您尝试手动移动滚动条时,您必须将其移动到另一侧 -> 糟糕的用户体验。

      • 在设计图形界面时使用计时器是一种非常糟糕的做法 -> 在用于更新/生成图形工件时,计时器是一种病毒。

      无论如何,说到问题,完成任务的正确方法是使用jumpTo 方法和hasClients 方法作为警卫。

      是否有任何 ScrollPosition 对象已使用 attach 方法将自己附加到 ScrollController。 如果为 false,则不得调用与 ScrollPosition 交互的成员,例如 position、offset、animateTo 和 jumpTo。

      用代码说话只需做这样的事情:

      if (_scrollController.hasClients) {
          _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
      }
      

      无论如何,这段代码还不够,即使滚动条不在屏幕末尾,也会触发该方法,因此如果您手动移动栏,该方法将触发并执行自动滚动。

      我们可以做得更好,在听众的帮助下,几个 bool 就可以了。
      我正在使用这种技术在 SelectableText 中可视化大小为 100000 的 CircularBuffer 的值,并且内容不断正确更新,自动滚动非常流畅,即使对于非常非常长的内容也没有性能问题。也许正如有人在其他答案中所说的那样,animateTo 方法可能更流畅,更可定制,所以请随意尝试。

      • 首先声明这些变量:
      ScrollController _scrollController = new ScrollController();
      bool _firstAutoscrollExecuted = false;
      bool _shouldAutoscroll = false;
      
      • 然后让我们创建一个自动滚动的方法:
      void _scrollToBottom() {
          _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
      }
      
      • 那么我们需要监听器:
      void _scrollListener() {
          _firstAutoscrollExecuted = true;
      
          if (_scrollController.hasClients && _scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
              _shouldAutoscroll = true;
          } else {
              _shouldAutoscroll = false;
          }
      }
      
      • initState注册:
      @override
      void initState() {
          super.initState();
          _scrollController.addListener(_scrollListener);
      }
      
      • 删除dispose中的监听器:
      @override
      void dispose() {
          _scrollController.removeListener(_scrollListener);
          super.dispose();
      }
      
      • 然后触发_scrollToBottom,根据您的逻辑和需求,在您的setState
      setState(() {
          if (_scrollController.hasClients && _shouldAutoscroll) {
              _scrollToBottom();
          }
      
          if (!_firstAutoscrollExecuted && _scrollController.hasClients) {
               _scrollToBottom();
          }
      });
      

      解释

      • 为了避免代码重复,我们做了一个简单的方法:_scrollToBottom()
      • 我们创建了一个_scrollListener() 并将它附加到initState 中的_scrollController -> 将在滚动条第一次移动后触发。在这个监听器中,我们更新布尔值_shouldAutoscroll 的值,以了解滚动条是否位于屏幕底部。
      • 我们删除了 dispose 中的侦听器,只是为了确保在小部件处置后不会做无用的事情。
      • 在我们的setState 中,当我们确定_scrollController 已连接并且位于底部(检查shouldAutoscroll 的值)时,我们可以调用_scrollToBottom()
        同时,仅在第一次执行时,我们强制 _scrollToBottom() 短路 _firstAutoscrollExecuted 的值。

      【讨论】:

        【解决方案6】:

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

        【讨论】:

        • 我会把 TL;DR WidgetsBinding.instance.addPostFrameCallback((_){_scrollController.jumpTo(_scrollController.position.maxScrollExtent);}) 放在这个答案的顶部,因为周围的代码很混乱。
        【解决方案7】:

        我在使用StreamBuilder 小部件从我的数据库中获取数据时遇到了这个问题。我将WidgetsBinding.instance.addPostFrameCallback 放在小部件的build 方法之上,它不会一直滚动到最后。我通过这样做修复了它:

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

        我也用_controller.animateTo 尝试过,但似乎没有用。

        【讨论】:

          【解决方案8】:

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

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

          所以,每次我有一个新项目时,我都会在列表顶部插入。

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

          【讨论】:

          • 我遇到的唯一问题是任何滚动条现在都向后工作。当我向下滚动时,它们会向上移动。
          【解决方案9】:
          _controller.jumpTo(_controller.position.maxScrollExtent);
          _controller.animateTo(_controller.position.maxScrollExtent);
          

          这些调用不适用于动态大小的项目列表。我们不知道您调用jumpTo() 时列表有多长,因为所有项目都是可变的,并且在我们向下滚动列表时会延迟构建。

          这可能不是明智的方法,但作为最后的手段,您可以执行以下操作:

          Future scrollToBottom(ScrollController scrollController) async {
            while (scrollController.position.pixels != scrollController.position.maxScrollExtent) {
              scrollController.jumpTo(scrollController.position.maxScrollExtent);
              await SchedulerBinding.instance!.endOfFrame;
            }
          }
          

          【讨论】:

            【解决方案10】:

            取决于this answer我已经创建了这个类,只需发送你的scroll_controller,如果你想要相反的方向使用reversed参数

            import 'package:flutter/material.dart';
            import 'package:flutter/scheduler.dart';
            
            class ScrollService {
              static scrollToEnd(
                  {required ScrollController scrollController, reversed = false}) {
                SchedulerBinding.instance!.addPostFrameCallback((_) {
                  scrollController.animateTo(
                    reverced
                        ? scrollController.position.minScrollExtent
                        : scrollController.position.maxScrollExtent,
                    duration: const Duration(milliseconds: 300),
                    curve: Curves.easeOut,
                  );
                });
              }
            }
            

            【讨论】:

              【解决方案11】:

              最简单的方法

              mListView.smoothScrollToPosition(mList.size()-1);
              

              【讨论】:

                【解决方案12】:

                您可以使用它,其中0.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));
                    }
                }
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-10-19
                  • 2011-11-11
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2018-07-11
                  相关资源
                  最近更新 更多