Android源碼分析-Scroller

本篇文章主要介紹了一下Scroller的使用並對其源碼進行了簡單的分析,感興趣的朋友可以看一下。

基本用法:

Scroller有兩個比較重要且常用的方法:startScroll和fling,特別是第一個方法我們在定義View或ViewGroup的時候經常用到,我寫了一個簡單的定義ViewGroup的小demo演示這兩個方法的用法,先看下效果圖:
startScroll方法:
這個效果比較常見,效果類似於ViewPager控件,當我們鬆開鼠標後,我們使用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的下載鏈接
如果大家有什麼疑問或文章中有講的不對的地方,歡迎大家提出來討論,謝謝大家。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章