做爲系列文章的第十八篇,本篇將經過 ScrollPhysics 和 Simulation ,帶你深刻走進 Flutter 的滑動新世界,爲你打開 Flutter 滑動操做的另外一扇窗。android
前文:git
以下圖所示,Flutter 默認的可滑動 Widget
,在 Android 和 iOS 上具有不一樣的 滑動與邊緣拖拽效果 ,這是由於在不一樣平臺上,默認使用了不一樣的 ScrollPhysics
與 Simulation
,後面咱們將逐步介紹這兩大主角的實現原理,最終讓你對 Flutter 世界的滑動拖拽進階到 「隨心所欲」 的境界。github
下方開始高能幹貨,請自帶茶水食用。算法
首先介紹 ScrollPhysics
,在 Flutter 官方的介紹中,ScrollPhysics
的做用是 肯定可滾動控件的物理特性, 常見的有如下四大金剛:spring
BouncingScrollPhysics
:容許滾動超出邊界,但以後內容會反彈回來。ClampingScrollPhysics
: 防止滾動超出邊界,夾住 。AlwaysScrollableScrollPhysics
:始終響應用戶的滾動。NeverScrollableScrollPhysics
:不響應用戶的滾動。在開發過程當中,通常會經過以下代碼進行設置:bash
CustomScrollView(physics: const BouncingScrollPhysics())
ListView.builder(physics: const AlwaysScrollableScrollPhysics())
GridView.count(physics: NeverScrollableScrollPhysics())
複製代碼
但在通常咱們都不會主動去設置 physics
屬性, 那麼默認狀況下,爲何在 Flutter 中的 ListView
、CustomScrollView
等 Scrollable
控件,在 Android 和 iOS 平臺的滾動和邊界拖拽效果,會有以下圖所示的平臺區別呢?app
這裏的關鍵就在於 ScrollConfiguration
和 ScrollBehavior
。less
咱們知道,全部的滑動控件都是經過 Scrollable
進行滑動的。ide
以下代碼所示,在 Scrollable
內的 _updatePosition
方法裏,當 widget.physics == null
時,_physics
默認是從 ScrollConfiguration.of(context)
的 getScrollPhysics(context)
方法獲取 ,而 ScrollConfiguration.of(context)
返回的是一個 ScrollBehavior
對象。佈局
// Only call this from places that will definitely trigger a rebuild.
void _updatePosition() {
_configuration = ScrollConfiguration.of(context);
_physics = _configuration.getScrollPhysics(context);
if (widget.physics != null)
_physics = widget.physics.applyTo(_physics);
final ScrollController controller = widget.controller;
final ScrollPosition oldPosition = position;
if (oldPosition != null) {
controller?.detach(oldPosition);
scheduleMicrotask(oldPosition.dispose);
}
_position = controller?.createScrollPosition(_physics, this, oldPosition)
?? ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition);
assert(position != null);
controller?.attach(position);
}
複製代碼
因此默認狀況下 ,ScrollPhysics
是和 ScrollConfiguration
和 ScrollBehavior
有關係。
那麼 ScrollBehavior
是這麼工做的?
查看 ScrollBehavior
的源碼可知,它的 getScrollPhysics
方法中,默認實現了平臺返回了不一樣的 ScrollPhysics
,因此默認狀況下,在不一樣平臺上的滾動和邊緣推拽,會出現不同的效果:
ScrollPhysics getScrollPhysics(BuildContext context) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
return const BouncingScrollPhysics();
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return const ClampingScrollPhysics();
}
return null;
}
複製代碼
前面說過, ScrollPhysics
是肯定可滾動控件的物理特性 ,那麼如前圖所示,Android 平臺上拖拽溢出的藍色半圓的怎麼來的?ScrollConfiguration
的 ScrollBehavior
是在何時被設置的?
查看 ScrollConfiguration
的源碼咱們得知, ScrollConfiguration
和 Theme
、Localizations
等同樣是 InheritedWidget
,那麼它應該是從上層往下共享的。
因此查看 MaterialApp
的源碼,獲得以下代碼,能夠看到 ScrollConfiguration
是在 MaterialApp
內默認嵌套的,而且經過 _MaterialScrollBehavior
設置了 ScrollBehavior
, 其 override 的buildViewportChrome
方法,就是實現了Android 上溢出拖拽的半圓效果, 其中 GlowingOverscrollIndicator
就是半圓效果的繪製控件。
@override
Widget build(BuildContext context) {
····
return ScrollConfiguration(
behavior: _MaterialScrollBehavior(),
child: result,
);
}
class _MaterialScrollBehavior extends ScrollBehavior {
@override
TargetPlatform getPlatform(BuildContext context) {
return Theme.of(context).platform;
}
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
return child;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
color: Theme.of(context).accentColor,
);
}
return null;
}
}
複製代碼
到這裏咱們就知道了,在默認狀況下可滑動控件的 ScrollPhysics
是如何配置的:
ScrollConfiguration
是一個 InheritedWidget
。MaterialApp
內部利用 ScrollConfiguration
並共享了一個 ScrollBehavior
的子類 _MaterialScrollBehavior
。ScrollBehavior
默認根據平臺返回了特定的 BouncingScrollPhysics
和 ClampingScrollPhysics
效果。_MaterialScrollBehavior
中針對 Android 平臺實現了 buildViewportChrome
的藍色半球拖拽溢出效果。ps :咱們能夠經過實現本身的
ScrollBehavior
, 實現自定義的拖拽溢出效果。
那麼 ScrollPhysics
是怎麼實現滾動和邊緣拖拽的呢? ScrollPhysics
默認是沒有什麼代碼邏輯的,它的主要定義方法以下所示:
/// [position] 當前的位置, [offset] 用戶拖拽距離
/// 將用戶拖拽距離 offset 轉爲須要移動的 pixels
double applyPhysicsToUserOffset(ScrollMetrics position, double offset)
/// 返回 overscroll ,若是返回 0 ,overscroll 就一直是0
/// 返回邊界條件
double applyBoundaryConditions(ScrollMetrics position, double value)
///建立一個滾動的模擬器
Simulation createBallisticSimulation(ScrollMetrics position, double velocity)
///最小滾動數據
double get minFlingVelocity
///傳輸動量,返回重複滾動時的速度
double carriedMomentum(double existingVelocity)
///最小的開始拖拽距離
double get dragStartDistanceMotionThreshold
///滾動模擬的公差
///指定距離、持續時間和速度差應視爲平等的差別的結構。
Tolerance get tolerance
複製代碼
上方代碼標註了 ScrollPhysics
各個方法的大體做用,而在前面 《十3、全面深刻觸摸和滑動原理》 中,咱們深刻解析過觸摸和滑動的原理,大體流程從觸摸開始往下傳遞, 最終觸發 layout
實現滑動的現象:
而 ScrollPhysics
的工做原理就穿插在其中,其流程以下圖所示, 主要的邏輯在於紅色標註的的三個方法:
applyPhysicsToUserOffset
:經過 physics 將用戶拖拽距離 offset
轉化爲 setPixels
(滾動) 的增量。applyBoundaryConditions
:經過 physics 計算當前滾動的邊界條件。createBallisticSimulation
: 建立自動滑動的模擬器。這三個方法的觸發時機在於 _handleDragUpdate
、 _handleDragCancel
和 _handleDragEnd
,也就是拖動過程和拖動結束的時機:
applyPhysicsToUserOffset
和 applyBoundaryConditions
是在 _handleDragUpdate
時被觸發的。createBallisticSimulation
是在 _handleDragCancel
和 _handleDragEnd
時被觸發的。因此默認的 BouncingScrollPhysics
和 ClampingScrollPhysics
最大的差別也在這個三個方法。
ClampingScrollPhysics
默認是沒有重載 applyPhysicsToUserOffset
方法的,當 parent == null
時,用戶的滑動 offset
是什麼就返回什麼:
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if (parent == null)
return offset;
return parent.applyPhysicsToUserOffset(position, offset);
}
複製代碼
BouncingScrollPhysics
中對 applyPhysicsToUserOffset
方法進行了 override
,其中 用戶沒有達到邊界前,依舊返回默認的 offset
,當用戶到達邊界時,經過算法來達到模擬溢出阻尼效果。
///摩擦因子
double frictionFactor(double overscrollFraction) => 0.52 * math.pow(1 - overscrollFraction, 2);
@override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
assert(offset != 0.0);
assert(position.minScrollExtent <= position.maxScrollExtent);
if (!position.outOfRange)
return offset;
final double overscrollPastStart = math.max(position.minScrollExtent - position.pixels, 0.0);
final double overscrollPastEnd = math.max(position.pixels - position.maxScrollExtent, 0.0);
final double overscrollPast = math.max(overscrollPastStart, overscrollPastEnd);
final bool easing = (overscrollPastStart > 0.0 && offset < 0.0)
|| (overscrollPastEnd > 0.0 && offset > 0.0);
final double friction = easing
// Apply less resistance when easing the overscroll vs tensioning.
? frictionFactor((overscrollPast - offset.abs()) / position.viewportDimension)
: frictionFactor(overscrollPast / position.viewportDimension);
final double direction = offset.sign;
return direction * _applyFriction(overscrollPast, offset.abs(), friction);
}
複製代碼
ClampingScrollPhysics
的 applyBoundaryConditions
方法中,在計算邊界條件值的時候,滑動值會和邊界值相減獲得相反的數據,使得滑動邊界相對靜止,從而達到「夾住」的做用 ,也就是動態邊界 ,因此默認請下 Android 上滾動到了邊界就會中止響應。
@override
double applyBoundaryConditions(ScrollMetrics position, double value) {
if (value < position.pixels && position.pixels <= position.minScrollExtent) // underscroll
return value - position.pixels;
if (position.maxScrollExtent <= position.pixels && position.pixels < value) // overscroll
return value - position.pixels;
if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
return 0.0;
}
複製代碼
ps: 前面說過藍色的半圓是默認的
ScrollBehavior
內buildViewportChrome
方法實現的。
BouncingScrollPhysics
中 applyBoundaryConditions
直接返回 0 ,也就是達到 0 是就邊界,過了 0 的就是邊界外的拖拽效果了。
@override
double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;
複製代碼
由於 createBallisticSimulation
是在 _handleDragCancel
和 _handleDragEnd
時觸發的,其實就是中止觸摸的時候,當 createBallisticSimulation
返回 null
時,Scrllable
將進入 IdleScrollActivity
,也就是中止滾動的狀態。
以下圖所示,徹底沒有 Simulation
的列表滾動,是不會連續滾動的。
ClampingScrollPhysics
的 createBallisticSimulation
方法中,使用了 ClampingScrollSimulation
(固定) 和 ScrollSpringSimulation
(彈性) 兩種 Simulation
,以下代碼所示,理論上只有 position.outOfRange
纔會觸發彈性的回彈效果,但 ScrollPhysics
採用了相似 雙親代理模型 ,其 parent
可能會觸發 position.outOfRange
,因此推測這裏纔會有 ScrollSpringSimulation
補充的判斷。
以下代碼能夠看出,只有在 velocity
速度大於默認加速度,而且是可滑動範圍內,才返回 ClampingScrollPhysics
模擬滑動,不然返回 null 進入前面所說的 Idle 中止滑動,這也是爲何普通慢速拖動,不會觸發自動滾動的緣由。
@override
Simulation createBallisticSimulation(
ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance;
if (position.outOfRange) {
double end;
if (position.pixels > position.maxScrollExtent)
end = position.maxScrollExtent;
if (position.pixels < position.minScrollExtent)
end = position.minScrollExtent;
assert(end != null);
return ScrollSpringSimulation(
spring,
position.pixels,
end,
math.min(0.0, velocity),
tolerance: tolerance,
);
}
if (velocity.abs() < tolerance.velocity) return null;
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent)
return null;
if (velocity < 0.0 && position.pixels <= position.minScrollExtent)
return null;
return ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
tolerance: tolerance,
);
}
複製代碼
BouncingScrollPhysics
的 createBallisticSimulation
則簡單一些,只有在結束觸摸時,初始速度大於默認加速度或者超出區域,纔會返回 BouncingScrollSimulation
進行模擬滑動計算,不然經進入前面所說的 Idle 中止滑動。
@override
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance;
if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
return BouncingScrollSimulation(
spring: spring,
position: position.pixels,
velocity: velocity * 0.91, // TODO(abarth): We should move this constant closer to the drag end.
leadingExtent: position.minScrollExtent,
trailingExtent: position.maxScrollExtent,
tolerance: tolerance,
);
}
return null;
}
複製代碼
能夠看出,在中止觸摸時,列表是否會繼續模擬滑動是和 velocity
和 tolerance.velocity
有關,也就是速度大於指定的加速度時纔會繼續滑動 ,而且在可滑動區域內 ClampingScrollSimulation
和 BouncingScrollSimulation
呈現的效果也不同。
以下圖所示,第一頁面的 ScrollSpringSimulation
在中止滾動前是有必定的減速效果的;而第二個頁面 ClampingScrollSimulation
是直接快速滑動到邊界。
事實上,經過選擇或者調整
Simulation
,就能夠對列表滑動的速度、阻尼、回彈效果等實現靈活的自定義。
前面最後說到了,利用 Simulation
實現對列表的滑動、阻尼、回彈效果的實現處理,那麼 Simulation
是如何工做的呢?
如上圖所示,在 Simulation
的建立是在 ScrollPositionWithSingleContext
的 goBallistic
方法中被調用的 ,而後經過 BallisticScrollActivity
去觸發執行。
@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();
}
}
複製代碼
在 BallisticScrollActivity
狀態中,Simulation
被用於驅動 AnimationController
的 value
,而後在動畫的回調中獲取 Simulation
計算後獲得的 value
進行 setPixels(value)
實現滾動。
這裏又涉及到了動畫的繪製機制,動畫的機制等新篇再詳細說明,簡單來講就是 當系統
drawFrame
的vsync
信號到來時,會執行到AnimationController
內部的_tick
方法,從而觸發_value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
改變和notifyListeners();
通知更新。
對於 Simulation
的內部計算邏輯這裏就不展開了,大體上可知 ClampingScrollSimulation
的摩擦因子是固定的,而 BouncingScrollSimulation
內部的摩擦因子和計算,是和傳遞的位置有關係。
這裏須要着重說起的就是,爲何 BouncingScrollPhysics
會自動回彈呢?
其實也是 BouncingScrollSimulation
的功勞,由於 BouncingScrollSimulation
構建時,會傳遞有 leadingExtent:position.minScrollExtent
和 trailingExtent: position.maxScrollExtent
兩個參數,在 underscroll 和 overscroll 的狀況下,會利用 ScrollSpringSimulation
實現彈性的回滾到 leadingExtent
和 trailingExtent
的動畫,從而達到以下圖的效果:
到這裏 Flutter 的 ScrollPhysics
和 Simulation
就基本分析完了,嚴格意義上, Simulation
應該是屬於動畫的部分,可是這裏由於ScrollPhysics
也放到了一塊兒。
總結起來就是 ScrollPhysics
中控制了用戶觸摸轉化和邊界條件,而且在用戶中止觸摸時,利用 Simulation
實現了自動滾動與溢出回彈的動畫效果。
自此,第十八篇終於結束了!(///▽///)