Scroller源码分析
Android API 21
- 作用
- 成员
- 方法
- 使用注意
- 替补选手 OverScroller
1. 作用
- Scroller是一个滚动的计算工具类,他根据客户端传入的滚动初始坐标x,y数值,以及最终的x,y数值,时间或者速度等,利用时间变化插值器来计算不同时刻的滚动坐标的,从而实现一个动态,平滑的滚动效果。比如我们的viewPager手动设置Position页面的的平滑滚动效果,又比如我们的ScrollView的滑翔滚动都是利用它来计算和实现的。
2. 重要成员
- mStartX, mStartY,mFinalX, mFinalY:起始和滚动终点的x, y坐标。
- mCurrX, mCurrY:某时刻的当前坐标
- mDuration, mDistance:时间和距离
- SCROLL_MODE, FLING_MODE:前者是普通的滚动方式,后者是松手后会滑翔一段距离的滚动方式
- mFinished: true结束滚动啦, false还在滚动
3. 关键方法
一波代码正向你袭来额 ~
-
构造
Scroller()
: 如果客户端 (就是调用端哦 ) 没有给Scroller设定插值器,就给他构建一个插值器,然后初始化一些物理效果的减速配置类似于摩擦力,滑翔操作就是靠这个摩擦力来停下来的哦。时间插值器,简单理解就是随着时间比例变化,我们的对应坐标数值比例变化。如果是匀速那么二者是一样的,如果是加速那么就先变化快,后变化慢,这样子啦。类似于如果匀速变化时间变化比例达到了0.5, 坐标变化也就是0.5.如果是加速那么时间0.5, 坐标变化可能就是0.3, 0.4这样子。public Scroller(Context context, Interpolator interpolator, boolean flywheel) { mFinished = true; if (interpolator == null) { //创建一个插值器,粘性效果 mInterpolator = new ViscousFluidInterpolator(); } else { mInterpolator = interpolator; } mPpi = context.getResources().getDisplayMetrics().density * 160.0f; //减速的加速度, 模拟摩擦力效果 mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); //fling模式下,是否累加前面还未结束的速度 mFlywheel = flywheel; mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning }
-
普通滚动:
startScroll
: 这里主要初始客户端配置的滚动信息,可以看出,这里start并没有真正地启动滚动哦。public void startScroll(int startX, int startY, int dx, int dy, int duration) { //普通滚动模式 mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); //设定启动,和结束的滚动x, y坐标 mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; //总时间作为底数,为了后面计算时间变化因子 mDurationReciprocal = 1.0f / (float) mDuration; }
-
滑翔滚动
fling
: 这里的主要意图是根据客户端设置的起点x, y坐标以及滑翔的初始速度以及设置的摩擦力减速来计算将要滑动的最终目标坐标和所需时间。因此这里的客户端一开始是不需要知道滑动的目的地,距离是多少的哦,系统会计算的;public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) { // 如果支持滑动累积,并且还没有结束滑动 if (mFlywheel && !mFinished) { float oldVel = getCurrVelocity(); float dx = (float) (mFinalX - mStartX); float dy = (float) (mFinalY - mStartY); float hyp = FloatMath.sqrt(dx * dx + dy * dy); float ndx = dx / hyp; float ndy = dy / hyp; //水平方向的速度分量 float oldVelocityX = ndx * oldVel; //垂直方向的速度分量 float oldVelocityY = ndy * oldVel; //如果新的速度方向和原始的速度方向一致,就累计起来啦。 if (Math.signum(velocityX) == Math.signum(oldVelocityX) && Math.signum(velocityY) == Math.signum(oldVelocityY)) { velocityX += oldVelocityX; velocityY += oldVelocityY; } } //设置滑翔模式, mMode = FLING_MODE; mFinished = false; //计算速度 float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); mVelocity = velocity; //根据开动速度和默认的摩擦减速度来计算将要滚动的时间 mDuration = getSplineFlingDuration(velocity); mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; //计算x, y方向上速度的分量 float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; //根据初始速度,计算总的滑行距离 double totalDistance = getSplineFlingDistance(velocity); //设定正,负方向 mDistance = (int) (totalDistance * Math.signum(velocity)); mMinX = minX; mMaxX = maxX; mMinY = minY; mMaxY = maxY; //根据x方向的分量和总距离,来计算x向最终的滚动位置 mFinalX = startX + (int) Math.round(totalDistance * coeffX); // Pin to mMinX <= mFinalX <= mMaxX mFinalX = Math.min(mFinalX, mMaxX); mFinalX = Math.max(mFinalX, mMinX); //根据y方向的分量和总距离,来计算y向最终的滚动位置 mFinalY = startY + (int) Math.round(totalDistance * coeffY); // Pin to mMinY <= mFinalY <= mMaxY mFinalY = Math.min(mFinalY, mMaxY); mFinalY = Math.max(mFinalY, mMinY); }
-
computeScrollOffset
: 我们的startScroll和fling都只是设地滚动的初始值,或者根据设定的初始值来计算最后的滚动位置,但是中间特定时刻的滚动位置,他们二者是没有去做的,那这些功能肯定是要有人来做的,要不然视图在刷新ui的时候怎么知道该在哪个位置绘制新的内容呢, 这就是computeScrollOffset
的意图啦。public boolean computeScrollOffset() { //如果滚动结束,该方法返回false. if (mFinished) { return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); //如果滚动过的时间还没到我们总的滚动耗时 if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE://普通滚动 //根据时间插值器和当前消耗的时间比例来计算数值消费的比例因子; //如匀速计算,500ms/1000s的消耗的时间因子是0.5,那么数值消费的因子也是0.5, //如果是加速计算,那么时间因子0.5对应的数值因子会大于0.5,可能是0.6,0.7等 //当前滚动距离 = 总距离 × 数值消费因子 final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); //根据滚动距离和起始位置计算当前的x, y位置。 mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; case FLING_MODE://滑行模式 final float t = (float) timePassed / mDuration; final int index = (int) (NB_SAMPLES * t); //距离系数 float distanceCoef = 1.f; float velocityCoef = 0.f; if (index < NB_SAMPLES) {//有点绕,懒得看了------ final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + 1) / NB_SAMPLES; final float d_inf = SPLINE_POSITION[index]; final float d_sup = SPLINE_POSITION[index + 1]; velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); distanceCoef = d_inf + (t - t_inf) * velocityCoef; } mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; //计算当前的x滚动位置 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); // Pin to mMinX <= mCurrX <= mMaxX mCurrX = Math.min(mCurrX, mMaxX); mCurrX = Math.max(mCurrX, mMinX); //计算当前的y滚动位置 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); // Pin to mMinY <= mCurrY <= mMaxY mCurrY = Math.min(mCurrY, mMaxY); mCurrY = Math.max(mCurrY, mMinY); if (mCurrX == mFinalX && mCurrY == mFinalY) { mFinished = true; } break; } } else { //如果滚动完了,那么就设定一下最终数值 mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } //没有滚动完,返回true; return true; }
-
abortAnimation
: 终止滑动过程,如果在滑动的过程中突然想要中止滑动就利用他啦,比如ScrollView滑动的时候,手指突然触摸一下屏幕,就停啦,原理也很简单。mFinished = true;
public void abortAnimation() { //设定位置 mCurrX = mFinalX; mCurrY = mFinalY; // 关闭滚动 mFinished = true; }
Scroller基本就这些内容啦~
4. 使用注意
Scrollers trackscroll offsets for you over time, but they don't automatically apply those
positions to your view. It's your responsibility to get and apply new coordinates .....
- 代码里有一段这样的话,意思就是说scroller只负责计算,但是他可不管把当前计算的值给到view上,不会去绘制view新的位置新的内容,这是你自己的事,别找我。好吧.....所以我们使用的时候,要自己来应用啦,怎么应用?
- 使用mScroller.startScroll(0, 0, xxxx, 0);
- 重写view的computeScroll方法, 在里面调用mScroller.computeScrollOffset()计算滚动位置,然后通过scroller.getCurentX, getCurretnY来获取位置,并应用这些位置到view上。重写computeScroll,是因为view在每60ms刷新屏幕的时候会来调我们的这个方法有没有滚动内容计算呢。
5. OverScroller: Scroller的替补选手
他其实和Scoller的意图是一样的,都是计算不同时刻的滚动坐标的。只不过他内部的是实现策略剥离到一个单独的类SplineOverScroller中去了,大致实现思路是相同的。不过他加强了Scroller的功能,支持了边界回弹效果,是通过对外接口
springBack
实现的。意思是如果我当前位置没有在设定的边界内,会继续以这个边界为目标数值,继续滚动以回到这个范围。一般会在手势跟踪中的手势抬起的时候,判断当前停靠的位置满不满足范围限制,如果不满足就要再设定一下滚动目标位置,再滚动下。
-
springBack
//返回false,表示当前滚动已经在目标区域了,可以停止渲染了; 返回true,表示当前的滚动位置还不在目标区域中,他还需要继续设置滚动位置, 计算滚动,并且你需要手动去触发一次重绘以开始新的滚动内容绘制,所以在调用该方法之后如果返回true,就要postInvalidateOnAnimation()一下啦。不信你看看ScrollView源码 public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) { mMode = FLING_MODE; // Make sure both methods are called. //会将目标数值minx, maxY作为目标值设置 final boolean spingbackX = mScrollerX.springback(startX, minX, maxX); final boolean spingbackY = mScrollerY.springback(startY, minY, maxY); return spingbackX || spingbackY; }
-
SplineOverScroller.springback, startSpringback
: 其实就是根据新的目标位置来计算新一轮的滚动,就是我们的回弹滚动啦。
boolean springback(int start, int min, int max) { mFinished = true; //重置前面的滚动计算的内容 mStart = mFinal = start; mVelocity = 0; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mDuration = 0; if (start < min) { startSpringback(start, min, 0); } else if (start > max) { startSpringback(start, max, 0); } //mFinished=true,结束了就表示不用再搞啥计算了;mFinished=false,表示还没结束,你要给 //计算咯,和前面正好对应。 return !mFinished; } --- private void startSpringback(int start, int end, int velocity) { // mStartTime has been set //没结束呢! mFinished = false; mState = CUBIC; //初始化start, final的x, y位置啦。 mStart = start; mFinal = end; final int delta = start - end; mDeceleration = getDeceleration(delta); // TODO take velocity into account mVelocity = -delta; // only sign is used //计算新的滚动距离,和滚动时间。 mOver = Math.abs(delta); mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration)); }
-
好啦,结束啦, 道声晚安吧......