在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。
1. ScrollController
ScrollController构造函数如下:
ScrollController({
double initialScrollOffset = 0.0, //初始滚动位置
this.keepScrollOffset = true,//是否保存滚动位置
...
})
ScrollController常用的属性和方法:
- offset:可滚动组件当前的滚动位置。
- jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
ScrollController滚动监听
ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件,如:
controller.addListener(()=>print(controller.offset))
示例
我们创建一个ListView,当滚动位置发生变化时,我们先打印出当前滚动位置,然后判断当前位置是否超过1000像素,如果超过则在屏幕右下角显示一个“返回顶部”的按钮,该按钮点击后可以使ListView恢复到初始位置;如果没有超过1000像素,则隐藏“返回顶部”按钮。
class MSHomePage extends StatefulWidget {
@override
State createState() => _MSHomePageState();
}
class _MSHomePageState extends State {
ScrollController scr = ScrollController();
bool _showTopBtn = false; //是否显示“返回到顶部”按钮
@override
void initState() {
super.initState();
// 监听滚动位置,并打印
scr.addListener(() {
print(scr.offset);
bool _curShowBtnState = false;
if (scr.offset < 1000) {
_curShowBtnState = false;
} else {
_curShowBtnState = true;
}
// 只有showTopBtn的值发生变化时,才刷新
if (_curShowBtnState != _showTopBtn) {
_showTopBtn = _curShowBtnState;
setState(() {});
}
});
}
@override
void dispose() {
//为了避免内存泄露,需要调用src.dispose
scr.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("滚动组件"),
),
// Scrollbar 滚动条
body: Scrollbar(
controller: scr,
child: ListView.builder(
controller: scr,
itemBuilder: (BuildContext ctx, int index) {
return ListTile(
leading: Text("$index"),
);
},
itemCount: 100,
itemExtent: 50, // 列表项 固定高度
),
),
floatingActionButton: _showTopBtn
? FloatingActionButton(
onPressed: () {
// 返回到顶部时执行动画
scr.animateTo(0,
duration: Duration(microseconds: 200),
curve: Curves.easeIn);
},
child: Icon(Icons.arrow_upward),
)
: null,
);
}
}
ScrollPosition
ScrollPosition是用来保存可滚动组件的滚动位置的。一个ScrollController对象可以同时被多个可滚动组件使用,ScrollController会为每一个可滚动组件创建一个ScrollPosition对象,这些ScrollPosition保存在ScrollController的positions属性中(List
double get offset => position.pixels;
一个ScrollController
虽然可以对应多个可滚动组件,但是有一些操作,如读取滚动位置offset
,则需要一对一!但是我们仍然可以在一对多的情况下,通过其它方法读取滚动位置,举个例子,假设一个ScrollController
同时被两个可滚动组件使用,那么我们可以通过如下方式分别读取他们的滚动位置:
...
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
...
我们可以通过controller.positions.length
来确定controller
被几个可滚动组件使用。
ScrollPosition的方法
ScrollPosition
有两个常用方法:animateTo()
和 jumpTo()
,它们是真正来控制跳转滚动位置的方法,ScrollController
的这两个同名方法,内部最终都会调用ScrollPosition
的。
ScrollController控制原理
我们来介绍一下ScrollController
的另外三个方法:
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition);
void attach(ScrollPosition position) ;
void detach(ScrollPosition position) ;
当ScrollController
和可滚动组件关联时,可滚动组件首先会调用ScrollController
的createScrollPosition()
方法来创建一个ScrollPosition
来存储滚动位置信息,接着,可滚动组件会调用attach()
方法,将创建的ScrollPosition
添加到ScrollController
的positions
属性中,这一步称为“注册位置”,只有注册后animateTo()
和 jumpTo()
才可以被调用。
当可滚动组件销毁时,会调用ScrollController
的detach()
方法,将其ScrollPosition
对象从ScrollController
的positions
属性中移除,这一步称为“注销位置”,注销后animateTo()
和 jumpTo()
将不能再被调用。
需要注意的是,ScrollController
的animateTo()
和 jumpTo()
内部会调用所有ScrollPosition
的animateTo()
和 jumpTo()
,以实现所有和该ScrollController
关联的可滚动组件都滚动到指定的位置。
2. NotificationListener
Flutter Widget树中子Widget可以通过发送通知(Notification)与父(包括祖先)Widget通信。父级组件可以通过NotificationListener组件来监听自己关注的通知
可滚动组件在滚动时会发送ScrollNotification
类型的通知,ScrollBar
正是通过监听滚动通知来实现的。通过NotificationListener
监听滚动事件和通过ScrollController
有两个主要的不同:
- 通过NotificationListener可以在从可滚动组件到widget树根之间任意位置都能监听。而
ScrollController
只能和具体的可滚动组件关联后才可以。 - 收到滚动事件后获得的信息不同;
NotificationListener
在收到滚动事件时,通知中会携带当前滚动位置和ViewPort的一些信息,而ScrollController
只能获取当前滚动位置。
示例
下面,我们监听ListView的滚动通知,然后显示当前滚动进度百分比:
class MSHomePage extends StatefulWidget {
@override
State createState() {
return _MSHomePageState();
}
}
class _MSHomePageState extends State {
String _progress = "0%";
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification noti) {
if (noti is ScrollStartNotification) {
print("开始滚动");
} else if (noti is ScrollUpdateNotification) {
print("正在滚动");
} else if (noti is ScrollEndNotification) {
print("结束滚动");
} else {}
double pro = noti.metrics.pixels / noti.metrics.maxScrollExtent;
setState(() {
_progress = "${(pro * 100).toInt()}%";
});
return false;
},
child: Stack(
alignment: AlignmentDirectional.center,
children: [
ListView.builder(
itemCount: 100,
itemExtent: 56,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.people),
title: Text("联系人 ${index + 1}"),
trailing: Icon(Icons.delete),
);
},
),
CircleAvatar(
radius: 30,
child: Text(_progress),
backgroundColor: Colors.black54,
),
],
),
);
}
}
在接收到滚动事件时,参数类型为ScrollNotification,它包括一个metrics属性,它的类型是ScrollMetrics,该属性包含当前ViewPort及滚动位置等信息:
- pixels:当前滚动位置。
- maxScrollExtent:最大可滚动长度。
- extentBefore:滑出ViewPort顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。
- extentInside:ViewPort内部长度;此示例中屏幕显示的列表部分的长度。
- extentAfter:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。
- atEdge:是否滑到了可滚动组件的边界(此示例中相当于列表顶或底部)
https://book.flutterchina.club/chapter6/scroll_controller.html