7、Scroller与VelocityTracker:平滑滚动的实现原理,Fling手势的速度追踪与衰减
各位同学,今天我们来聊聊Android滚动机制里两个核心角色——Scroller和VelocityTracker。
说实话,很多做了两三年的Android开发,对这两个类还是「会用但不懂」。知道怎么调scrollTo、怎么用fling,但问起内部怎么算的、为什么能平滑停下来,就支支吾吾了。
嗯,今天我们就把它彻底讲透。
7.1 VelocityTracker:手指离开那一瞬间的速度
先问个问题:为什么需要VelocityTracker?
你想想看,用户手指在屏幕上快速一划然后松开,系统怎么知道这一划有多快?是快如闪电还是慢如蜗牛?
答案就是——VelocityTracker。
它本质上是一个速度计算器。它会记录手指移动的历史坐标和时间,然后用这些数据算出瞬时速度。
7.1.1 基本用法
我个人习惯在onTouchEvent里这样用:
// 1. 创建
private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
// 2. 在ACTION_DOWN时重置
case MotionEvent.ACTION_DOWN:
mVelocityTracker.clear();
mVelocityTracker.addMovement(event);
break;
// 3. 在ACTION_MOVE时持续添加
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(event);
// 注意:这里可以每帧都add,但不要每帧都compute
break;
// 4. 在ACTION_UP时计算速度
case MotionEvent.ACTION_UP:
mVelocityTracker.computeCurrentVelocity(1000); // 单位:像素/秒
float velocityX = mVelocityTracker.getXVelocity();
float velocityY = mVelocityTracker.getYVelocity();
// 用这个速度去启动Scroller的fling
break;
// 5. 用完记得回收
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
computeCurrentVelocity()的参数是时间单位,传入1000表示「每秒多少像素」。如果你传入1,得到的就是「每毫秒多少像素」,数值会非常小,容易搞混。我建议统一用1000。
7.1.2 速度计算的原理
VelocityTracker内部是怎么算速度的?说白了就是位移除以时间。
但它不是简单用最后两个点算,那样太粗糙了。它会取最近一段时间(默认100ms)内的多个采样点,做加权平均。
公式大致是:
V = (Δx₁ * w₁ + Δx₂ * w₂ + ...) / (Δt₁ * w₁ + Δt₂ * w₂ + ...)
权重w越靠近当前时刻越大。这样算出来的速度更平滑,也更符合人的直觉。
computeCurrentVelocity()的参数。数值越大,速度越「敏感」。我一般用500~1000之间。
7.2 Scroller:平滑滚动的幕后英雄
好,速度拿到了。接下来怎么让View平滑地滚起来?
这就是Scroller的活了。
很多人以为Scroller是负责「滚动」的,其实不对。Scroller只负责计算每一帧应该滚动到哪个位置,真正的滚动动作还是靠View的scrollTo()或scrollBy()来执行。
7.2.1 Scroller的核心方法
| 方法 | 作用 |
|---|---|
startScroll() |
启动一段匀速滚动(很少用,因为不平滑) |
fling() |
启动一段带衰减的惯性滚动(最常用) |
computeScrollOffset() |
计算当前帧的滚动位置,返回true表示还在滚动中 |
getCurrX() / getCurrY() |
获取当前帧应该滚动到的位置 |
isFinished() |
判断滚动是否结束 |
abortAnimation() |
强制停止滚动 |
7.2.2 标准用法:配合View的computeScroll
这是最经典的搭配:
// 在自定义View中
private Scroller mScroller = new Scroller(getContext());
// 启动fling
public void fling(int velocityX, int velocityY) {
// 参数:起始X, 起始Y, 速度X, 速度Y, 最小X, 最大X, 最小Y, 最大Y
mScroller.fling(
getScrollX(), getScrollY(),
velocityX, velocityY,
0, maxScrollX,
0, maxScrollY
);
postInvalidateOnAnimation(); // 触发重绘
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
// 还在滚动中,更新位置
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidateOnAnimation(); // 继续下一帧
}
}
注意这个computeScroll()方法。它是View的一个回调,在draw()方法中被调用。你不需要手动调用它,系统会在每一帧绘制时自动调用。
- fling() 启动后,Scroller内部记录起始位置、速度、衰减系数
- 每一帧,computeScrollOffset() 根据当前时间计算新位置
- scrollTo() 执行实际滚动
- postInvalidateOnAnimation() 请求下一帧
- 直到 computeScrollOffset() 返回false,循环结束
7.3 Fling的衰减算法:从物理到代码
为什么fling能模拟出「惯性滑动然后慢慢停」的效果?
Scroller内部用的是物理衰减模型。简单说就是:
位置 = 起始位置 + 速度 × 时间 - 0.5 × 减速度 × 时间²
但实际实现更复杂。Scroller用了三次样条插值,把速度从初始值逐渐衰减到0。衰减的快慢由mDeceleration控制,这个值跟屏幕密度有关。
我记得在Android 4.x时代,Google改过一次衰减算法。老版本的fling滑起来特别「飘」,新版本更「黏手」。如果你在低版本设备上测试过,应该能感觉到差异。
7.3.1 关键参数:摩擦力
Scroller里有个mFlywheel的概念,其实就是摩擦力。摩擦力越大,衰减越快,滑动距离越短。
你可以通过ViewConfiguration.getScaledMaximumFlingVelocity()和getScaledMinimumFlingVelocity()来获取系统建议的速度范围。
fling()的边界参数设错了。注意:最小边界要≤起始位置,最大边界要≥起始位置,否则fling会立即结束。
7.4 实战:手写一个可fling的滚动容器
光说不练假把式。我们写一个简单的可垂直滚动的ViewGroup:
public class FlingScrollLayout extends ViewGroup {
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
private int mTotalHeight; // 所有子View的总高度
public FlingScrollLayout(Context context) {
super(context);
mScroller = new Scroller(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 手指按下时,停止正在进行的fling
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float deltaY = mLastY - event.getY();
// 边界检查:不能滚过头
int targetScrollY = (int) (getScrollY() + deltaY);
targetScrollY = Math.max(0, Math.min(targetScrollY, mTotalHeight - getHeight()));
scrollTo(getScrollX(), targetScrollY);
mLastY = event.getY();
break;
case MotionEvent.ACTION_UP:
mVelocityTracker.computeCurrentVelocity(1000);
int velocityY = (int) mVelocityTracker.getYVelocity();
// 启动fling
mScroller.fling(
getScrollX(), getScrollY(),
0, -velocityY, // 注意:速度方向与滚动方向相反
0, 0,
0, mTotalHeight - getHeight()
);
postInvalidateOnAnimation();
break;
}
return true;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidateOnAnimation();
}
}
// ... onMeasure和onLayout略
}
7.5 知识体系总览
下面这张图总结了本章的核心脉络:
整个流程就是:触摸 → 速度追踪 → 惯性计算 → 帧循环滚动。每一步都有对应的类和算法在背后支撑。
7.6 总结
好了,这一章的内容就这些。我帮你理一下重点:
- VelocityTracker:负责在手指抬起时计算瞬时速度。记得用完要recycle。
- Scroller:负责模拟惯性滚动。它只算位置,不负责滚动。
- computeScroll():View提供的帧循环回调,是连接Scroller和实际滚动的桥梁。
- fling衰减:基于物理模型,用三次样条插值实现平滑减速。
说实话,这两个类单独看都不难,但把它们组合起来用对,才是真正理解Android滚动机制的开始。下一章我们会深入OverScroller和边缘回弹效果,那又是另一番天地了。
公众号:蓝海资料掘金营,微信deep3321