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

Programmatically scrolling to the end of a ListView

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

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

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

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

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

截图:

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

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

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

因此,每次我有一个新项目时,我都会简单地插入列表顶部。

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

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

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

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

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

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

我在使用 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 试过了,但似乎没有用。

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

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

  • 使用 reverse:true 应该是 'tail' 内容的一个好技巧,但是物理会反转,所以当您尝试手动移动滚动条时,您必须将它移动到对面 -> 糟糕的用户体验。

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

无论如何,关于这个问题,完成任务的正确方法是使用 jumpTo 方法和 hasClients 方法作为守卫。

Whether any ScrollPosition objects have attached themselves to the ScrollController using the attach method. If this is false, then members that interact with the ScrollPosition, such as position, offset, animateTo, and jumpTo, must not be called

用代码简单地做这样的事情:

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

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

我们可以做得更好,在监听器的帮助下和几个布尔值就可以了。
我正在使用这种技术在 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() .
    同时,仅在第一次执行时,我们强制对 _firstAutoscrollExecuted.
  • 的值进行 _scrollToBottom() 短路
_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;
  }
}

取决于 我创建了这个 class,只需发送您的 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,
      );
    });
  }
}

最简单的方法

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

我的解决方案:

步骤:1 像这样定义全局键:

final lastKey = GlobalKey();

步骤:2 附加到最后一条消息

SingleChildScrollView(
    controller: scrollController,
    padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
    physics: const AlwaysScrollableScrollPhysics(),
    child: Column(
        children: List.generate(
            data.length,
            (index) {
            return BuildMessage(
                key:
                    data.length == index + 1 ? lastKey : null,
                message: data[index],
                focusNode: focusNode,
            );
            },
        ),
    ),
)

步骤:3 创建对 scroll

的函数调用
void scrollToBottom() {
    Scrollable.ensureVisible(lastKey.currentContext!,alignment: 1, duration: const Duration(milliseconds: 500));
}

当你想滚动到底部并延迟 100 毫秒时调用

Timer(const Duration(milliseconds: 100),() => scrollToBottom());

_scrollController.animateTo( _scrollController.position.maxScrollExtent, 持续时间:常量持续时间(毫秒:400), 曲线:Curves.fastOutSlowIn); });

我使用的是动态列表视图,但是 scrollController.animateTo() 不适用于这里提到的动态列表 ,而且我什至在之前的回复中都没有找到任何好的解决方案.这就是我解决问题的方法。

void scrollToMaxExtent() {
  WidgetsBinding.instance.addPostFrameCallback((_) {
    scrollController.animateTo(
      scrollController.position.maxScrollExtent,
      duration: const Duration(milliseconds: 100),
      curve: Curves.easeIn,
    );
  });
}

如果你想看到最后一个可见的项目,从底部开始填充,然后像这样添加额外的距离

 _controller.jumpTo(_controller.position.maxScrollExtent + 200);

此处 200 将是额外的距离