滚动操做在 UI 交互设计中是不可或缺的,由于咱们的显示设备大小毕竟有限,没有办法一次性展现全部的信息,因此只有借助于滚动操做,才能够向用户展现更多的内容。markdown
在 Flutter 中,一样有许多滚动相关的 widget,像 SingleChildScrollView
、ListView
和 GridView
等,都是可滚动 widget。它们的使用也很广泛。因此掌握它们的用法是颇有必要的。咱们比较熟悉的滚动相关类应该非 ScrollController
莫属了。那么就从第一位最被人熟知的剑客提及。ide
顾名思义,ScrollController 的做用是控制一个可滚动的控件。一个 ScrollController 能够被用于多个可滚动控件,可是当咱们经过 ScrollController#offset
来获取滚动量时,这时就要求此 ScrollController 只能被一个可滚动控件绑定,不然就会抛出断言异常。动画
咱们首先看一下 ScrollController 的结构。ui
从上图能够看到,它的方法很少,主要功能就是对外暴露位置接口,其次就是能够经过 animateTo()
和 jumpTo()
方法设置可滚动控件滚动到指定位置。那么从它的这两个主要使命出发,咱们看看它都是怎么实现的。this
offset
spa
double get offset => position.pixels;
翻译
offset 是一个 get 方法,真正返回的是 position 的 pixels 成员的值,而这个 position,从上面的结构图里,能够看到它是一个 ScrollPosition 的类型,至于这个 ScrollPosition 类的做用和实现是什么样的,先按下不表,继续往下看。debug
animateTo()
和 jumpTo()
设计
Future<void> animateTo(
double offset, {
@required Duration duration,
@required Curve curve,
}) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
final List<Future<void>> animations = List<Future<void>>(_positions.length);
for (int i = 0; i < _positions.length; i += 1)
animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve);
return Future.wait<void>(animations).then<void>((List<void> _) => null);
}
void jumpTo(double value) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
for (final ScrollPosition position in List<ScrollPosition>.from(_positions))
position.jumpTo(value);
}
复制代码
这两个方法的做用都是滚动到指定的位置,区别就是 animateTo()
方法是伴随着动画滚动到目的位置,而 jumpTo
方法则不伴随动画,直接跳转到目的位置,它们都借助 ScrollPosition 对象来完成。3d
能够看到,上面几个方法的实现中,都是使用了 position 的对应方法。那么与位置相关的数据的管理看来都是由 ScrollPosition 类来完成的了。在深刻了解 ScrollPosition以前,咱们先看看 ScrollPosition 对象是如何建立的。
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition,
) {
return ScrollPositionWithSingleContext(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
复制代码
上面的方法建立了一个 ScrollPositionWithSingleContext 对象,ScrollPositionWithSingleContext 类是 ScrollPosition 的子类,不少的操做都是在它内部实现的。好了,下面咱们就来揭开 ScrollPosition 的神秘面纱。
官方文档对它的描述是:决定滚动视图的哪一部份内容是可见的。从上面 ScrollController 也能够知道,滚动视图的滚动位置等属性以及使滚动视图滚动到指定位置等操做都是由 ScrollPosition 完成。那就先分别看看 ScrollController 里的属性获取和操做方法具体是怎么实现的。
pixels
ScrollController 的 offset 属性获取的就是 ScrollPosition 的 pixels 值,它指向私有变量 _pixels
。
_pixels 的值写入的地方有下面几个:
先跳过这一部分,再看看两外两个操做方法。
jumpTo()
@override
void jumpTo(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
// 设置当前的滚动像素数
// (另外,根据上面的图,这个方法会更新 ScrollPosition 的 pixels,
// 同即 ScroController 的 offset)
forcePixels(value);
// 处理位置跳转前的回调
didStartScroll();
// 处理位置变化回调
didUpdateScrollPositionBy(pixels - oldPixels);
// 处理滚动完成回调
didEndScroll();
}
goBallistic(0.0);
}
复制代码
goIdle()
方法将当前的滚动状态置为闲置。
@override
void goIdle() {
beginActivity(IdleScrollActivity(this));
}
复制代码
至于状态的管理和设置方法,咱们在介绍第三个剑客的时候再接着说。
而 goBallistic()
方法则是处理诸如滚动到最大程度后的回弹等状态。
@override
void goBallistic(double velocity) {
assert(pixels != null);
final Simulation simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
} else {
goIdle();
}
}
复制代码
animateTo()
@override
Future<void> animateTo(
double to, {
@required Duration duration,
@required Curve curve,
}) {
// 若是目的位置和如今的位置接近到一个程度,那么直接跳转到目的位置,而不须要再启动动画过程
if (nearEqual(to, pixels, physics.tolerance.distance)) {
// Skip the animation, go straight to the position as we are already close.
jumpTo(to);
return Future<void>.value();
}
final DrivenScrollActivity activity = DrivenScrollActivity(
this,
from: pixels,
to: to,
duration: duration,
curve: curve,
vsync: context.vsync,
);
beginActivity(activity);
return activity.done;
}
复制代码
上面能够看到位置的变换最终都是经过 beginActivity()
方法保存和更新一系列的滚动状态。这个 ScrollActivity 正是第三位剑客。
照例,咱们先来看看这个类的结构。
经过类的结构,咱们也能够很清晰地看到这个类地基本功能。它有4个成员变量:delegate
、shouldIgnorePointer
、isScrolling
和 velocity
,其中 delegate
是 ScrollActivityDelegate 类型,一个虚拟类,它的子类有以下这些,除了第一个,它们又都是类 ScrollPositionWithSingleContext 的子类,而 ScrollPositionWithSingleContext 类又是 ScrollPosition 的子类,因此 delegate
就是一个 ScrollPosition 对象。shouldIgnorePointer
对象的做用根据文档的翻译一下就是“滚动视图在进行此滚动活动时是否应该忽视触摸事件”。其余两个就很好理解了。
ScrollActivity 是一个抽象类,因此它的真正用法不在于它自己,而在于其子类的定制化,那看看它的子类有哪些。
其中的 IdleScrollActivity 咱们在 ScrollPosition 的 goIdle()
方法中已经见过,它其实模拟的就是一个没有滚动时的滚动活动,用来标识一个滚动视图闲置的状态。再好比,DrivenScrollActivity(ScrollPosition 的 animateTo()
方法建立的就是这个 ScrollActivity),模拟一种动画驱动的滚动状态;HoldScrollActivity 模拟的是没有作任何工做,释放掉后就可转换为 IdelScrollActivity,一般模拟滚动视图已经被触摸但还未被拖拽的状态,等。
三剑客大体介绍这么多,它们基本是 ScrollController -> ScrollPosition -> ScrollActivity 的一个建立过程,而且在建立过程当中持有对方的对象实例。ScrollController 更主要的功能是对用户暴露接口,主要的工做由 ScrollPosition 搭配 ScrollActivity 完成。
梳理了一下这三个滚动相关的重要机制后,咱们能够发现,flutter 实际上是把滚动过程给抽象成了不一样的部分,这三个类只是对滚动过程的一种抽象,真正的滚动和滚动过程当中的视图更新和绘制并不禁它们负责。