可以使用 ScrollController 进行滚动监听及控制

ScrollController 的构造函数:

  1. ScrollController({
  2. double initialScrollOffset = 0.0, // 初始滚动位置
  3. this.keepScrollOffset = true, // 是否保存滚动位置
  4. ...
  5. })

ScrollController 的相关实例方法与属性:

  • offset 可滚动组件当前的滚动位置。
  • jumpTo(double offset)animateTo(double offset,...) 这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。

滚动监听

ScrollController 间接继承自 Listenable,我们可以根据 ScrollController 来监听滚动事件,如:

  1. controller.addListener(()=>print(controller.offset))

滚动位置恢复

PageStorage 是一个用于保存页面(路由)相关数据的组件,它并不会影响子树的 UI 外观,其实,PageStorage 是一个功能型组件,它拥有一个存储桶(bucket),子树中的 Widget 可以通过指定不同的 PageStorageKey 来存储各自的数据或状态。

每次滚动结束,可滚动组件都会将滚动位置 offset 存储到 PageStorage 中,当可滚动组件重新创建时再恢复。如果 ScrollController.keepScrollOffset 为 false,则滚动位置将不会被存储,可滚动组件重新创建时会使用 ScrollController.initialScrollOffset;ScrollController.keepScrollOffset 为 true 时,可滚动组件在第一次创建时,会滚动到 initialScrollOffset 处,因为这时还没有存储过滚动位置。在接下来的滚动中就会存储、恢复滚动位置,而 initialScrollOffset 会被忽略。

当一个路由中包含多个可滚动组件时,如果你发现在进行一些跳转或切换操作后,滚动位置不能正确恢复,这时你可以通过显式指定 PageStorageKey 来分别跟踪不同的可滚动组件的位置,如:

  1. ListView(key: PageStorageKey(1), ... );
  2. ...
  3. ListView(key: PageStorageKey(2), ... );

不同的 PageStorageKey,需要不同的值,这样才可以为不同可滚动组件保存其滚动位置。

:::info 一个路由中包含多个可滚动组件时,如果要分别跟踪它们的滚动位置,并非一定就得给他们分别提供 PageStorageKey。这是因为 Scrollable 本身是一个 StatefulWidget,它的状态中也会保存当前滚动位置,所以,只要可滚动组件本身没有被从树上 detach 掉,那么其 State 就不会销毁(dispose),滚动位置就不会丢失。只有当 Widget 发生结构变化,导致可滚动组件的 State 销毁或重新构建时才会丢失状态,这种情况就需要显式指定 PageStorageKey,通过 PageStorage 来存储滚动位置,一个典型的场景是在使用 TabBarView 时,在 Tab 发生切换时,Tab 页中的可滚动组件的 State 就会销毁,这时如果想恢复滚动位置就需要指定 PageStorageKey。 :::

ScrollPosition

ScrollPosition 是用来保存可滚动组件的滚动位置的。一个 ScrollController 对象可以同时被多个可滚动组件使用,ScrollController 会为每一个可滚动组件创建一个 ScrollPosition 对象,这些 ScrollPosition 保存在 ScrollController 的 positions 属性中(List<ScrollPosition>)。ScrollPosition 是真正保存滑动位置信息的对象,offset 只是一个便捷属性:

  1. double get offset => position.pixels;

一个 ScrollController 虽然可以对应多个可滚动组件,但是有一些操作,如读取滚动位置 offset,则需要一对一!但是我们仍然可以在一对多的情况下,通过其它方法读取滚动位置,举个例子,假设一个 ScrollController 同时被两个可滚动组件使用,那么我们可以通过如下方式分别读取他们的滚动位置:

  1. ...
  2. controller.positions.elementAt(0).pixels
  3. controller.positions.elementAt(1).pixels
  4. ...

我们可以通过 controller.positions.length 来确定 controller 被几个可滚动组件使用。

ScrollPosition 有两个常用方法:animateTo() 和 jumpTo(),它们是真正来控制跳转滚动位置的方法,ScrollController 的这两个同名方法,内部最终都会调用 ScrollPosition 的。

示例

  1. class ScrollControllerTestRoute extends StatefulWidget {
  2. @override
  3. ScrollControllerTestRouteState createState() {
  4. return new ScrollControllerTestRouteState();
  5. }
  6. }
  7. class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> {
  8. ScrollController _controller = new ScrollController();
  9. bool showToTopBtn = false; // 是否显示“返回到顶部”按钮
  10. @override
  11. void initState() {
  12. // 监听滚动事件,打印滚动位置
  13. _controller.addListener(() {
  14. setState(() {
  15. showToTopBtn = _controller.offset >= 1000;
  16. });
  17. });
  18. }
  19. @override
  20. void dispose() {
  21. // 为了避免内存泄露,需要调用_controller.dispose
  22. _controller.dispose();
  23. super.dispose();
  24. }
  25. @override
  26. Widget build(BuildContext context) {
  27. return Scaffold(
  28. appBar: AppBar(title: Text("滚动控制")),
  29. body: Scrollbar(
  30. child: ListView.builder(
  31. itemCount: 100,
  32. itemExtent: 50.0, // 列表项高度固定时,显式指定高度是一个好习惯(性能消耗小)
  33. controller: _controller,
  34. itemBuilder: (context, index) {
  35. return ListTile(title: Text("$index"),);
  36. }
  37. ),
  38. ),
  39. floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
  40. child: Icon(Icons.arrow_upward),
  41. onPressed: () {
  42. // 返回到顶部时执行动画
  43. _controller.animateTo(.0,
  44. duration: Duration(milliseconds: 200),
  45. curve: Curves.ease
  46. );
  47. }
  48. ),
  49. );
  50. }
  51. }

效果:
013.gif