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