Android Scroller源碼解析

來源:互聯網
上載者:User

標籤:

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源碼解析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.