標籤:
1. 前言
通過view本身提供的scrollTo/scrollBy方法實現滑動,其過程是瞬間的,想要實現彈性滑動的時候,需要用scroller來實現。Android裡Scroller類是為了實現View平滑滾動的一個Helper類。通常在自訂的View時使用,在View中定義一個私人成員mScroller = new Scroller(context)。mScroller本身,並不會導致View的滾動,通常是用mScroller記錄/計算View滾動的位置,需要重寫View的computeScroll(),配合view的重新整理,完成實際的滾動,後面會有詳細的源碼分析。
2.相關API介紹如下
mScroller.getCurrX() //擷取mScroller當前水平滾動的位置 mScroller.getCurrY() //擷取mScroller當前豎直滾動的位置 mScroller.getFinalX() //擷取mScroller最終停止的水平位置 mScroller.getFinalY() //擷取mScroller最終停止的豎直位置 mScroller.setFinalX(int newX) //設定mScroller最終停留的水平位置,沒有動畫效果,直接跳到目標位置 mScroller.setFinalY(int newY) //設定mScroller最終停留的豎直位置,沒有動畫效果,直接跳到目標位置 //滾動,startX, startY為開始滾動的位置,dx,dy為滾動的位移量, duration為完成滾動的時間 mScroller.startScroll(int startX, int startY, int dx, int dy) //使用預設完成時間250ms mScroller.startScroll(int startX, int startY, int dx, int dy, int duration) mScroller.computeScrollOffset() //傳回值為boolean,true說明滾動尚未完成,false說明滾動已經完成。這是一個很重要的方法,通常放在View.computeScroll()中,用來判斷是否滾動是否結束。
3.範例程式碼
一般滑動一個view,需要自訂view,然後實現smoothScrollTo(),重寫computeScroll方法。代碼如下:
package com.view.viewtest;import android.content.Context;import android.util.AttributeSet;import android.widget.LinearLayout;import android.widget.Scroller;public class CustomView extends LinearLayout { private static final String TAG = "Scroller"; private Scroller mScroller; public CustomView(Context context, AttributeSet attrs) { super(context, attrs); mScroller = new Scroller(context); } //調用此方法滾動到目標位置 public void smoothScrollTo(int fx, int fy) { int dx = fx - mScroller.getFinalX(); int dy = fy - mScroller.getFinalY(); smoothScrollBy(dx, dy); } //調用此方法設定滾動的相對位移 public void smoothScrollBy(int dx, int dy) { //設定mScroller的滾動位移量 mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy); invalidate();//這裡必須調用invalidate()才能保證computeScroll()會被調用,否則不一定會重新整理介面,看不到滾動效果 } @Override public void computeScroll() { //先判斷mScroller滾動是否完成 if (mScroller.computeScrollOffset()) { //這裡調用View的scrollTo()完成實際的滾動 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); //必須調用該方法,否則不一定能看到滾動效果 postInvalidate(); } super.computeScroll(); }}
4.源碼分析
首先,看下scroller的構造方法:
/** * Create a Scroller with the default duration and interpolator. */ public Scroller(Context context) { this(context, null); } /** * Create a Scroller with the specified interpolator. If the interpolator is * null, the default (viscous) interpolator will be used. "Flywheel" behavior will * be in effect for apps targeting Honeycomb or newer. */ public Scroller(Context context, Interpolator interpolator) { this(context, interpolator, context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); }
只有兩個構造方法,第一個只有一個Context參數,第二個構造方法中指定了Interpolator,什麼Interpolator呢?中文意思插補器,瞭解Android動畫的朋友都應該熟悉Interpolator,他指定了動畫的變動率,比如說勻速變化,先加速後減速,正弦變化等等,不同的Interpolator可以做出不同的效果出來,第一個使用預設的Interpolator(viscous)
上面範例程式碼是scroller的典型使用方法,使用了第一種構造方法,隨後我們調用自訂view的smoothScrollTo(int fx, int fy)方法的時候,方法內部會調用構造出的scroller的startScroll(int startX, int startY, int dx, int dy, int duration)方法,startScroll()源碼如下所示:
//滾動,startX, startY為開始滾動的位置,dx,dy為滾動的位移量, duration為完成滾動的時間 public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration; }
雖然方法名稱為開始滑動,但是並沒有讓view滑動,很明顯,內部都是一些賦值操作,沒有滑動的相關代碼。那為什麼view可以滑動呢?看下範例程式碼裡面,在startScroll方法下面,立即調用了invalidate()方法,而invalidate()方法會導致view的重繪。
invalidate();//這裡必須調用invalidate()才能保證computeScroll()會被調用,否則不一定會重新整理介面,看不到滾動效果
通過上面的注釋,可以知道,真正的滑動,是在computeScroll()方法裡面實現的,為什麼調用invalidate()才能保證computeScroll()會被調用?invalidate()方法會導致view的重繪,及調用draw()方法,我們繼續看下view的draw()方法的源碼:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { ...省略很多行 int sx = 0; int sy = 0; if (!drawingWithRenderNode) { computeScroll(); sx = mScrollX; sy = mScrollY; } final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode; final boolean offsetForScroll = cache == null && !drawingWithRenderNode; int restoreTo = -1; if (!drawingWithRenderNode || transformToApply != null) { restoreTo = canvas.save(); } if (offsetForScroll) { canvas.translate(mLeft - sx, mTop - sy); } else { if (!drawingWithRenderNode) { canvas.translate(mLeft, mTop); } if (scalingRequired) { if (drawingWithRenderNode) { // TODO: Might not need this if we put everything inside the DL restoreTo = canvas.save(); } // mAttachInfo cannot be null, otherwise scalingRequired == false final float scale = 1.0f / mAttachInfo.mApplicationScale; canvas.scale(scale, scale); } }...省略很多行
可以看到,draw方法會去調用computeScroll()方法,在view中computeScroll其實是一個空方法,
如下所示:
/** * Called by a parent to request that a child update its values for mScrollX * and mScrollY if necessary. This will typically be done if the child is * animating a scroll using a {@link android.widget.Scroller Scroller} * object. */ public void computeScroll() { }
很明顯,需要我們自訂view的時候,去重寫,去實現。上面我們的範例程式碼,已經實現了該方法,所以才能實現滑動。
下面通過圖的方式,來讓更好的理解滑動過程
startScroll->invalidate()->draw()->computeScroll()->scrollTo()->postInvalidate()->draw()->computeScroll()...不斷重複,一直完成滑動過程。
最後,看下computeScrollOffset()方法,在computeScroll()方法裡面調用的,如下所示:
//先判斷mScroller滾動是否完成if (mScroller.computeScrollOffset())
同樣,我們看下computeScrollOffset()的源碼:
/** * Call this when you want to know the new location. If it returns true, * the animation is not yet finished. */ public boolean computeScrollOffset() { if (mFinished) { return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); 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; mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); // Pin to mMinX <= mCurrX <= mMaxX mCurrX = Math.min(mCurrX, mMaxX); mCurrX = Math.max(mCurrX, mMinX); 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; } return true; }
代碼不是很多,比較容易理解,我們一起梳理下。
在startScroll()方法中把當前的動畫毫秒值賦值給了mStartTime,在computeScrollOffset()方法中再一次執行AnimationUtils.currentAnimationTimeMillis()來擷取動畫毫秒,然後減去mStartTime就是進行了多少時間,然後在跟mDuration進去判斷,如果動畫進行時間小於我們設定的滾動期間mDuration,進去switch的SCROLL_MODE,然後根據Interpolator來計算出在該時間段裡面移動的距離,賦值給mCurrX, mCurrY, 所以該方法的作用是,計算在0到mDuration時間段內滾動的位移量,並且判斷滾動是否結束,true代表還沒結束,false則表示滾動介紹了。
最後調用scrollTo,則執行了滑動事件。
//這裡調用View的scrollTo()完成實際的滾動scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
好了,整個scroller的源碼到這裡基本結束了,下一篇講介紹,更加深入的使用。如果有不清楚的,或者有不對的地方,歡迎留言一起探討。
Android Scroller源碼解析