本篇將通過一個體驗優(yōu)化需求來學習Flutter滑動體系中的ScrollPyhsics。
發(fā)現(xiàn)問題
接到這樣一個體驗問題,在iOS設備上瀏覽視頻帖子時,發(fā)現(xiàn)每次脫手后滑動停止較緩慢,由于視頻是在滑動停止后才播放,從而給人啟播較慢的感受。

作為一名Android使用者,確實可以明顯感受到差別。為了確認是否和原生體驗相同,又找了非Flutter的原生應用進行對比,可以看到在iPhone上脫手滑動動畫的表現(xiàn)是緩慢減速的,而在aPhone上會有個輕微的加速(吸附效果)。事實上在Flutter中,默認情況下和原生滑動效果是一致的。

相關知識
ScrollPyhsics 和 Simulation
控制這個滑動過程物理特性的正是ScrollPyhsics,包括滾動和邊緣拖拽效果。常見列表如ListView、CustomScrollView和GridView等ScrollView可以通過physics屬性來設置ScrollPyhsics。
const ScrollView({
Key? key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
bool? primary,
ScrollPhysics? physics,
...
}) : assert(scrollDirection != null),
...
physics = physics ?? ((primary ?? false) || (primary == null && controller == null && identical(scrollDirection, Axis.vertical)) ? const AlwaysScrollableScrollPhysics() : null),
super(key: key);
如果不設置physics默認用AlwaysScrollableScrollPhysics,這是個組合Pyhsics,表示在iOS平臺上會用BouncingScrollPhysics,Android平臺上會用ClampingScrollPhysics,兩者區(qū)別:
- 其一是上文提到的脫手滑動效果,通過
createBallisticSimulation實現(xiàn); - 其二是達到邊界效果,同樣的和原生類似,iOS上允許超過邊界且有彈簧效果,Android不允許超出且有半圓波紋效果。主要通過
applyPhysicsToUserOffset和applyBoundaryConditions實現(xiàn)。
/// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior
/// found on iOS.
/// * [ClampingScrollPhysics], which provides the clamping overscroll behavior
/// found on Android.
class AlwaysScrollableScrollPhysics extends ScrollPhysics {
...
}
接下來主要分析兩端是如何通過Simulation實現(xiàn)不同的停止效果。
ClampingScrollPhysics 和 ClampingScrollSimulation
先看下Android上的ClampingScrollPhysics ,它使用了兩種 Simulation:
-
ClampingScrollSimulation:處理velocity大于默認加速度且處于可滑動范圍內的情況; -
ScrollSpringSimulation:處理超過邊界情況; - 其他情況直接返回 null,即
ScrollDirection.idle狀態(tài),表示停止滑動。
class ClampingScrollPhysics extends ScrollPhysics {
@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,
);
}
}
那么關鍵在于ClampingScrollSimulation怎么處理的,官方給出了具體的函數(shù),從對應的圖像可知,速度曲線在后部分有個升高,所以才會更快停止下來。

BouncingScrollPhysics 和 FrictionSimulation
再來看下iOS的BouncingScrollPhysics,都由BouncingScrollSimulation來處理,一旦velocity小于默認加速度即停止。
class BouncingScrollPhysics extends ScrollPhysics {
@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,
leadingExtent: position.minScrollExtent,
trailingExtent: position.maxScrollExtent,
tolerance: tolerance,
);
}
return null;
}
}
可以看到Tolerance.velocity是一個影響因素,取值 = 1/(影響因子*設備像素比),默認是0.05,這個值越小velocity越大。
static final Tolerance _kDefaultTolerance = Tolerance(object.
velocity: 1.0 / (0.050 * WidgetsBinding.instance.window.devicePixelRatio),
distance: 1.0 / WidgetsBinding.instance.window.devicePixelRatio,
);
因為BouncingScrollPhysics允許超過邊界,這里同樣有兩種 Simulation:
-
FrictionSimulation(_frictionSimulation):摩擦模擬器,負責減速過程 -
ScrollSpringSimulation(_springSimulation):彈簧模擬器,負責超過邊界彈回過程
兩個Simulation同時存在就是要一起配合處理減速過程中可能存在的超過邊界的情況。比如,_frictionSimulation是一開始用速度創(chuàng)建的,_springTime 是達到邊界的時間,一旦 time大于這個值,就會啟動 _springSimulation。
class BouncingScrollSimulation extends Simulation {
late FrictionSimulation _frictionSimulation;
late Simulation _springSimulation;
late double _springTime;
double _timeOffset = 0.0;
Simulation _underscrollSimulation(double x, double dx) {
return ScrollSpringSimulation(spring, x, leadingExtent, dx);
}
Simulation _overscrollSimulation(double x, double dx) {
return ScrollSpringSimulation(spring, x, trailingExtent, dx);
}
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;
}
}
關于FrictionSimulation的運動曲線,官方主要給了這樣一個描述,這里的0.135表示速度衰減率(decelerationRate),即減速時速度每秒衰減為原來的0.135倍,這個值越小速度衰減越快。
// Taken from UIScrollView.decelerationRate (.normal = 0.998)
// 0.998^1000 = ~0.135
_frictionSimulation = FrictionSimulation(0.135, position, velocity);
解決辦法
到這里可以給出幾個iOS減速滑動優(yōu)化方案了:
- 同Android都設置為
ClampingScrollPhysics,改動小,但可能失去iOS原生體驗; - 擴展本身的
BouncingScrollPhysics,如增加速度衰減率 【decelerationRate】使其更快減速、同時提高滑動停止的最小速度【tolerance.velocity】使其更快停止,這些都能讓減速運動快一些,并更大程度保留原生體驗。當前是采用了這個方案,參數(shù)為0.135 -> 0.05、0.05->0.005,效果如圖:

總結
在研究iOS減速運動時參考了一些文章,讀起來是有些吃力,感覺把數(shù)學和物理都還給老師了,這里也許還有其他影響運動曲線的因子,有時間的可以繼續(xù)研究下,補充參考文檔~