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

时间:2017-04-19 02:55:37

标签: flutter

我有一个可滚动的margin calc($var / 2),其中项目的数量可以动态更改。每当将新项添加到列表末尾时,我想以编程方式将ListView滚动到结尾。 (例如,聊天消息列表,最后可以添加新消息)

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

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

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

10 个答案:

答案 0 :(得分:33)

如果您使用收缩包装的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 :(得分:16)

使用ScrollController.jumpTo()ScrollController.animateTo()方法来实现。

这是代码段(1秒后,ListView将滚动到底部)

class _HomePageState extends State<HomePage> {
  ScrollController _controller = ScrollController();

  @override
  Widget build(BuildContext context) {
    Timer(Duration(milliseconds: 1000), () => _controller.jumpTo(_controller.position.maxScrollExtent));

    return ListView.builder(
      controller: _controller,
        itemCount: 50,
        itemBuilder: (context, index) => ListTile(title: Text("ListTile"),));
  }
}

答案 2 :(得分:5)

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

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

答案 3 :(得分:4)

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

  • 首先,在大多数情况下(谈到自动滚动)使用 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);
}

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

我们可以做得更好,在听众的帮助下和几个布尔值会很好。
我正在使用这种技术在 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()。< br/> 同时,仅在第一次执行时,我们强制对 _scrollToBottom() 的值进行 _firstAutoscrollExecuted 短路。

答案 4 :(得分:3)

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

答案 5 :(得分:1)

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

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

因此,每次我有一个新项目时,我都会简单地在列表顶部的insert处进行制作。

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

答案 6 :(得分:1)

int是最简单的方法。

答案 7 :(得分:1)

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

答案 8 :(得分:0)

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

答案 9 :(得分:-2)

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