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

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

有没有简单的方法来实现这个目标?

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


当前回答

我使用的是一个动态列表视图,但scrollController.animateTo()不会工作在这里提到的动态列表https://stackoverflow.com/a/67561421/13814518,我甚至没有在以前的答复中找到任何好的解决方案。下面是我解决这个问题的方法。

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

其他回答

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

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

对我来说,问题是scrollController.position.maxScrollExtent总是返回0.0。原因是我的ListView在ScrollView里面。

删除ScrollView修复了这个问题。

截图:

Scrolling with animation: 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')), ), ); } Scrolling without animation: Replace above _scrollDown method with this: void _scrollDown() { _controller.jumpTo(_controller.position.maxScrollExtent); }

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

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

First of all in most cases (speaking about auto scrolling) is useless using postFrameCallbacks because some stuff could be rendered after the ScrollController attachment (produced by the attach method), the controller will scroll until the last position that he knows and that position could not be the latest in your view. Using reverse:true should be a good trick to 'tail' the content but the physic will be reversed so when you try to manually move the scrollbar you must move it to the opposite side -> BAD UX. Using timers is a very bad practice when designing graphic interfaces -> timer are a kind of virus when used to update/spawn graphics artifacts.

不管怎样,说到这个问题,完成任务的正确方法是使用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();
    }
});

解释

We made a simple method: _scrollToBottom() in order to avoid code repetitions; We made a _scrollListener() and we attached it to the _scrollController in the initState -> will be triggered after the first time that the scrollbar will move. In this listener we update the value of the bool value _shouldAutoscroll in order to understand if the scrollbar is at the bottom of the screen. We removed the listener in the dispose just to be sure to not do useless stuff after the widget dispose. In our setState when we are sure that the _scrollController is attached and that's at the bottom (checking for the value of shouldAutoscroll) we can call _scrollToBottom(). At the same time, only for the 1st execution we force the _scrollToBottom() short-circuiting on the value of _firstAutoscrollExecuted.