本篇将通过一个体验优化需求来学习Flutter滑动体系中的ScrollPyhsics。
发现问题
接到这样一个体验问题,在iOS设备上浏览视频帖子时,发现每次脱手后滑动停止较缓慢,由于视频是在滑动停止后才播放,从而给人启播较慢的感受。
作为一名Android使用者,确实可以明显感受到差别。为了确认是否和原生体验相同,又找了非Flutter的原生应用进行对比,可以看到在iPhone上脱手滑动动画的表现是缓慢减速的,而在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
,两者区别:
- 其一是上文提到的脱手滑动效果,通过
createBallisticSimulation
实现; - 其二是达到边界效果,同样的和原生类似,iOS上允许超过边界且有弹簧效果,Android不允许超出且有半圆波纹效果。主要通过
applyPhysicsToUserOffset
和applyBoundaryConditions
实现。
/// * [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
实现不同的停止效果。
ClampingScrollPhysics 和 ClampingScrollSimulation
先看下Android上的ClampingScrollPhysics
,它使用了两种 Simulation
:
-
ClampingScrollSimulation
:处理velocity大于默认加速度且处于可滑动范围内的情况; -
ScrollSpringSimulation
:处理超过边界情况; - 其他情况直接返回 null,即
ScrollDirection.idle
状态,表示停止滑动。
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
怎么处理的,官方给出了具体的函数,从对应的图像可知,速度曲线在后部分有个升高,所以才会更快停止下来。
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是一开始用速度创建的,_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减速滑动优化方案了:
- 同Android都设置为
ClampingScrollPhysics
,改动小,但可能失去iOS原生体验; - 扩展本身的
BouncingScrollPhysics
,如增加速度衰减率 【decelerationRate】使其更快减速、同时提高滑动停止的最小速度【tolerance.velocity】使其更快停止,这些都能让减速运动快一些,并更大程度保留原生体验。当前是采用了这个方案,参数为0.135 -> 0.05、0.05->0.005,效果如图:
总结
在研究iOS减速运动时参考了一些文章,读起来是有些吃力,感觉把数学和物理都还给老师了,这里也许还有其他影响运动曲线的因子,有时间的可以继续研究下,补充参考文档~