Flutter 之 滚动监听及控制(十九)

在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。

ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。

1. 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像素,则隐藏“返回顶部”按钮。


class MSHomePage extends StatefulWidget {
  @override
  State createState() => _MSHomePageState();
}

class _MSHomePageState extends State {
  ScrollController scr = ScrollController();
  bool _showTopBtn = false; //是否显示“返回到顶部”按钮
  @override
  void initState() {
    super.initState();
    // 监听滚动位置,并打印
    scr.addListener(() {
      print(scr.offset);

      bool _curShowBtnState = false;
      if (scr.offset < 1000) {
        _curShowBtnState = false;
      } else {
        _curShowBtnState = true;
      }

      // 只有showTopBtn的值发生变化时,才刷新
      if (_curShowBtnState != _showTopBtn) {
        _showTopBtn = _curShowBtnState;
        setState(() {});
      }
    });
  }

  @override
  void dispose() {
    //为了避免内存泄露,需要调用src.dispose
    scr.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("滚动组件"),
      ),
      // Scrollbar 滚动条
      body: Scrollbar(
        controller: scr,
        child: ListView.builder(
          controller: scr,
          itemBuilder: (BuildContext ctx, int index) {
            return ListTile(
              leading: Text("$index"),
            );
          },
          itemCount: 100,
          itemExtent: 50, // 列表项 固定高度
        ),
      ),
      floatingActionButton: _showTopBtn
          ? FloatingActionButton(
              onPressed: () {
                // 返回到顶部时执行动画
                scr.animateTo(0,
                    duration: Duration(microseconds: 200),
                    curve: Curves.easeIn);
              },
              child: Icon(Icons.arrow_upward),
            )
          : null,
    );
  }
}

2.gif

ScrollPosition

ScrollPosition是用来保存可滚动组件的滚动位置的。一个ScrollController对象可以同时被多个可滚动组件使用,ScrollController会为每一个可滚动组件创建一个ScrollPosition对象,这些ScrollPosition保存在ScrollController的positions属性中(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关联的可滚动组件都滚动到指定的位置。

2. NotificationListener

Flutter Widget树中子Widget可以通过发送通知(Notification)与父(包括祖先)Widget通信。父级组件可以通过NotificationListener组件来监听自己关注的通知

可滚动组件在滚动时会发送ScrollNotification类型的通知,ScrollBar正是通过监听滚动通知来实现的。通过NotificationListener监听滚动事件和通过ScrollController有两个主要的不同:

  1. 通过NotificationListener可以在从可滚动组件到widget树根之间任意位置都能监听。而ScrollController只能和具体的可滚动组件关联后才可以。
  2. 收到滚动事件后获得的信息不同;NotificationListener在收到滚动事件时,通知中会携带当前滚动位置和ViewPort的一些信息,而ScrollController只能获取当前滚动位置。

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


class MSHomePage extends StatefulWidget {
  @override
  State createState() {
    return _MSHomePageState();
  }
}

class _MSHomePageState extends State {
  String _progress = "0%";
  @override
  Widget build(BuildContext context) {
    return NotificationListener(
      onNotification: (ScrollNotification noti) {
        if (noti is ScrollStartNotification) {
          print("开始滚动");
        } else if (noti is ScrollUpdateNotification) {
          print("正在滚动");
        } else if (noti is ScrollEndNotification) {
          print("结束滚动");
        } else {}
        double pro = noti.metrics.pixels / noti.metrics.maxScrollExtent;
        setState(() {
          _progress = "${(pro * 100).toInt()}%";
        });
        return false;
      },
      child: Stack(
        alignment: AlignmentDirectional.center,
        children: [
          ListView.builder(
            itemCount: 100,
            itemExtent: 56,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(
                leading: Icon(Icons.people),
                title: Text("联系人 ${index + 1}"),
                trailing: Icon(Icons.delete),
              );
            },
          ),
          CircleAvatar(
            radius: 30,
            child: Text(_progress),
            backgroundColor: Colors.black54,
          ),
        ],
      ),
    );
  }
}

3.gif

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

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

https://book.flutterchina.club/chapter6/scroll_controller.html

你可能感兴趣的:(Flutter 之 滚动监听及控制(十九))