开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 28 天,点击查看活动详情
ScrollController
构造函数
ScrollController({
double initialScrollOffset = 0.0, //初始滚动位置
this.keepScrollOffset = true,//是否保存滚动位置
...
})
- offset:可滚动组件当前滚动位置
- jumpTo(double offset)、animateTo(double offset,...):两个方法跳转到指定的位置,不同的是后者在执行时会有动画效果。
滚动监听
ScrollController间接继承自Listenable。
controller.addListener(()=>print(controller.offset))
实例:创建一个ListView,当位置发生变化时,判断当前位置是否超过1000像素,超过则显示返回顶部按钮,没超过1000像素则隐藏按钮。
class ScrollControllerTestRoute extends StatefulWidget {
@override
ScrollControllerTestRouteState createState() {
return ScrollControllerTestRouteState();
}
}
class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> {
ScrollController _controller = ScrollController();
bool showToTopBtn = false; //是否显示“返回到顶部”按钮
@override
void initState() {
super.initState();
//监听滚动事件,打印滚动位置
_controller.addListener(() {
print(_controller.offset); //打印滚动位置
if (_controller.offset < 1000 && showToTopBtn) {
setState(() {
showToTopBtn = false;
});
} else if (_controller.offset >= 1000 && showToTopBtn == false) {
setState(() {
showToTopBtn = true;
});
}
});
}
@override
void dispose() {
//为了避免内存泄露,需要调用_controller.dispose
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("滚动控制")),
body: Scrollbar(
child: ListView.builder(
itemCount: 100,
itemExtent: 50.0, //列表项高度固定时,显式指定高度是一个好习惯(性能消耗小)
controller: _controller,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"),);
}
),
),
floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//返回到顶部时执行动画
_controller.animateTo(
.0,
duration: Duration(milliseconds: 200),
curve: Curves.ease,//动画类型
);
}
),
);
}
}
滚动位置恢复
PageStorage是一个用于保存页面相关数据的组件,它不会影响子树UI的外观,PageStorage是一个功能型组件,它拥有一个存储桶(bucket),子树中的Widget可以通过指定不同的PageStorageKey来存储各自的数据或状态。
每次滚动结束,可滚动组件都会将滚动位置offset存储到PageStorage中,当可滚动组件重新创建时再恢复,如果ScrollContriller.keepScrollOffset为false,则滚动位置将不会被存储,可滚动组件重新创建时会使用initialScrollOffset处,因为这是还没有存储过滚动位置。在接下来的滚动中会存储,恢复滚动位置,而initialScrollOffset会被忽略。
当一个路由中包含多个可滚动组件时,在进行一些跳转或切换操作后,滚动位置不能正确恢复,此时可通过显示指定PageStorageKey来分别跟踪不同的可滚动组件的位置。
ListView(key: PageStorageKey(1), ... );
...
ListView(key: PageStorageKey(2), ... );
不同的PageStorageKey,需要不同的值,这样才可以为不同可滚动组件保存其滚动位置。
注意:一个路由中包含多个可滚动组件时,如果要分别跟踪他们的滚动位置,并非一定就的给他们分别提供PageStoraageKey。这是因为Scrollable本身是一个StatefulWidget,它的状态中也会保存当前的滚动位置,所以,只要可滚动组件本身没有被从树上detach掉,那么其State就不会销毁(dispose),滚动位置就不会丢失。只有当Widget发生结构变化,到纸可滚动组件的State销毁或者重新构建时才会丢失状态,这种情况就需要来显示的指定PageStorageKey,通过PageStorage来存储滚动位置。典型的场景是在使用TabBarView时,在Tab发生切换时,Tab页中可滚动组件的State就会销毁,这时如果想恢复滚动位置就需要指定PageStorageKey。
ScrollPosition
ScrollPosition是用来保存可滚动组件的滚动位置的,一个ScrollController对象可以同时被多个可滚动组件使用,ScrollController会为每一个可滚动组件创建一个ScrollPosition对象,这些ScrollPosition保存在ScrollController的position属性中(List(ScrollPosition))。ScrollPosition是真正保存滑动位置信息的对象,offset只是一个便捷属性,默认取第一个。
double get offset => position.pixels;
一个ScrollController可以对应多个可滚动组件,如果需要一一对应,可通过下面方式分别读取滚动位置:
...
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
...
可以通过controller.positions.length来确定controller被几个可滚动组件使用。
ScrollPosition的方法
ScrollPosition有两个常用方法:animateTo()和jumpTo(),它们是真正来控制跳转滚动位置的方法,ScrollController的两个同名方法,最终会调用ScrollPosition。
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关联的可滚动组件都滚动到指定位置。
滚动监听
Flutter Widget树中子Widget可以通过发送通知Notification与父Widget通讯。父级组件可以通过NotificationListener组件来监听自己关注的通知,这种通信方式类似于Web开发中浏览器的事件冒泡,iOS中的通知。
可滚动组件在滚动时会发送ScrollNotification类型的通知,scrollBar正是通过监听滚动通知来实现的。通过Notification Listener监听滚动事件和通知ScrollController有两个主要的不同:
- 通过NotificationListener可以在从滚动组件到widget树根之间任意位置都能监听。而ScrollController只能和具体的可滚动组件关联后才可以。
- 收到滚动事件后获得的信息不同:NotificationListener在收到滚动事件时,通知中会携带当前滚动位置和ViewPort的一些信息,而ScrollController只能获得当前滚动位置。
实例:
class ScrollNotificationTestRoute extends StatefulWidget {
@override
_ScrollNotificationTestRouteState createState() =>
_ScrollNotificationTestRouteState();
}
class _ScrollNotificationTestRouteState
extends State<ScrollNotificationTestRoute> {
String _progress = "0%"; //保存进度百分比
@override
Widget build(BuildContext context) {
return Scrollbar(
//进度条
// 监听滚动通知
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
double progress = notification.metrics.pixels /
notification.metrics.maxScrollExtent;
//重新构建
setState(() {
_progress = "${(progress * 100).toInt()}%";
});
print("BottomEdge: ${notification.metrics.extentAfter == 0}");
return false;
//return true; //放开此行注释后,进度条将失效
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (context, index) => ListTile(title: Text("$index")),
),
CircleAvatar(
//显示进度百分比
radius: 30.0,
child: Text(_progress),
backgroundColor: Colors.black54,
)
],
),
),
);
}
}
在接收到滚动事件时,参数类型为ScrollNotification,它包含一个metrics属性,它的类型是ScrollMetrics,该属性包含当前ViewPort及滚动位置等信息:
- pixels:当前滚动位置。
- maxScrollExtent:最大可滚动长度
- extentBefore:滑出ViewPort顶部的长度,此实例中相当于顶部滑出屏幕上方的列表长度。
- extentInside:ViewPort内部长度,此实例中屏幕显示的列表部分的长度。
- extentAfter:列表中未滑入ViewPort部分的长度;此实例中列表底部未显示到屏幕范围部分的长度。
- atEdge:是否滑倒了可滚动组件的边界。相当于列表顶或底部。