本篇文章主要介绍了一下Scroller的使用并对其源码进行了简单的分析,感兴趣的朋友可以看一下。
基本用法:
Scroller有两个比较重要且常用的方法:startScroll和fling,特别是第一个方法我们在定义View或ViewGroup的时候经常用到,我写了一个简单的定义ViewGroup的小demo演示这两个方法的用法,先看下效果图:
startScroll方法:
;
fling方法:
主要代码
下面是主要的实现代码,比较简单,全部的代码会在最下面给出:
@Override
public boolean onTouchEvent(MotionEvent event) {
//fling方法用到
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
preX = event.getX();
if (!mScroller.isFinished())
mScroller.abortAnimation();
break;
case MotionEvent.ACTION_MOVE:
float curX = event.getX();
int dx = (int) (preX - curX);
//滑动的范围是0=<x<=getMeasuredWidth()
if (getScrollX() + (int) dx < 0) {
this.scrollTo(0, 0);
} else if (getScrollX() + (int) dx > getMeasuredWidth()) {
this.scrollTo(getMeasuredWidth(), 0);
} else {
this.scrollBy((int) dx, 0);
}
preX = curX;
break;
case MotionEvent.ACTION_UP:
//判断应该滑动到左右哪一侧(startScroll方法用到)
int sX = this.getScrollX();
int index = sX / getMeasuredWidth();
int d = sX % (getMeasuredWidth());
if (d > getMeasuredWidth() / 2.0f) {
index += 1;
}
//startScroll用法
smoothScrollToIndex(index);
//fling用法
//smoothFling();
break;
}
//消费事件
return true;
}
/**
* startScroll的用法
* @param index
*/
private void smoothScrollToIndex(int index) {
mScroller.startScroll(getScrollX(), 0, index * getMeasuredWidth() - getScrollX(), 0);
//View重绘的时候draw方法会调用下面的computeScroll()方法
invalidate();
}
/**
* fling的用法
*/
private void smoothFling() {
mVelocityTracker.computeCurrentVelocity(1000);
float fXV = mVelocityTracker.getXVelocity();
//mVelocityTracker得到的速度从左向右是正的,而scroller里是从右向左是负的,所认这里在fXV前面加了一个负号
mScroller.fling(getScrollX(), 0, -(int) fXV, 0, 0, getMeasuredWidth(), 0, 0);
//View重绘的时候draw方法会调用下面的computeScroll()方法
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {//scroll没有结束
int curX = mScroller.getCurrX();
int curY = mScroller.getCurrY();
this.scrollTo(curX, curY);
//继续调用computeScroll()方法
invalidate();
}
}
源码浅析
先看Scroller的构造方法:
151 public Scroller(Context context) {
152 this(context, null);
153 }
160 public Scroller(Context context, Interpolator interpolator) {
161 this(context, interpolator,
162 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
163 }
170 public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
171 mFinished = true;
172 if (interpolator == null) {
173 mInterpolator = new ViscousFluidInterpolator();
174 } else {
175 mInterpolator = interpolator;
176 }
177 mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
178 mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
179 mFlywheel = flywheel;
180
181 mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
182 }
Scroller总共有三个构造方法,最终调用的都是第三个,可以看到我们可以传一个插值器Interpolator进来,Interpolator可以控制滑动的速度变化,比如我们可以用它实现均匀滑动或加速滑动以及其他一些效果,不清楚的可以自己google一下,如果我们没有提供Interpolator的话,Scroller会使用一个默认的插值器ViscousFluidInterpolator:
562 static class ViscousFluidInterpolator implements Interpolator {
563
564 private static final float VISCOUS_FLUID_SCALE = 8.0f;
565
566 private static final float VISCOUS_FLUID_NORMALIZE;
567 private static final float VISCOUS_FLUID_OFFSET;
568
569 static {
570
571 // must be set to 1.0 (used in viscousFluid())
572 VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
573 // account for very small floating-point error
574 VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
575 }
576
577 private static float viscousFluid(float x) {
578 x *= VISCOUS_FLUID_SCALE;
579 if (x < 1.0f) {
580 x -= (1.0f - (float)Math.exp(-x));
581 } else {
582 float start = 0.36787944117f; // 1/e == exp(-1)
583 x = 1.0f - (float)Math.exp(1.0f - x);
584 x = start + x * (1.0f - start);
585 }
586 return x;
587 }
588
589 @Override
590 public float getInterpolation(float input) {
591 final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
592 if (interpolated > 0) {
593 return interpolated + VISCOUS_FLUID_OFFSET;
594 }
595 return interpolated;
596 }
597 }
其中getInterpolation方法可以控制scroll的滑动快慢。
再看startScroll方法:
369 public void startScroll(int startX, int startY, int dx, int dy) {
370 startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
371 }
387 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
388 mMode = SCROLL_MODE;//模式,startScroll方法是SCROLL_MODE
389 mFinished = false;//标识是否滑动结束
390 mDuration = duration;//滑动时间
391 mStartTime = AnimationUtils.currentAnimationTimeMillis();//开始时间
392 mStartX = startX;//开始x位置
393 mStartY = startY;//开始y位置
394 mFinalX = startX + dx;//结束x位置
395 mFinalY = startY + dy;//结束y位置
396 mDeltaX = dx;//x方向上的滑动间距
397 mDeltaY = dy;//y方向上的滑动间距
398 mDurationReciprocal = 1.0f / (float) mDuration;//滑动时间的倒数
399 }
startScroll方法有两个实现,第一个方法调用的是第二个方法,如果我们没有提供滑动的时间的话,会有一个默认的,默认是250ms:
private static final int DEFAULT_DURATION = 250;
可以看到startScroll方法只是对Scroller里面的成员变进行了赋值操作。
在上面的小demo中,我们在调用完startScroll方法后接着调用了invalidate()方法,invalidate()方法会导致computeScroll()方法被调用(invalidate()方法会导致view绘,view的draw方法内部会调用computeScroll(),具体过程可以查看相关源码):
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {//scroll没有结束
int curX = mScroller.getCurrX();
int curY = mScroller.getCurrY();
this.scrollTo(curX, curY);
//继续调用computeScroll()方法
invalidate();
}
}
在computeScroll()方法内部我们调用了Scroller的computeScrollOffset()方法,看一下这个方法是干什么的:
300 public boolean computeScrollOffset() {
301 if (mFinished) {//滑动结束,返回false
302 return false;
303 }
304
305 int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);//当前过去的时间
306
307 if (timePassed < mDuration) {//滑动没有结束
308 switch (mMode) {
309 case SCROLL_MODE://startScroll的mMode是SCROLL_MODE
310 final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);//调用插值器的getInterpolation方法,传进去的是当前经过的时间占总滑动时间的百分比,返回的是重新计算的百分比,这是插值器的一种常用用法。
311 mCurrX = mStartX + Math.round(x * mDeltaX);//将当前的x位置赋值给mCurrX成员
312 mCurrY = mStartY + Math.round(x * mDeltaY);//将当前的y位置赋值给mCurrY成员
313 break;
314 case FLING_MODE://fling的mMode是FLING_MODE
315 final float t = (float) timePassed / mDuration;
316 final int index = (int) (NB_SAMPLES * t);
317 float distanceCoef = 1.f;
318 float velocityCoef = 0.f;
319 if (index < NB_SAMPLES) {
320 final float t_inf = (float) index / NB_SAMPLES;
321 final float t_sup = (float) (index + 1) / NB_SAMPLES;
322 final float d_inf = SPLINE_POSITION[index];
323 final float d_sup = SPLINE_POSITION[index + 1];
324 velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
325 distanceCoef = d_inf + (t - t_inf) * velocityCoef;
326 }
327
328 mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
329
330 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
331 // Pin to mMinX <= mCurrX <= mMaxX
332 mCurrX = Math.min(mCurrX, mMaxX);
333 mCurrX = Math.max(mCurrX, mMinX);
334
335 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
336 // Pin to mMinY <= mCurrY <= mMaxY
337 mCurrY = Math.min(mCurrY, mMaxY);
338 mCurrY = Math.max(mCurrY, mMinY);
339
340 if (mCurrX == mFinalX && mCurrY == mFinalY) {
341 mFinished = true;
342 }
343
344 break;
345 }
346 }
347 else {//滑动结束
348 mCurrX = mFinalX;
349 mCurrY = mFinalY;
350 mFinished = true;
351 }
352 return true;
353 }
看以看出computeScrollOffset()的处理逻辑是首先判断滑动是否结束,结束的话返回fasle,如果还没有结束则根据当前经过的时间计算出当前应该滑动的位置赋值给mCurrX,mCurrY,然后返回true。
所以我们就可以调用computeScrollOffset()计算出最新的位置,如果computeScrollOffset()返回true代表滑动还没有结束,我们就可以使用scrollTo滑动到最新的位置,然后继续通过通过调用invalidate()方法重复上面的逻辑。
fling方法的使用跟startScroll方法是差不多的,只不过计算的过程更复杂一些,这里就不分析了。
演示Demo的下载链接
如果大家有什么疑问或文章中有讲的不对的地方,欢迎大家提出来讨论,谢谢大家。