本篇文章主要介紹了一下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的下載鏈接
如果大家有什麼疑問或文章中有講的不對的地方,歡迎大家提出來討論,謝謝大家。