可滑动视图的父类,ListView,CustomScrollView 和 GridView 都是它的子类,它们经过实现 buildSlivers 函数为 ScrollView 提供子视图,同时将 ScrollController,ScrollPhysics,ViewportBuilder 和 children 等传递给 Scrollable。linux
在 ScrollView 的 build 中,一些 ScrollView 的参数,如 dragStartBehavior,controller 以及 buildSlivers 这些函数,会用来生成一个 Scrollable,它对 ScrollView 的一些东西进行收拢(好比 ScrollView 不一样子类的实现),而后专一于实现滑动这一功能。android
Viewport 负责计算 ScrollView 的大小,通常有两种,ShrinkWrappingViewport 和 Viewport,它们的区别在于 ScrollView 大小的计算方式,Viewport 在 performResize 阶段就能够肯定本身的大小,即父 widget 提供的最大空间,而 ShrinkWrappingViewport 要在 performLayout 阶段才能肯定,由于它的大小依赖于本身的子 widget,须要先统计子 view 的大小,再肯定自身的大小。ios
因此,当咱们使用 ScrollView 时,通常咱们须要给它必定有限大小的 constraints,它才能正确计算本身的大小,当咱们没法提供这样一个环境,就能够将它的 shrinkWrap 设置为 true,这样它会给本身计算一个合适的大小。spring
这是一个 InheritedWidget,它的做用是给 Scrollable 传递 ScrollBehavior,而它的肯定,在很早以前就肯定了,且通常一个 app 只有一个(本身单独声明使用的另算),好比在 _CupertinoAppState 中:windows
Widget build(BuildContext context) {
final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
return ScrollConfiguration(
behavior: _AlwaysCupertinoScrollBehavior(),
child: CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.base,
child: CupertinoTheme(
data: effectiveThemeData,
child: HeroControllerScope(
controller: _heroController,
child: Builder(
builder: _buildWidgetApp,
),
),
),
),
);
}
复制代码
给整个 app 指定了一个全局的 ScrollBehavior _AlwaysCupertinoScrollBehavior,这个会在 Scrollable 执行滑动的时候用到。markdown
ScrollBehavior 自己在 flutter 的设计中是一个平台相关的 Widget,它会根据当前的平台,选择一个合适的 ScrollPhysics,以下:app
ScrollPhysics getScrollPhysics(BuildContext context) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return _bouncingPhysics;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return _clampingPhysics;
}
}
复制代码
而 ScrollPhysics 的定位,能够从名字上理解,控制滑动过程的物理特性,定义了如当滑动到顶部的时候的表现、滑过头了以后的回弹方式等。less
ScrollView 中 build 主要返回的 widget,一个通用的滑动模型,它是滑动功能的载体,与 ScrollController、ScrollPhysics 一块儿实现了一个可滑动的控件。而它也只算是一个载体,一个中介,它最主要的做用,就是利用身为 widget 的优点,从整个视图体系中拿到触摸事件,而剩下的功能,交给其余人就好。ide
Viewport 负责决定滑动视图的大小,ScrollPosition 决定滑动的位置,ScrollPhysics 决定滑动的物理属性,ScrollController 能够支持外部使用者控制滑动过程。还有其余的一些,好比 ScrollActivity 表示了滑动过程当中的某一阶段等。在这样一个体系中,ScrollPosition 更像是一个 controller,它直接从 Scrollable 中拿到未处理的触摸事件,根据事件类型计算出本身当前的状态。函数
Scrollable 对应的 state 为 ScrollableState,在它的 build 中,返回的 child 中,有 Listener、RawGestureDetector 和 Viewport 等,Listener 用于监听 PointerScrollEvent 事件,通常这个事件应该是在滚动滑动条时触发的,此时它会计算出一个滑动位置,并直接调用 ScrollPosition.jumpTo 滚动到对应位置。
而 RawGestureDetector 会监听一些滑动手势,好比 dragDown、dragStart 等,ScrollPosition 根据这些手势信息更新状态,计算滑动。
Viewport 就是在 ScrollView 中生成的。
ScrollController 能够从 ScrollView 中设置,并一路传递到 Scrollable 中,它虽然名为 controller,但并非一个 center controller,而是一个 user controller,即给用户提供控制滑动状态的一种途径,但自己在滑动体系中做用不大,只是一个将用户的命令传达到 ScrollPosition 中的角色,不过它还有一点权利,就是可能决定建立的 ScrollPosition,固然这个最终仍是将权利传递到外部用户的手上而已。
它有诸如 adjumpTo、animateTo 等函数,经过调用 position 的同名函数实现。另外,ScrollController 能够绑定多个 ScrollPosition,能够据此实现多个视图同步滑动的能力。
ScrollPosition 承担着滑动过程当中的主要责任,上承 ScrollController,Scrollable,下启 ScrollActivity、ScrollPhysics 等。
首先,ScrollPosition 是 ViewportOffset 的一个子类,这是一个 widget 向概念,它表示的是 Viewport 这个 widget 的偏移量,因为 Viewport 自己就是用于承载滑动视图的 widget,在不少状况下,它的 children 的总体长度要大于它自身,因此就须要有一个 offset 属性,控制当前应该显示的内容。另外,ViewportOffset 中也经过 applyViewportDimension 等函数,接收来自 widget 的信息,及时根据当前的布局,更改显示内容。
其次,ScrollPosition 做为接收触摸事件者,它还完成了对触摸事件的分发功能,以及进一步,将处理过的触摸事件转换成视图滑动(其中有一些复杂操做,好比须要考虑视图滑动范围、滑动物理属性等),最终视图更新。
举个例子,当一个滑动事件发生时,它会建立一个Drag 处理后续的滑动事件,Drag 后续对原始的滑动事件进行第一次加工以后,再给到 ScrollPosition,而后 ScrollPosition 还会再将这个数据拿给 ScrollPhysics 进行一些相似边界问题的判断,完了以后,将最终结果给到 ViewportOffset 的 pixels 属性,最后通知 Viewport 进行从新 layout,由此完成一次滑动。更具体的流程,在最后详细说明。
ScrollPhysic 描述的是一个滑动视图,也就是 Viewport 的内容,在执行滑动过程当中的一些物理属性,好比是否能够 overscroll,在一个给定的 ScrollMetrics 和理论偏移值下计算一个实际的偏移值等。再看下它的一些成员变量:
spring,SpringDescription,描述了滑动的一些物理特性,会在建立 Simulation 时传递过去
tolerance,Tolerance,定义了一些可忽略的距离、速度、时间等
flingDistance,定义了最小的可被认定为 fling 手势的距离
flingVelocity,定义了最小的可被认定为 fling 手势的速度,和最大的 fling 速度
dragStartDistanceMotionThreshold,定义了开始滑动时,可被认定为是滑动手势的最小距离
allowImplicitScrolling,这是一个来自 ViewportOffset 的变量
ScrollActivity 能够表示滑动过程当中的一个阶段,只是记录了当前的状态,好比是不是滑动中、当前的滑动速度等。它的几个基本参数分别为:
它大体能够分为两种类型,滑动和不滑动。
其中表示不滑动的有两个 ScrollActivity,HoldScrollActivity 和 IdleScrollActivity。
HoldScrollActivity 会在手指按下的瞬间生成,它有表示一种蓄势待发的状态,是为了下一刻的滑动,因此在启动 HoldScrollActivity 的时候,会保存下来当前的滑动速度,而后在开始滑动时,会在一个初始速度上接着滑动。
ScrollHoldController hold(VoidCallback holdCancelCallback) {
final double previousVelocity = activity!.velocity;
// ...
}
复制代码
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
final ScrollDragController drag = ScrollDragController(
delegate: this,
details: details,
onDragCanceled: dragCancelCallback,
carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
);
// ...
}
复制代码
而 IdleScrollActivity 只是表示这是一个静止状态,此时 ScrollPosition 不进行滑动,也基本不处理事件。不过换句话说,ScrollPosition 也只是处理两种事件,在 dragDown 时将状态切换至 HoldScrollActivity,当 dargStart 时,生成 Drag 并将状态切换至 DragScrollActivity,至于 dragUpdate 事件,则是直接交给 Drag 来处理的。
表示滑动状态的 ScrollActivity 有三种,分别是事件驱动、速度驱动和动画驱动。
所谓事件驱动,就是滑动过程是根据外部传进来的滑动事件,来决定是否以及如何更新视图。这个就是在基本的滑动过程当中,ScrollPosition 接收到 dragStart 事件时,进入的滑动状态,与之关联的 ScrollDragController,它会被传递回 Scrollable,在 dragUpdate 事件到来时直接处理事件。
void _handleDragStart(DragStartDetails details) {
// It's possible for _hold to become null between _handleDragDown and
// _handleDragStart, for example if some user code calls jumpTo or otherwise
// triggers a new activity to begin.
assert(_drag == null);
_drag = position.drag(details, _disposeDrag);
assert(_drag != null);
assert(_hold == null);
}
复制代码
void _handleDragUpdate(DragUpdateDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_drag?.update(details);
}
复制代码
当 drag 系列事件结束后,会留下一个滑动速度,此时滑动并不会中止,而是在基于这个速度下,作减速滑动,直到速度为 0,或者滑动到边界,这个阶段,对应的就是 BallisticScrollActivity。
当咱们直接经过 ScrollController 控制 Scrollable 进行滑动时,通常就是调用 animateTo,会建立一个 DrivenScrollActivity,根据当前给出的 duration、curve 等,建立一个动画并执行。
在 BallisticScrollActivity 执行过程当中,用于决定滑动位置的就是 Simulation,
void goBallistic(double velocity) {
assert(hasPixels);
final Simulation? simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
} else {
goIdle();
}
}
复制代码
Simulation 由 ScrollPhysics 建立,在必定程度上是平台相关的,自己也算是 ScrollPhysics 功能组成的一部分,主要是用于控制拖拽滑动结束后的过程,好比在 ios 中默认使用的 BouncingScrollPhysics 会建立一个 BouncingScrollSimulation,建立 BouncingScrollSimulation 的时候,给了它一个初速度、滑动范围等,而后就由它来肯定滑动的距离以及中止的时间。
下面从一次完整的滑动过程再次分析下 flutter 中 Scrollable 的滑动体系,以 ScrollPositionWithSingleContext 和 BouncingScrollPhysics 为例。
首先,当 Scrollable 建立完成以后,它会利用 RawGestureDetector 监听当前的手势操做,主要监听的操做就是 drag 事件相关的,好比 dragDown、dragStart、dragUpdat、dragCancel 等,在这些过程当中,主要涉及的只有 DragScrollActivity 这个。
onDown 用于处理 dragDown 事件,
void _handleDragDown(DragDownDetails details) {
assert(_drag == null);
assert(_hold == null);
_hold = position.hold(_disposeHold);
}
复制代码
很简单,这里只是调用 ScrollPosition 的 hold,建立一个 HoldScrollActivity,如上所介绍的,为下一步的滑动做准备。这里有一点,就是在建立 HoldScrollActivity 的时候,同时传进去了一个 dispose 回调,在这个回调中,会将 _hold 置空,固然这里的考虑并非释放空间这么简单,_hold 自己仍是一种状态,当它不为空的时候,就意味着当前处于 HoldScrollActivity 所管辖的状态,_drag 也是同理,会有 assert 对当前的状态进行判断。
onStart 处理 dragStart 事件,
void _handleDragStart(DragStartDetails details) {
// It's possible for _hold to become null between _handleDragDown and
// _handleDragStart, for example if some user code calls jumpTo or otherwise
// triggers a new activity to begin.
assert(_drag == null);
_drag = position.drag(details, _disposeDrag);
assert(_drag != null);
assert(_hold == null);
}
复制代码
跟 onDown 相似,此时建立了 _drag,在 position.drag 中,
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
final ScrollDragController drag = ScrollDragController(
delegate: this,
details: details,
onDragCanceled: dragCancelCallback,
carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
);
beginActivity(DragScrollActivity(this, drag));
assert(_currentDrag == null);
_currentDrag = drag;
return drag;
}
复制代码
ScrollDragController 会被建立并进入到 DragScrollActivity 状态。在 ScrollPosition 中,每次启用一个 ScrollActivity 时,都是使用 beginActivity 进行状态切换的。
void beginActivity(ScrollActivity? newActivity) {
if (newActivity == null)
return;
bool wasScrolling, oldIgnorePointer;
if (_activity != null) {
oldIgnorePointer = _activity!.shouldIgnorePointer;
wasScrolling = _activity!.isScrolling;
if (wasScrolling && !newActivity.isScrolling)
didEndScroll(); // notifies and then saves the scroll offset
_activity!.dispose();
} else {
oldIgnorePointer = false;
wasScrolling = false;
}
_activity = newActivity;
if (oldIgnorePointer != activity!.shouldIgnorePointer)
context.setIgnorePointer(activity!.shouldIgnorePointer);
isScrollingNotifier.value = activity!.isScrolling;
if (!wasScrolling && _activity!.isScrolling)
didStartScroll();
}
复制代码
一个比较常规的切换逻辑,有一点就是,在 beginActivity 执行时,会判断一下切换先后的滑动状态和是否能够接收事件,并产生相应的通知。
好比 didStartScroll,
void didStartScroll() {
activity!.dispatchScrollStartNotification(copyWith(), context.notificationContext);
}
void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext? context) {
ScrollStartNotification(metrics: metrics, context: context).dispatch(context);
}
void dispatch(BuildContext? target) {
// The `target` may be null if the subtree the notification is supposed to be
// dispatched in is in the process of being disposed.
target?.visitAncestorElements(visitAncestor);
}
bool visitAncestor(Element element) {
if (element is StatelessElement) {
final StatelessWidget widget = element.widget;
if (widget is NotificationListener<Notification>) {
if (widget._dispatch(this, element)) // that function checks the type dynamically
return false;
}
}
return true;
}
复制代码
这个过程会建立一个 ScrollStartNotification,并沿着 widget 树向上传递,经过 visitAncestor,传递给上层第一个接收消费掉此通知的 NotificationListener。(只有当 NotificationListener 的 NotificationListenerCallback 返回 true 才是消费此通知,不然通知会一直向上传递)
onUpdate 会处理 dragUpdate 事件,也就是手指滑动时的事件,此时这个事件会直接交给 ScrollDragController 处理,
void _handleDragUpdate(DragUpdateDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_drag?.update(details);
}
void update(DragUpdateDetails details) {
assert(details.primaryDelta != null);
_lastDetails = details;
double offset = details.primaryDelta!;
if (offset != 0.0) {
_lastNonStationaryTimestamp = details.sourceTimeStamp;
}
// By default, iOS platforms carries momentum and has a start threshold
// (configured in [BouncingScrollPhysics]). The 2 operations below are
// no-ops on Android.
_maybeLoseMomentum(offset, details.sourceTimeStamp);
offset = _adjustForScrollStartThreshold(offset, details.sourceTimeStamp);
if (offset == 0.0) {
return;
}
if (_reversed) // e.g. an AxisDirection.up scrollable
offset = -offset;
delegate.applyUserOffset(offset);
}
复制代码
在 update 中,保存下来的 _lastDetails 是为了在以后发送通知的时候,加上这个滑动事件信息,好比 dispatchScrollUpdateNotification,
void dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {
final dynamic lastDetails = _controller!.lastDetails;
assert(lastDetails is DragUpdateDetails);
ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta, dragDetails: lastDetails as DragUpdateDetails).dispatch(context);
}
复制代码
而后就是一个关因而否损失动量的判断,
void _maybeLoseMomentum(double offset, Duration? timestamp) {
if (_retainMomentum &&
offset == 0.0 &&
(timestamp == null || // If drag event has no timestamp, we lose momentum.
timestamp - _lastNonStationaryTimestamp! > momentumRetainStationaryDurationThreshold)) {
// If pointer is stationary for too long, we lose momentum.
_retainMomentum = false;
}
}
复制代码
这个过程的目的,是为了判断是否损失动量,咱们知道,通常在 ios 的滑动中,连续快速滑动的时候,速度是会积累的,因此后面会越滑越快,而 flutter 为了保持这一特性,就有了动量积累这样一个功能,目前也只在 BouncingScrollPhysics 中才有。关于这个就要从 HoldScrollActivity 开始提及,以前 HoldScrollActivity 有提到,当 dragDown 事件发生时,ScrollPosition 会记录下当前的滑动速度(若是当前还在滑动中),而后在 dragStart 时,将以前的滑动速度传递给 ScrollDragController,不过须要通过 ScrollPhysics 再过滤,而只有 BouncingScrollPhysics 才会提供这个初速度,不过也是通过计算的:
double carriedMomentum(double existingVelocity) {
return existingVelocity.sign *
math.min(0.000816 * math.pow(existingVelocity.abs(), 1.967).toDouble(), 40000.0);
}
复制代码
而后就是在 ScrollDragController 结束时,它会在滑动速度的基础上,再把初速度加上去,构成滑动后的速度。而在滑动后是否加上初速度也是须要判断的,就是经过 _maybeLoseMomentum,若是是滑动太慢或者有悬停的话,就认为这不能进行动量积累,也就不会把初速度再加上去。
而后,_adjustForScrollStartThreshold 会就开始滑动的距离作些处理,大致就是,当本次滑动距离超过某个阈值的时候,才真正开始滑动,不然就看成偏差忽略掉。固然这个逻辑也是能够经过 ScrollPhysics 控制的,就是它的 dragStartDistanceMotionThreshold,目前也只是 BouncingScrollPhysics 才有。
double _adjustForScrollStartThreshold(double offset, Duration? timestamp) {
if (timestamp == null) {
// If we can't track time, we can't apply thresholds.
// May be null for proxied drags like via accessibility.
return offset;
}
if (offset == 0.0) {
if (motionStartDistanceThreshold != null &&
_offsetSinceLastStop == null &&
timestamp - _lastNonStationaryTimestamp! > motionStoppedDurationThreshold) {
// Enforce a new threshold.
_offsetSinceLastStop = 0.0;
}
// Not moving can't break threshold.
return 0.0;
} else {
if (_offsetSinceLastStop == null) {
// Already in motion or no threshold behavior configured such as for
// Android. Allow transparent offset transmission.
return offset;
} else {
_offsetSinceLastStop = _offsetSinceLastStop! + offset;
if (_offsetSinceLastStop!.abs() > motionStartDistanceThreshold!) {
// Threshold broken.
_offsetSinceLastStop = null;
if (offset.abs() > _bigThresholdBreakDistance) {
// This is heuristically a very deliberate fling. Leave the motion
// unaffected.
return offset;
} else {
// This is a normal speed threshold break.
return math.min(
// Ease into the motion when the threshold is initially broken
// to avoid a visible jump.
motionStartDistanceThreshold! / 3.0,
offset.abs(),
) * offset.sign;
}
} else {
return 0.0;
}
}
}
}
复制代码
在这个函数中,大致分为了几种判断标准:
当没有时间信息和 motionStartDistanceThreshold 没有值的时候,这个函数能够认为不工做状态,都是直接返回原滑动距离就完了。主要仍是看 motionStartDistanceThreshold 不为空的状况。
首先,当已经开始滑动,但滑动过程当中有悬停时,_offsetSinceLastStop 会归零,从新开始计算。当开始滑动时,会逐渐积累 _offsetSinceLastStop,这个过程当中不会有实际滑动,直到它大于 motionStartDistanceThreshold 时,阈值到达,此时 _offsetSinceLastStop 置空,开始实际滑动。
不过这还只是从 ScrollDragController 的角度,认为能够滑动的距离,但真正反馈到 Viewport 以前,ScrollPhysics 也要来表现一下,
void applyUserOffset(double delta) {
updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
}
复制代码
首先就是 applyPhysicsToUserOffset,也是只有 BouncingScrollPhysics 有实现,缘由是由于它是一个容许 overscroll 的 ScrollPhysics,在这个函数中,主要是就 overscroll 的状况,经过改变实际移动的距离,添加一种相似“阻力”的概念,即在 overscroll 状态下,实际的滑动距离要小于手势滑动距离。
double setPixels(double newPixels) {
if (newPixels != pixels) {
final double overscroll = applyBoundaryConditions(newPixels);
final double oldPixels = pixels;
_pixels = newPixels - overscroll;
if (_pixels != oldPixels) {
notifyListeners();
didUpdateScrollPositionBy(pixels - oldPixels);
}
if (overscroll != 0.0) {
didOverscrollBy(overscroll);
return overscroll;
}
}
return 0.0;
}
double applyBoundaryConditions(double value) {
final double result = physics.applyBoundaryConditions(this, value);
return result;
}
复制代码
而后在 setPixels 中,继续调用 ScrollPhysics 的 applyBoundaryConditions 判断当前的 overscroll,固然这里的 overscroll 与上面 applyPhysicsToUserOffset 中所说的不太同样,上面说的 overscroll 是指用户所观察到的,若是要用语言简单区分,它们能够是:
因此能够看到,只有 ClampingScrollPhysics 对其有实现,而由于 BouncingScrollPhysics 是始终能够滑动的状态(经过阻力表达滑动到边界),因此它在这里的 overscroll 是始终为 0。
完了以后才能获得真正的须要偏移的数值,此时一次 dragUpdate 完成。
接下来就是 dragEnd,滑动手势结束的时候触发。
void end(DragEndDetails details) {
assert(details.primaryVelocity != null);
// We negate the velocity here because if the touch is moving downwards,
// the scroll has to move upwards. It's the same reason that update()
// above negates the delta before applying it to the scroll offset.
double velocity = -details.primaryVelocity!;
if (_reversed) // e.g. an AxisDirection.up scrollable
velocity = -velocity;
_lastDetails = details;
// Build momentum only if dragging in the same direction.
if (_retainMomentum && velocity.sign == carriedVelocity!.sign)
velocity += carriedVelocity!;
delegate.goBallistic(velocity);
}
复制代码
此时就是计算一下当前的滑动速度,以便后面进入 BallisticScrollActivity 阶段,也就是 goBallistic 调用。
void goBallistic(double velocity) {
assert(hasPixels);
final Simulation? simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
} else {
goIdle();
}
}
BallisticScrollActivity(
ScrollActivityDelegate delegate,
Simulation simulation,
TickerProvider vsync,
) : super(delegate) {
_controller = AnimationController.unbounded(
debugLabel: kDebugMode ? objectRuntimeType(this, 'BallisticScrollActivity') : null,
vsync: vsync,
)
..addListener(_tick)
..animateWith(simulation)
.whenComplete(_end); // won't trigger if we dispose _controller first
}
复制代码
函数自己很简单,首先经过 ScrollPhysics 建立一个 Simulation,而后将其传给 BallisticScrollActivity。从 BallisticScrollActivity 的构造函数能够看到,本质上咱们也能够将其看做是一个由动画驱动的滑动过程,只不过这个动画是根据一个给定的初始速度建立的。
BallisticScrollActivity 与 DrivenScrollActivity 的类似度很高,它们都是在构造函数中先根据提供的信息(simulation,duration、curve等)建立一个 AnimationController,而后监听更新和结束事件,在 _tick 中更新偏移值,在 _end 中结束本身。
当滑动手势结束时,远不意味着整个滑动的结束,为了用户体验,咱们赋予滑动速度的概念,那它的滑动也就有动量,因此中止不能只是戛然而止,须要一个慢慢停下来的过程,因此就有了 BallisticScrollActivity 所表明的减速过程,而这个过程的主要控制者,实际为 ScrollPhysics 所生成的 Simulation,不一样的 Simulation 相距甚远。这里就以较为复杂的 BouncingScrollSimulation 为例说明。
BouncingScrollSimulation({
required double position,
required double velocity,
required this.leadingExtent,
required this.trailingExtent,
required this.spring,
Tolerance tolerance = Tolerance.defaultTolerance,
}) : assert(position != null),
assert(velocity != null),
assert(leadingExtent != null),
assert(trailingExtent != null),
assert(leadingExtent <= trailingExtent),
assert(spring != null),
super(tolerance: tolerance) {
if (position < leadingExtent) {
_springSimulation = _underscrollSimulation(position, velocity);
_springTime = double.negativeInfinity;
} else if (position > trailingExtent) {
_springSimulation = _overscrollSimulation(position, velocity);
_springTime = double.negativeInfinity;
} else {
// Taken from UIScrollView.decelerationRate (.normal = 0.998)
// 0.998^1000 = ~0.135
_frictionSimulation = FrictionSimulation(0.135, position, velocity);
final double finalX = _frictionSimulation.finalX;
if (velocity > 0.0 && finalX > trailingExtent) {
_springTime = _frictionSimulation.timeAtX(trailingExtent);
_springSimulation = _overscrollSimulation(
trailingExtent,
math.min(_frictionSimulation.dx(_springTime), maxSpringTransferVelocity),
);
assert(_springTime.isFinite);
} else if (velocity < 0.0 && finalX < leadingExtent) {
_springTime = _frictionSimulation.timeAtX(leadingExtent);
_springSimulation = _underscrollSimulation(
leadingExtent,
math.min(_frictionSimulation.dx(_springTime), maxSpringTransferVelocity),
);
assert(_springTime.isFinite);
} else {
_springTime = double.infinity;
}
}
assert(_springTime != null);
}
复制代码
首先看它的构造函数,从参数来看,有滑动速度、当前位置、滑动范围和 spring 信息(质量、刚度、摩擦等),而后,在初始化的时候,又分三种状况,underscroll、overscroll 和其余。三种状况对应的三种不一样的滑动方式。
首先,总体来讲,在 BouncingScrollSimulation 中滑动也是分阶段的,由于它对应的 BouncingScrollPhysics 是一个容许 overscroll 的 ScrollPhysics,因此这就致使它的减速过程也变得复杂,须要考虑是不是 overscroll 状态下的减速,以及减速过程当中是否会产生 overscroll。因此在 BouncingScrollSimulation 中,_springSimulation 负责由 overscroll 状态下回滚到边界的过程,_frictionSimulation 才是负责减速过程。
下面就直接看先减速后回弹的状况,首先,是根据当前速度建立 _frictionSimulation 并判断它是否会 overscroll,若是会,就再计算到达边界的时间,而后再约过边界的瞬间,启用 _springSimulation。而 _springTime 就是区分的中界线,
double x(double time) => _simulation(time).x(time - _timeOffset);
Simulation _simulation(double time) {
final Simulation simulation;
if (time > _springTime) {
_timeOffset = _springTime.isFinite ? _springTime : 0.0;
simulation = _springSimulation;
} else {
_timeOffset = 0.0;
simulation = _frictionSimulation;
}
return simulation..tolerance = tolerance;
}
复制代码
对于任何一个函数,它都是须要经过 _simulation 先拿到当前使用的 Simulation 再计算。从这个角度上看,BouncingScrollSimulation 只是一个代理,它的全部实现都是经过 _springSimulation 和 _frictionSimulation 完成的。
直到动画结束,一个完整的滑动过程也基本结束了。
上面介绍的还只是滑动体系的一部分,除此以外,还有更多不一样的 ScrollPhysics,不一样的 ScrollPosition,固然基本的逻辑都是如此。
不一样的 ScrollPhysics 表明着不一样的滑动方式,好比 NeverScrollableScrollPhysics,表示不可滑动,好比 PageScrollPhysics,将滑动固定在页与页之间。又好比 _NestedScrollPosition,专门用于控制 NestedScrollView 中,多层 view 同时滑动的逻辑,好比 _PagePosition,为 PageView 细化了一些滑动规则等等,这些都是基于当前所描述的 ScrollPosition 规则,可是在一些函数上有了新的实现,从而胜任不一样的目标,这些都值得去看。