文章已同步至掘金:https://juejin.cn/post/6844903957358198798
欢迎访问😃,有任何问题都可留言评论哦~
在前面的滚动控件篇的文章中,我们提到了controller
属性,他接收一个ScrollController
对象。ScrollController
的主要作用是控制滚动位置和监听滚动事件。
本章以ListView
为例,展示一下ScrollController
的具体用法
ScrollController
ScrollController
控制滚动位置和监听滚动事件。
源码示例
构造函数如下:
ScrollController({
double initialScrollOffset = 0.0, //初始滚动位置
this.keepScrollOffset = true,//是否保存滚动位置
...
})
属性解释
上面已经表明了具体属性的意思,这里介绍一下ScrollController
常用的属性和方法
offset
该属性表示可滚动组件当前的滚动位置
jumpTo、animateTo
jumpTo(double offset)
、animateTo(double offset,...)
:这两个方法都用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
滚动监听
ScrollController
间接继承自Listenable
,我们可以根据ScrollController
来监听滚动事件,
例如:
controller.addListener(()=>print(controller.offset))
这段代码就可以打印出当前滚动的位置
代码示例:
我们创建一个ListView
,当滚动位置发生变化时,我们先打印出当前滚动位置,然后判断当前位置是否超过1000像素,如果超过则在屏幕右下角显示一个“返回顶部”的按钮,该按钮点击后可以使ListView
恢复到初始位置;如果没有超过1000像素,则隐藏“返回顶部”按钮。
import 'package:flutter/material.dart';
class CategoryPage extends StatefulWidget {
@override
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
ScrollController _controller = new ScrollController();
bool showToTopBtn = false; //是否显示“返回到顶部”按钮
@override
void 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);
}),
);
}
}
由于列表项高度为50像素,当滑动到第20个列表项后,右下角“返回顶部”按钮会显示,点击该按钮,ListView会在返回顶部的过程中执行一个滚动动画,动画时间是200毫秒,动画曲线是Curves.ease
运行效果:
滚动位置恢复
PageStorage
是一个用于保存页面(路由)相关数据的组件,它并不会影响子树的UI外观。
每次滚动结束,可滚动组件都会将滚动位置offset
存储到PageStorage
中,当可滚动组件重新创建时再恢复。
如果ScrollController.keepScrollOffset
为false
,则滚动位置将不会被存储,可滚动组件重新创建时会使用ScrollController.initialScrollOffset
;
当ScrollController.keepScrollOffset
为true
时,可滚动组件在第一次创建时,会滚动到initialScrollOffset
处,因为这时还没有存储过滚动位置。在接下来的滚动中就会存储、恢复滚动位置,而initialScrollOffset
会被忽略。
当一个路由中包含多个可滚动组件时,如果你发现在进行一些跳转或切换操作后,滚动位置不能正确恢复,这时你可以通过显式指定PageStorageKey
来分别跟踪不同的可滚动组件的位置,
如:
ListView(key: PageStorageKey(1), ... );
...
ListView(key: PageStorageKey(2), ... );
不同的PageStorageKey
,需要不同的值,这样才可以为不同可滚动组件保存其滚动位置。
注意:一个路由中包含多个可滚动组件时,如果要分别跟踪它们的滚动位置,并非一定就得给他们分别提供
PageStorageKey
。这是因为Scrollable本身是一个StatefulWidget,它的状态中也会保存当前滚动位置,所以,只要可滚动组件本身没有被从树上detach
掉,那么其State就不会销毁(dispose),滚动位置就不会丢失。只有当Widget发生结构变化,导致可滚动组件的State销毁或重新构建时才会丢失状态,这种情况就需要显式指定PageStorageKey
,通过PageStorage
来存储滚动位置,一个典型的场景是在使用TabBarView
时,在Tab发生切换时,Tab页中的可滚动组件的State就会销毁,这时如果想恢复滚动位置就需要指定PageStorageKey
。
ScrollPosition
ScrollPosition
是用来保存可滚动组件的滚动位置的。
一个ScrollController
对象可以同时被多个可滚动组件使用,ScrollController
会为每一个可滚动组件创建一个ScrollPosition
对象,这些ScrollPosition
保存在ScrollController
的positions
属性中。
ScrollPosition
是真正保存滑动位置信息的对象,offset
只是一个便捷属性:
double get offset => position.pixels;
从上面可以看出offect
是来自于position
的。
一个ScrollController
虽然可以对应多个可滚动组件,但是有一些操作,如读取滚动位置offset
,则需要一对一!
但是我们仍然可以在一对多的情况下,通过其它方法读取滚动位置,
举个例子,假设一个ScrollController
同时被两个可滚动组件使用,那么我们可以通过如下方式分别读取他们的滚动位置:
...
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
...
我们可以通过controller.positions.length
来确定controller
被几个可滚动组件使用。
ScrollPosition
有两个常用方法:animateTo()
和 jumpTo()
,它们是真正来控制跳转滚动位置的方法,ScrollController
的这两个同名方法,内部最终都会调用ScrollPosition
的。
需要注意的是,
ScrollController
的animateTo()
和jumpTo()
内部会调用所有ScrollPosition
的animateTo()
和jumpTo()
,以实现所有和该ScrollController
关联的可滚动组件都滚动到指定的位置。
滚动监听示例
下面,我们监听ListView
的滚动通知,然后显示当前滚动进度百分比:
import 'package:flutter/material.dart';
class ScrollNotificationTestRoute extends StatefulWidget {
@override
_ScrollNotificationTestRouteState createState() =>
new _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 true; //放开此行注释后,进度条将失效
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"));
}
),
CircleAvatar( //显示进度百分比
radius: 30.0,
child: Text(_progress),
backgroundColor: Colors.black54,
)
],
),
),
);
}
}
运行效果:
Flutter Widget树中子Widget可以通过发送通知(Notification)与父(包括祖先)Widget通信。父级组件可以通过NotificationListener
组件来监听自己关注的通知,这种通信方式类似于Web开发中浏览器的事件冒泡。
可滚动组件在滚动时会发送ScrollNotification
类型的通知,ScrollBar
正是通过监听滚动通知来实现的。通过NotificationListener
监听滚动事件和通过ScrollController
有两个主要的不同:
-
通过
NotificationListener
可以在从可滚动组件到widget树根之间任意位置都能监听。而ScrollController
只能和具体的可滚动组件关联后才可以。 -
收到滚动事件后获得的信息不同;
NotificationListener
在收到滚动事件时,通知中会携带当前滚动位置和ViewPort
的一些信息,而ScrollController
只能获取当前滚动位置。
在接收到滚动事件时,参数类型为ScrollNotification
,它包括一个metrics
属性,它的类型是ScrollMetrics
,该属性包含当前ViewPort
及滚动位置等信息:
pixels
:当前滚动位置。maxScrollExtent
:最大可滚动长度。extentBefore
:滑出ViewPort顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。extentInside
:ViewPort内部长度;此示例中屏幕显示的列表部分的长度。extentAfter
:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。atEdge
:是否滑到了可滚动组件的边界(此示例中相当于列表顶或底部)。
评论区