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

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

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

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


当前回答

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

其他回答

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

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.

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

如果你使用一个带有reverse: true的收缩包装的ListView,那么将它滚动到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),
          );
        }
      ),
    );
  }
}

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

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

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