Flutter 滚动监听
ScrollPosition和ScrollController
我们知道,在使用ListView
等可滚动组件时,可以通过给它传进去一个ScrollController
来控制滚动行为,那么ScrollController
是怎么做到的呢?实际上ScrollController
内部会在每一次ScrollController
和一个可滚动组件绑定时为其创建一个ScrollPosition
对象,并放在一个数组中进行统一管理,真正对滚动起控制作用的其实是ScrollPosition
,例如,当我们调用ScrollController
的animateTo
方法的时候,实际上是这样的:
1 | // ScrollController的animateTo方法(伪实现) |
没错,当有多个可滚动组件被绑定到同一个ScrollController
时,animateTo
(jumpTo
方法同理)会遍历当前所有的ScrollPosition
对象,并依次调用它们的animateTo
方法
ScrollController处理多个绑定时的逻辑
在绑定了多个可滚动组件时,ScrollController
的某些属性直接使用会直接报异常,假设_controller
绑定了多个可滚动组件:
1 | debugPrint("${_controller.offset}"); //报异常 |
为什么报异常其实很好理解,当我们绑定了多个可滚动组件时,每个可滚动组件的offset
和position
都不相同,那么你直接访问这两个属性谁知道是代表的哪个可滚动组件的信息呢,所以此处报异常很合理,可以看看报的具体内容:
可以看到,在ScrollController
的内部有一个断言: _positions.length == 1
,当我们绑定了多个可滚动组件时,这个断言是不成立的,自然就会报异常了
对于多个可滚动组件,我们只能使用ScrollController
的positions
属性(注意比前面多了一个s),这是一个数组,里面存放的就是所有可滚动组件对应的各自的ScrollPosition
对象,我们可以通过遍历等方式来获取信息或者批量处理多个可滚动组件。
监听滚动事件
监听滚动事件的办法有很多,我们依次介绍
方法一: 通过ScrollController来监听
我们查看ScrollController
的实现就可以知道,它是间接继承自Listenable
的,所以我们可以直接使用addListener
来进行滚动事件的监听:
1 | _controller.addListener((){ |
这种监听方式的特点是,从滚动开始一直到滚动结束这个过程中,你通过addListener
添加的回调函数不间断地多次调用(比如你若在里面使用了打印函数,控制台在一瞬间会打印出很多行内容),能让你实现精细化的滚动控制,但如果你在里面执行开销较大的操作时,会影响性能,例如setState
导致多个组件被重绘。
方法二: 通过NotificationListener组件来监听
我们先来了解一下什么是NotificationListener
。在Flutter中,子组件可以向父组件发送通知,而父组件可以选择监听自己关心的通知,这里的父组件范围是很广的,在子组件到根组件之间的所有组件其实都可以对该子组件发出的通知进行监听。通知的机制有点像Web开发中的事件冒泡,我们在NotificationListener
的回调函数中可以通过返回值来决定当前通知是否继续向更上一级的组件进行传播,这让我们获得了很强大的事件管理能力。而我们在此处要监听的通知就是: ScrollNotification
。
示例如下:
1 | NotificationListener<ScrollNotification>( |
我们在onNotification
中即可进行事件的处理,注意该函数返回false
表示事件应当继续冒泡,返回true
则表示事件已经处理完毕,无需再通知更上一级的组件。其实在上述代码中,我们无需让ListView
作为NotificationListener
的直接子组件,只需要NotificationListener
在ListView
的上层即可。我们可以在onNotification这个回调函数中,通过notification变量获取到滚动信息,包括Viewport的信息:
1 | NotificationListener<ScrollNotification>( |
这种监听方法也会在滚动时不间断地调用你提供的回调函数
这种监听方法的好处是,我们可以在ScrollController
无法访问的情况下获取到滚动的详细信息,例如我们想要写一个单独的组件来控制子组件的滚动行为的时候,下面要介绍的ScrollBar
就是通过这种方式来实现的。
ScrollBar
这个组件可以作为可滚动组件的子组件以显示一个滚动条,它内部是通过监听子组件的滚动通知来实现的,基础用法如下:
1 | ScrollBar( |
请思考下面的代码:
1 | ScrollBar( |
在这个例子中,ScrollBar
会直接失效,这是因为我们在onNotification
中返回了true
,这表示滚动事件不再向上冒泡,那么ScrollBar
也就接收不到滚动事件,自然就失效了
方法三: 通过ScrollPosition的isScrollingNotifier来监听
isScrollingNotifier也是一个Listenable,所以我们也可以给他添加监听器:
1 | _controller = ScrollController( |
这种方法和前面两种不同的是,它不会在滚动期间持续调用回调函数,而是只在滚动开始和滚动结束时调用(可滚动组件在静止和滚动这两种状态之间切换时调用)
注意一定要在
onAttach
中进行监听器的绑定,因为这种监听方法依赖的是ScrollPosition
对象,这个对象在ScrollController
被关联到一个可滚动组件时才会创建,在绑定之前position
实际上是空的!而onAttach
方法就是在position
对象创建后被调用的,所以很适合在其中进行监听器的绑定。同时别忘了在onDetach
中移除监听处理方法,防止内存泄漏
滚动事件中的常用信息
这些信息的获取方法有两种,一种是通过ScrollNotification
来获取: notification.metrics.xxx
; 还可以直接通过ScrollController
来获取: _controller.position.xxx
,这两种方式中的xxx
属性名都是一样的
pixels
: 当前滚动的位置maxScrollExtent
: 最大滚动高度extentBefore
: 滑出Viewport顶部的长度extentAfter
: 滑出Viewport底部的长度extentInside
: Viewport的长度,即显示在屏幕上的长度extentTotal
:extentBefore
、extentAfter
、extentInside
这三者的和atEdge
: 是否滑到了可滚动组件的边界(顶部or底部)