40、Flutter之滚动监听及控制

ScrollController


ScrollController构造函数如下:

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

我们介绍一下ScrollController常用的属性和方法:

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

ScrollController还有一些属性和方法,我们将在后面原理部分解释。

滚动监听

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

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

示例

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

import 'package:demo202112/utils/common_appbar.dart';
import 'package:flutter/material.dart';

/// @Author wywinstonwy
/// @Date 2022/1/19 10:46 下午
/// @Description:

class MyScrollController extends StatefulWidget {
  const MyScrollController({Key? key}) : super(key: key);

  @override
  _MyScrollControllerState createState() => _MyScrollControllerState();
}

class _MyScrollControllerState extends State {
  final ScrollController _controller = ScrollController();
  bool showToTopBtn = false; //是否显示“返回到顶部”按钮

  @override
  void initState() {
    // TODO: implement 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: getAppBar('滚动监听以及控制'),
      body: _buildScollbar(),
      floatingActionButton: showToTopBtn==false?null:FloatingActionButton(
        onPressed: (){
          //返回到顶部时候执行动画
          _controller.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeIn);
        },
        child: const Icon(Icons.arrow_upward),),
    );
  }
  _buildScollbar(){
    return Scrollbar(
        child: ListView.builder(
          controller: _controller,
            itemCount: 100,
            itemExtent: 44,
            itemBuilder: (context,index){
          return ListTile(title: Text('$index'),);
        })
    );
  }
}

 运行效果:40、Flutter之滚动监听及控制_第1张图片40、Flutter之滚动监听及控制_第2张图片

 由于列表项高度为 50 像素,当滑动到第 20 个列表项后,右下角 “返回顶部” 按钮会显示,点击该按钮,ListView 会在返回顶部的过程中执行一个滚动动画,动画时间是 200 毫秒,动画曲线是 Curves.ease

滚动位置恢复

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

每次滚动结束,可滚动组件都会将滚动位置offset存储到PageStorage中,当可滚动组件重新创建时再恢复。如果ScrollController.keepScrollOffsetfalse,则滚动位置将不会被存储,可滚动组件重新创建时会使用ScrollController.initialScrollOffsetScrollController.keepScrollOffsettrue时,可滚动组件在第一次创建时,会滚动到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保存在ScrollControllerpositions属性中(List)。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的方法

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

ScrollController控制原理

我们来介绍一下ScrollController的另外三个方法:

ScrollPosition createScrollPosition(
    ScrollPhysics physics,
    ScrollContext context,
    ScrollPosition oldPosition);
void attach(ScrollPosition position) ;
void detach(ScrollPosition position) ;

ScrollController和可滚动组件关联时,可滚动组件首先会调用ScrollControllercreateScrollPosition()方法来创建一个ScrollPosition来存储滚动位置信息,接着,可滚动组件会调用attach()方法,将创建的ScrollPosition添加到ScrollControllerpositions属性中,这一步称为“注册位置”,只有注册后animateTo()jumpTo()才可以被调用。

当可滚动组件销毁时,会调用ScrollControllerdetach()方法,将其ScrollPosition对象从ScrollControllerpositions属性中移除,这一步称为“注销位置”,注销后animateTo()jumpTo() 将不能再被调用。

需要注意的是,ScrollControlleranimateTo()jumpTo()内部会调用所有ScrollPositionanimateTo()jumpTo(),以实现所有和该ScrollController关联的可滚动组件都滚动到指定的位置。

滚动监听


下面,我们监听ListView的滚动通知,然后显示当前滚动进度百分比:

import 'package:demo202112/utils/common_appbar.dart';
import 'package:flutter/material.dart';

/// @Author wywinstonwy
/// @Date 2022/1/19 11:21 下午
/// @Description: 

class MyScrollcontroller2 extends StatefulWidget {
  const MyScrollcontroller2({Key? key}) : super(key: key);

  @override
  _MyScrollcontroller2State createState() => _MyScrollcontroller2State();
}

class _MyScrollcontroller2State extends State {
  String _progress ='0%';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar("滚动监听"),
      body: NotificationListener(
        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: [
            ListView.builder(
              itemCount: 100,
                itemExtent: 50,
                itemBuilder: (context,index){
                return ListTile(title: Text('$index'),);

                }),
            CircleAvatar(
              radius: 30,
              child: Text(_progress),
              backgroundColor: Colors.black54,
            )
          ],
        ),
      ),
    );
  }
}

运行结果如图:40、Flutter之滚动监听及控制_第3张图片

 

在接收到滚动事件时,参数类型为ScrollNotification,它包括一个metrics属性,它的类型是ScrollMetrics,该属性包含当前ViewPort及滚动位置等信息:

  • pixels:当前滚动位置。
  • maxScrollExtent:最大可滚动长度。
  • extentBefore:滑出ViewPort顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。
  • extentInside:ViewPort内部长度;此示例中屏幕显示的列表部分的长度。
  • extentAfter:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。
  • atEdge:是否滑到了可滚动组件的边界(此示例中相当于列表顶或底部)。

ScrollMetrics还有一些其它属性,读者可以自行查阅API文档。

demo完整代码:flutter_demo: flutter组件测试学习demo

你可能感兴趣的:(Flutter,flutter,ios,android)