我们都使用过 Android 上的各种滚动控件,比如 ScrollView
、RecyclerView
等。不知读者是否注意过,它们在滚动时的效果看起来是有些生硬的。有 Android 设备的读者可以现在试一试:快速滑动一下屏幕然后立即松手(也就是 fling 动作),你可能会发现这个 view 从快速滚动到静止的减速过程不太自然,有点儿「太快」了。如果你使用过 iOS 设备的话,这种感觉会更加明显。
为什么会有「Android 动画有些生硬」的感觉呢?其中一个重要的原因——也是开发 Android 给我最大的感觉——就是 Android 的系统动画实在是「太快」了,很多本来比较复杂的动画却是「一闪而过」。就以下面 Android 原生 launcher 点击 Google 搜索的动画为例,我们来看一下慢放 10 倍是什么样子:
这个动画中有一个 G logo 旋转放大的效果,从动图可以感受到,即使是用 10 倍的慢速来播放,看上去依然是很快的。在手机上实际点击时,我甚至没有感觉到它在放大的同时也在旋转。
以前看过一个贴子说让手机变得「伪流畅」的办法就是在开发者选项里把动画速度调到 0.5x,可是 0.5 倍的速度不是让看不清的动画更看不清了么⋯⋯我觉得如果把动画做得快到看不清的话还不如没有动画来得方便。真的,以目前的动画时长来看,2x 慢速绝对不会耽误你做任何事。我倒是建议开发者可以尝试把系统动画都调成 1.5x 或 2x 体验一下那种顺滑感,真的你就不想再调回去了。这也算是题外话。
我们回到滚动视图本身。除了时长之外,还有加速度等不易察觉的因素在影响着滚动动画给用户的观感。
以 ScrollView
为例,查看它的源码可以发现,它通过一个 OverScroller 对象(通常用作 Scroller 对象的替代对象)来处理滚动。在 OverScroller 的构造方法中,会初始化一个 ViscousFluidInterpolator
(粘性流体插值器)对象作为插值器用来计算每一帧动画的值。我之前没有见过这个所谓「粘性流体」的插值器,推测它可能是按照某种粘性流体的运动规律来模拟视图的滚动吧(不过也好奇由硬卡纸为原型的 Material Design 为什么要模拟粘性流体的流动)。它是这样计算每帧的值的:
private static float viscousFluid(float x) {
x *= VISCOUS_FLUID_SCALE;
if (x < 1.0f) {
x -= (1.0f - (float)Math.exp(-x));
} else {
float start = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - (float)Math.exp(1.0f - x);
x = start + x * (1.0f - start);
}
return x;
}
我照着这个方法作了一张滑动曲线图:
从曲线中可以看出,到了动画的后期,速度衰减得相当快。而且不管内容以多大的初速度滚动,都会按照这个固定的曲线来变化,这就会导致在小距离滚动时动画很快停止。这也就是我们在滑动原生控件的时候感觉「滑一下就停了」或者「滑不动」的原因。
另外,Android 滑动给人造成生硬印象的另一个原因是当滑动到视图边缘的时候,里面的内容会直接「撞墙」——没有任何缓冲地在视图边缘戛然而止。甚至更有趣地,在对应的那个边上闪出一道光来,好像冲击波向四周扩散的感觉:
Google 在 I/O ’17 上发布了一套基于物理的动画类 SpringAnimation
(弹簧动画,模拟各种材质的弹簧在不同状态下的摆动)和 FlingAnimation
(模拟一个滑块在不光滑平面上的运动),这两种动画不是用插值器(interpolator)来计算每帧动画的值,而是使用「力」和「速度」进行物理上的矢量计算。这样的好处是使动画更加拟物,使呈现的效果更加贴近自然规律,符合人的直觉。如果能使用这两种基于物理的动画来使得视图的滚动动画过渡可以更加自然就好了。
实现的具体思路可以是这样:在一个 ScrollView 的 onTouchEvent()
上做文章。当检测到手指抬起的事件后,由 VelocityTracker
计算抬起这一瞬间的滑动速度,并以它为初速度选取合适的阻力开始 FlingAnimation(以下的代码均是由 Kotlin 写成的):
// 由获取到的 VelocityTracker 对象计算手指滑动的速度
val tracker = this.velocityTracker
tracker!!.computeCurrentVelocity(800)
val velocityY = tracker.yVelocity
// 将 FlingAnimation 设置初速度并启动动画
flingAnimation?.setStartVelocity(-velocityY)?.start()
然后为 FlingAnimation 添加一个监听器来监听滚动值的更新。当发现滚动到页面边缘时启动 SpringAnimation 实现回弹效果:
flingAnimation = FlingAnimation(this, DynamicAnimation.SCROLL_Y).apply {
friction = 0.3f
addUpdateListener { _, _, velocity ->
if ([email protected] == 0 || [email protected] == child!!.measuredHeight - [email protected]) {
flingAnimation!!.cancel()
overScrollAnimation!!.setStartVelocity(-velocity).start()
}
}
}
这样我们就实现了一个如下面动图般的滚动视图了。
为了方便各位读者研究和使用,我已经将这个视图控件开源,大家可以点击这里查看源代码和直接使用。我精心调好了阻力、弹簧刚度等参数,使得视图滑动时那种不慌不忙、恰到好处的感觉非常舒服,各位读者可以亲自试一试,一定会感叹「原来 Android 的滑动也可以这么舒服啊」。也欢迎大家 star,多多提 issue。