滚动控制器

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

ScrollController 的构造函数:

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

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

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

滚动监听

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

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

滚动位置恢复

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

每次滚动结束,可滚动组件都会将滚动位置 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 属性中(List<ScrollPosition>)。ScrollPosition 是真正保存滑动位置信息的对象,offset 只是一个便捷属性:

double get offset => position.pixels;

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

...
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
...

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

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

示例

class ScrollControllerTestRoute extends StatefulWidget {
  
  ScrollControllerTestRouteState createState() {
    return new ScrollControllerTestRouteState();
  }
}
class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> {
  ScrollController _controller = new ScrollController();
  bool showToTopBtn = false; // 是否显示“返回到顶部”按钮
  
  void initState() {
    // 监听滚动事件,打印滚动位置
    _controller.addListener(() {
      setState(() {
        showToTopBtn = _controller.offset >= 1000;
      });
    });
  }
  
  void dispose() {
    // 为了避免内存泄露,需要调用_controller.dispose
    _controller.dispose();
    super.dispose();
  }
  
  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
            );
          }
      ),
    );
  }
}

MIT Licensed | Copyright © 2018-present 滇ICP备16006294号

Design by Quanzaiyu | Power by VuePress