178

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

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

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

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

4

14 回答 14

208

截屏:

在此处输入图像描述

  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);
    }
    
于 2018-10-29T11:53:15.080 回答
151

如果您使用带有 的收缩包装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),
          );
        }
      ),
    );
  }
}
于 2017-04-19T04:28:12.757 回答
21

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

于 2019-09-26T08:48:19.023 回答
14

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

_scrollController.animateTo(
    _scrollController.position.maxScrollExtent,
    curve: Curves.easeOut,
    duration: const Duration(milliseconds: 500),
 );
于 2020-07-07T04:50:40.643 回答
10

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

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

  • 使用reverse:true应该是“尾随”内容的好技巧,但物理会反转,因此当您尝试手动移动滚动条时,您必须将其移动到另一侧 - > BAD UX。

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

无论如何,谈到这个问题,完成任务的正确方法是使用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()并将其附加到_scrollController- initState> 将在第一次滚动条移动后触发。在这个监听器中,我们更新 bool 值_shouldAutoscroll,以了解滚动条是否位于屏幕底部。
  • 我们删除了监听器,dispose只是为了确保在小部件处理后不会做无用的事情。
  • 在我们setState确定_scrollController已附加并且位于底部(检查 的值shouldAutoscroll)时,我们可以调用_scrollToBottom().
    同时,仅在第一次执行时,我们强制_scrollToBottom()短路_firstAutoscrollExecuted.
于 2021-04-14T17:07:53.377 回答
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');
    }
   }
于 2020-09-27T07:50:55.553 回答
4

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

于 2021-01-10T12:31:14.027 回答
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))
  ),
),
于 2019-01-25T10:07:40.250 回答
2
_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;
  }
}
于 2021-05-16T20:43:54.737 回答
1

根据 这个答案 我创建了这个类,只需发送你的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,
      );
    });
  }
}
于 2021-08-30T11:51:12.573 回答
0

最简单的方法

mListView.smoothScrollToPosition(mList.size()-1);
于 2022-01-11T12:43:25.340 回答
0

我的解决方案:

步骤: 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 创建函数调用以滚动

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

当您想滚动到底部时调用,延迟 100 毫秒

Timer(const Duration(milliseconds: 100),() => scrollToBottom());
于 2022-02-21T02:38:13.417 回答
0

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

于 2022-03-03T10:46:50.793 回答
-3

您可以使用它 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));
    }
}
于 2020-05-26T15:47:50.010 回答