以ListView为例,展示ScrollController的具体用法。最后,再介绍一下路由切换时如何保存滚动位置。

ScrollController

构造函数:

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

ScrollController常用属性和方法:

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

滚动监听

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

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

示例

我们创建一个ListView,当滚动位置放生变化时,我们先打印当前滚动位置,然后判断当前位置是否超过1000像素,如果草果则在屏幕右下角显示一个“返回顶部”的按钮,该按钮点击后可以使ListView恢复到初始位置;如果没有超过1000像素,则隐藏“返回顶部按钮”。代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class ScrollControllerTestRoute extends StatefulWidget {
  @override
  ScrollControllerTestRouteState createState() {
    return new ScrollControllerTestRouteState();
  }
}

class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> {
  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会在返回顶部过程中执行一个滚动动画,动画时间是200ms,动画曲线是Curves.ease。

滚动位置及恢复

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

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

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

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

不同的PageStorageKey,需要不同的值,这样才可以区分不同Scrollable Widget保存的滚动位置。

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

ScrollPosition

一个ScrollController可以同时被多个Scrollable Widget使用,ScrollController会为每一个Scrollable Widget创建一个ScrollPosition对象,这些ScrollPosition保存在ScrollController的position属性中(list)。ScrollPosition是真正保存滑动位置信息的对象,offset只是一个便捷属性: double get offset => position.pixels` 一个