很多和滚动啊滑动啊有关的功能,都绕不开一个类,Scroller,如果只是想移动画布的话,在View中就有一些我们可以使用的api, 例如mScrollX,mScrollY,scrollTo(),ScrollBy(),但是这些方法会直接一步到位,所以我们需要scroller来实现平滑的滚动,或者各种骚操作滚动。
在Api19,Google爸爸新引入了OverScroller,相比之下,多了处理超越边界的方法,所以我们只看新类的源码就好。
先看下OverScroller类的public方法和属性,见下图。
构造器中,初始化了两个主要的组件。
public OverScroller(Context context, Interpolator interpolator, boolean flywheel) {
if (interpolator == null) {
mInterpolator = new Scroller.ViscousFluidInterpolator();
} else {
mInterpolator = interpolator;
}
mFlywheel = flywheel;
mScrollerX = new SplineOverScroller(context);
mScrollerY = new SplineOverScroller(context);
}
一个是ViscousFluidInterpolator,看字面意思是一个插值器,看了下源码灰常复杂的小学四则混合运算,头疼,不过插值器这东西大概是调整速度曲线的吧,什么先快后慢,欲扬先抑,九浅一深之类的,我们先走一遍主流程,所以暂时搁置。
另一个组件SplineOverScroller也是非常重要,主线剧情上很多的函数最后都要靠这个小老弟来实现,不过点进去又是一堆四则混合运算,为了快速通关刷剧情,所以老办法,搁置!
我们是源码分析,所以讲道理应该已经会使用这个类了,But,作为一个资深的api调用师,在分析之前很有必要回想一下,完整的调用流程是什么?
class Api调用师的一个类{
val scroller:OverScroller...
fun 一个名为A的调用{
scroller.startScroll(startX,startY,dx,dy,duration)
invalidate()
}
@override
computeScroll(){
if(scroller.computeScrollOffset()){
scrollTo(scroller.currX,scroller.currY)
postInvalidate()
}else{
super.computeScroll()
}
}
}
So,我们的流程就是!:① 我们先startScroll一下(爹,我要买GTR!),② 然后invalidate()让自己和父View重绘(老爹很给面子,不就GT阿鲁吗,买了!),③ 有牌面的父View在draw()里都会调用子view的computeScroll(),大家可以自行查找源码(自己定义的ViewGroup如果把computeScroll()搞丢了,就相当于爹低头一看卡里就剩100块,就买不了GT阿鲁了),④ 自己的computeScroll()逻辑被触发,完成滚动的一部分,然后继续重复这个过程,一直到scroller.computeScrollOffset()返回为false,证明已经到达我们在startScroll中传入的终点,滚动结束!(开着你的GTR去逮虾户吧,翻不翻车全看你的车技computeScroll()写得好不好)
So,我们就先从startScroll开始我们的逮虾户之旅吧。
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mScrollerX.startScroll(startX, dx, duration);
mScrollerY.startScroll(startY, dy, duration);
}
扎心,就标记了个状态,然后就传给刚才精通四则混合运算的小老弟了。我们硬着头皮看下一哈,发现很简单,只是一些状态的赋值,以及加减法,连乘除都冇。
void startScroll(int start, int distance, int duration) {
mFinished = false;
mCurrentPosition = mStart = start;
mFinal = start + distance;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mDuration = duration;
// Unused
mDeceleration = 0.0f;
mVelocity = 0;
}
代码可谓非常的通俗易懂,标记下时间,计算下开始位置,终点位置,结束状态,速度状态的初始化。可见这个方法只是嘴炮选手而已,并没有产生实际的行动,根据刚才我们调用的流程,那么其他逻辑一定是在后面if(scroller.computeScrollOffset())
这个函数里,没想到她身兼数职,不但返回值可以判断是否已经滚动到位,内部还实现了计算逻辑。
public boolean computeScrollOffset() {
if (isFinished()) {
return false;
}
switch (mMode) {
case SCROLL_MODE:
long time = AnimationUtils.currentAnimationTimeMillis();
// Any scroller can be used for time, since they were started
// together in scroll mode. We use X here.
final long elapsedTime = time - mScrollerX.mStartTime;
final int duration = mScrollerX.mDuration;
if (elapsedTime < duration) {
final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
mScrollerX.updateScroll(q);
mScrollerY.updateScroll(q);
} else {
abortAnimation();
}
break;
case FLING_MODE:
if (!mScrollerX.mFinished) {
if (!mScrollerX.update()) {
if (!mScrollerX.continueWhenFinished()) {
mScrollerX.finish();
}
}
}
if (!mScrollerY.mFinished) {
if (!mScrollerY.update()) {
if (!mScrollerY.continueWhenFinished()) {
mScrollerY.finish();
}
}
}
break;
}
return true;
}
根据我们对startScroll的分析,从这个入口进入,mode一定是SCROLL_MODE的,所以我们先看这个case,首先就是取了下 从start到现在间隔的时间elapsedTime。而duration的时间是整个滚动应该花费的时间,所以,一小片的时间超过整个滚动时间的话就会打断动画了abortAnimation();
,否则就正常进入小老弟的updateScroll函数,我们注意到这个函数的入参是一个Float类型。
void updateScroll(float q) {
mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
}
其实看到这个可以猜出了,这必然是插值器根据时间比例给这一段滚动分配的配额,所有的配额肯定是0到1,配额*总长度 = 这一段距离起点的长度。
我们再回头看看这个参数是怎么计算的。
final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
插值器要的参数刚好是这一段的时间/总时间,也就是他的功能就是求一个x,这个x/滚动总长度 = fun(这一段时间/总时间)。这个fun函数不一定是fun(x) = k*x,而是根据差值器的算法来的,可能是匀速,也可能是先快后慢,九浅一深。。。
@Override
public float getInterpolation(float input) {
final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
if (interpolated > 0) {
return interpolated + VISCOUS_FLUID_OFFSET;
}
return interpolated;
}
终于还是逃不过四则混合运算,我们来重新回到小学,做一做数学题!第一句:
// must be set to 1.0 (used in viscousFluid())
VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
容我查一下单词。。viscous:黏性的; 黏的; 半流体的; 黏滞的; viscous fluid岂不就是: 黏液。。。我透。。看下这个函数
private static final float VISCOUS_FLUID_SCALE = 8.0f;
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;
}
算了一下,就是
打扰了,反正能确定的不是匀速的。。。
再一次回想我们实现computeScroll,会获取当前的位置并滚动到这个位置,getCurrX(),一看果然就是这个
public final int getCurrX() {
return mScrollerX.mCurrentPosition;
}
到此最基本的实现过程就已经清楚明白了。不过后面还有OverScroller独有的函数,再NestedScrollView等源码里用的非常的多,下一篇再写吧。
isOverScrolled()
springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY)
notifyHorizontalEdgeReached(int startX, int finalX, int overX)
notifyVerticalEdgeReached(int startY, int finalY, int overY)