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

Flutter 以编程方式滚动到ListView的末尾,flutter,flutter-layout,flutter-animation,Flutter,Flutter Layout,Flutter Animation,我有一个可滚动的列表视图,其中项目的数量可以动态变化。每当一个新项目被添加到列表的末尾,我都希望通过编程将列表视图滚动到末尾。(例如,类似聊天信息列表的内容,可在列表末尾添加新信息) 我的猜测是,我需要在我的状态对象中创建一个ScrollController,并手动将其传递给ListView构造函数,以便稍后在控制器上调用animateTo()/jumpTo()方法。但是,由于我无法轻松确定最大滚动偏移量,因此似乎不可能简单地执行scrollToEnd()类型的操作(而我可以轻松通过0.0使其滚

我有一个可滚动的
列表视图
,其中项目的数量可以动态变化。每当一个新项目被添加到列表的末尾,我都希望通过编程将
列表视图
滚动到末尾。(例如,类似聊天信息列表的内容,可在列表末尾添加新信息)

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

有没有一个简单的方法来实现这一点


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

如果使用收缩包装的
ListView
reverse: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),
          );
        }
      ),
    );
  }
}
导入“dart:collection”;
进口“包装:颤振/材料.省道”;
void main(){
runApp(新的MyApp());
}
类MyApp扩展了无状态小部件{
@凌驾
小部件构建(构建上下文){
返回新材料PP(
标题:“示例”,
主页:新建MyHomePage(),
);
}
}
类MyHomePage扩展StatefulWidget{
@凌驾
_MyHomePageState createState()=>new_MyHomePageState();
}
类_MyHomePageState扩展状态{
列表_messages=[新文本('hello')、新文本('world');
ScrollController_ScrollController=新的ScrollController();
@凌驾
小部件构建(构建上下文){
归还新脚手架(
正文:新中心(
子容器:新容器(
装饰:新盒子装饰(背景颜色:颜色。蓝灰色。阴影100),
宽度:100.0,
高度:100.0,
子:新列(
儿童:[
新柔性(
子:新列表视图(
控制器:\ u滚动控制器,
相反:是的,
收缩膜:对,
子项:新的不可修改列表视图(_消息),
),
),
],
),
),
),
floatingActionButton:新的floatingActionButton(
子:新图标(Icons.add),
已按下:(){
设置状态(){
_插入(0,新文本(“message${u messages.length}”);
});
_scrollController.animateTo(
0.0,
曲线:Curves.easeOut,
持续时间:常量持续时间(毫秒:300),
);
}
),
);
}
}

使用
ScrollController.jumpTo()
ScrollController.animateTo()
方法来实现此目的

例如:

final _controller = ScrollController();

@override
Widget build(BuildContext context) {
  
  // After 1 second, it takes you to the bottom of the ListView
  Timer(
    Duration(seconds: 1),
    () => _controller.jumpTo(_controller.position.maxScrollExtent),
  );

  return ListView.builder(
    controller: _controller,
    itemCount: 50,
    itemBuilder: (_, __) => ListTile(title: Text('ListTile')),
  );
}

如果您希望平滑滚动,请不要使用上面的
jumpTo

_controller.animateTo(
  _controller.position.maxScrollExtent,
  duration: Duration(seconds: 1),
  curve: Curves.fastOutSlowIn,
);

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

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

因此,每次我有一个新的项目,我只是,在列表顶部的插入

// 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
是列表中一行的高度,
\u controller
的定义如下
\u controller=ScrollController()


为了得到完美的结果,我将科林·杰克逊和科波森路的答案结合在一起,如下所示:

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

不要将widgetBinding置于initstate,相反,您需要将其置于从数据库获取数据的方法中。比如说像这样。如果置于initstate,则
scrollcontroller
将不会附加到任何listview

    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');
    }
   }
Future fetchMessage()异步{
var res=await Api().getData(“消息”);
var body=json.decode(res.body);
如果(res.statusCode==200){
列表消息=[];
var计数=0;
用于(主体中的变量u){
计数++;
Message Message=Message.fromJson(u);
消息。添加(消息);
}
WidgetsBinding.instance
.addPostFrameCallback((){
if(_scrollController.hasClients){
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
返回消息;
}否则{
抛出异常(“加载相册失败”);
}
}

我在使用
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
}),
...

我用
\u controller.animateTo
试过,但似乎不起作用。

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

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

  • 使用
    reverse:true
    应该是一个很好的技巧来“跟踪”内容,但physic将被反转,因此当您尝试手动移动滚动条时,必须将其移动到另一侧->坏用户体验

  • 在设计图形界面时,使用计时器是一种非常糟糕的做法->计时器在用于更新/繁殖图形艺术时是一种病毒
    ...
    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
    }),
    ...
    
    if (_scrollController.hasClients) {
        _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
    }
    
    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;
        }
    }
    
    @override
    void initState() {
        super.initState();
        _scrollController.addListener(_scrollListener);
    }
    
    @override
    void dispose() {
        _scrollController.removeListener(_scrollListener);
        super.dispose();
    }
    
    setState(() {
        if (_scrollController.hasClients && _shouldAutoscroll) {
            _scrollToBottom();
        }
    
        if (!_firstAutoscrollExecuted && _scrollController.hasClients) {
             _scrollToBottom();
        }
    });
    
    _controller.jumpTo(_controller.position.maxScrollExtent);
    _controller.animateTo(_controller.position.maxScrollExtent);
    
    Future scrollToBottom(ScrollController scrollController) async {
      while (scrollController.position.pixels != scrollController.position.maxScrollExtent) {
        scrollController.jumpTo(scrollController.position.maxScrollExtent);
        await SchedulerBinding.instance!.endOfFrame;
      }
    }