Scroller源碼分析

本文分析版本: Android API 22

1.簡介

Android開發(fā)中怖竭,如果我們希望使一個View滑動的話,除了使用屬性動畫外叉谜。我們還可以使用系統(tǒng)提供給我們的兩個類ScrollerOverScroller用來實現(xiàn)彈性滑動旗吁。在我以前的一篇ViewDragHelper源碼分析中我們有講到過Scroller的作用。那么我們今天就來仔細分析一下Scroller的使用方法以及實現(xiàn)方式停局。

2.使用方法

在看Scroller的使用方法之前我們需要先了解一下View中的scrollBy()scrollTo()方法很钓,scrollTo()方法的實現(xiàn)如下:


    public void scrollTo(int x, int y) {
        //如果當前偏移量變化
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            //賦值偏移量
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            //回調(diào)onScrollChanged方法
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

scrollTo()是指將前視圖內(nèi)容橫向偏移x距離,縱向偏移y距離董栽。注意這里是View的內(nèi)容的偏移码倦,而不是View本身。而scrollBy()方法如下:

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

scrollBy()方法里直接調(diào)用了scrollTo()方法锭碳,表示在當前偏移量的基礎上繼續(xù)偏移(x,y)≡現(xiàn)在我們來看看Scroller的用法。SkyScrollerDemo是我寫的一個ScrollerOverScroller的使用demo擒抛。下面的用法都是來自于這個demo里推汽,大家可以clone下來配合本文一起閱讀。本文我們主要研究Scroller歧沪。對于OverScroller我在demo里也寫了相關的使用方法歹撒,在本文的最后我們再做討論。

Scroller一般需要配合重寫computeScroll()一起使用诊胞,代碼如下:

public class ScrollTextView extends TextView {
    private Context mContext;
    private Scroller mScroller;

    public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        init();
    }

    private void init() {
        mScroller = new Scroller(mContext);
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            offsetLeftAndRight(mScroller.getCurrX() - mLeft);
            offsetTopAndBottom(mScroller.getCurrY() - mTop);
            invalidate();
        }
    }
    //以mLeft,mTop為初始點暖夭,在DEFAULT_DURATION的時間內(nèi),在Y軸上滑動-400的偏移量
    public void startScrollerScroll() {
        mScroller.startScroll(mLeft, mTop, 0, -400, DEFAULT_DURATION);
        invalidate();
    }
    //以mLeft,mTop為初始點,并以Y方向上-5000的加速度滑動鳞尔,最小Y坐標為200嬉橙,最大Y坐標為1200
    public void startScrollerFling() {
        mScroller.fling(mLeft, mTop, 0, -5000, mLeft, mLeft, 200, 1200);
        invalidate();
    }
}

在上面的代碼里,當我們調(diào)用startScrollerScroll()startScrollerFling()方法時我們就發(fā)現(xiàn)View滑動了寥假。如果以前沒了解過Scroller的同學可能會不理解市框。這里大致分析一下調(diào)用流程,首先我們要知道Scroller其實只負責計算糕韧,它并不負責滑動View枫振,當我們調(diào)用了ScrollerstartScrollerScroll()方法時,我們緊接著調(diào)用了invalidate()方法萤彩。invalidate()方法會使View重新繪制粪滤。因此會調(diào)用Viewdraw()方法,在Viewdraw()方法中又會去調(diào)用computeScroll()方法雀扶,computeScroll()方法在View中是一個空實現(xiàn)杖小,所以需要我們自己實現(xiàn)computeScroll()方法。在上面的computeScroll()方法中愚墓,我們調(diào)用了mScroller.computeScrollOffset()方法來計算當前滑動的偏移量予权。如果還在滑動過程中就會返回true。所以我們就能在if中通過Scroller拿到當前的滑動坐標從而做任何我們想做的處理浪册。在demo里我們根據(jù)滑動的偏移量來改變了View的坐標偏移量扫腺。從而形成了滑動動畫。下面我們解釋一下Scroller的兩個方法的具體作用:

1.startScroll(int startX, int startY, int dx, int dy, int duration):

通過起始點村象、偏移的距離和滑動的時間來開始滑動笆环。

  • startX 起始滑動點的X坐標
  • startY 起始滑動點的Y坐標
  • dx 滑動的水平偏移量。>0 則表示往左滑動厚者。
  • dy 滑動的垂直偏移量躁劣。>0 則表示往上滑動。
  • duration 滑動執(zhí)行的時間

2.fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) :

基于一個快速滑動手勢下的滑動库菲≌送滑動的距離與這個手勢最初的加速度有關。

  • startX 起始滑動點的X坐標
  • startY 起始滑動點的Y坐標
  • velocityX X方向上的加速度
  • velocityY Y方向上的加速度
  • minX X方向上滑動的最小值蝙昙,不會滑動超過這個點
  • maxX X方向上滑動的最大值闪萄,不會滑動超過這個點
  • minY Y方向上滑動的最小值,不會滑動超過這個點
  • maxY Y方向上滑動的最大值奇颠,不會滑動超過這個點

3.源碼分析

我們依然通過調(diào)用流程來分析Scroller的實現(xiàn):

1.構(gòu)造方法

public Scroller(Context context) {
    this(context, null);
}

public Scroller(Context context, Interpolator interpolator) {
    this(context, interpolator,
            context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}

public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
    mFinished = true;
    if (interpolator == null) {
        mInterpolator = new ViscousFluidInterpolator();
    } else {
        mInterpolator = interpolator;
    }
    mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
    mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
    mFlywheel = flywheel;

    mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}

最終都會調(diào)用最后一個構(gòu)造方法败去。必須傳入Context對象×揖埽可以傳入自定義的interpolator和是否支持飛輪flywheel的功能圆裕,當然這兩個并不是必須的广鳍。如果不傳入interpolator會默認創(chuàng)建一個ViscousFluidInterpolator,從字面意義上看是一個粘性流體插值器吓妆。對于flywheel是指是否支持在滑動過程中赊时,如果有新的fling()方法調(diào)用是否累加加速度。如果不傳默認在2.3以上都會支持行拢。剩下就是初始化了一些用于計算的參數(shù)祖秒。這樣就完成了Scroller的初始化了。下面我們來看看startScroll()方法的實現(xiàn):

2.startScroll()方法的實現(xiàn)

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
  // mMode 分兩種方式 1.滑動:SCROLL_MODE 2. 加速度滑動:FLING_MODE
  mMode = SCROLL_MODE;
  // 是否滑動結(jié)束 這里是開始所以設置為false
  mFinished = false;
  // 滑動的時間
  mDuration = duration;
  // 開始的時間
  mStartTime = AnimationUtils.currentAnimationTimeMillis();
  // 開始滑動點的X坐標
  mStartX = startX;
  // 開始滑動點的Y坐標
  mStartY = startY;
  // 最終滑動到位置的X坐標
  mFinalX = startX + dx;
  // 最終滑動到位置的Y坐標
  mFinalY = startY + dy;
  // X方向上滑動的偏移量
  mDeltaX = dx;
  // Y方向上滑動的偏移量
  mDeltaY = dy;
  // 持續(xù)時間的倒數(shù) 最終用來計算得到插值器返回的值
  mDurationReciprocal = 1.0f / (float) mDuration;
}

很簡單只是一些變量的賦值舟奠。根據(jù)我們前面使用方法里的分析竭缝,最終會調(diào)用computeScrollOffset()方法:

3.computeScrollOffset() 方法中 SCROLL_MODE 的實現(xiàn)

// 當你需要知道新的位置的時候調(diào)用這個方法,如果動畫還未結(jié)束則返回true
public boolean computeScrollOffset() {
    //如果已經(jīng)結(jié)束 則直接返回false
    if (mFinished) {
        return false;
    }
    //得到以及度過的時間
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

    //如果還在動畫時間內(nèi)
    if (timePassed < mDuration) {
        switch (mMode) {
            case SCROLL_MODE:
                // 根據(jù)timePassed * mDurationReciprocal,從mInterpolator中取出當前需要偏移量的比例
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                // 賦值給 mCurrX沼瘫,mCurrY
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                ...
                break;
        }
    }
    else {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}

首先的到當前時間與滑動開始時間的時間差抬纸,如果還在滑動時間內(nèi)則通過插值器獲得當前的進度并乘以總偏移量并賦值給mCurrXmCurrY耿戚。如果已經(jīng)結(jié)束則直接將mFinalXmFinalY賦值并將mFinished設置?為true湿故。所以這樣我們就能通過getCurrX()getCurrY()來得到對應的mCurrXmCurrY來做相應的處理了。整個Scroll的過程就是這樣了膜蛔。

4.fling()方法的實現(xiàn)

    public void fling(int startX, int startY, int velocityX, int velocityY,
                      int minX, int maxX, int minY, int maxY) {
        // 如果前一次滑動還未結(jié)束坛猪,又調(diào)用了新的fling()方法時,
        // 則累加相同方向上加速度
        if (mFlywheel && !mFinished) {
            float oldVel = getCurrVelocity();

            float dx = (float) (mFinalX - mStartX);
            float dy = (float) (mFinalY - mStartY);
            float hyp = FloatMath.sqrt(dx * dx + dy * dy);

            float ndx = dx / hyp;
            float ndy = dy / hyp;

            float oldVelocityX = ndx * oldVel;
            float oldVelocityY = ndy * oldVel;
            if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
                    Math.signum(velocityY) == Math.signum(oldVelocityY)) {
                velocityX += oldVelocityX;
                velocityY += oldVelocityY;
            }
        }

        //設置為FLING_MODE
        mMode = FLING_MODE;
        mFinished = false;
        //根據(jù)勾股定理獲得總加速度
        float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);

        mVelocity = velocity;
        // 通過加速度得到滑動持續(xù)時間
        mDuration = getSplineFlingDuration(velocity);
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;

        float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;

        double totalDistance = getSplineFlingDistance(velocity);
        mDistance = (int) (totalDistance * Math.signum(velocity));

        mMinX = minX;
        mMaxX = maxX;
        mMinY = minY;
        mMaxY = maxY;

        mFinalX = startX + (int) Math.round(totalDistance * coeffX);
        // Pin to mMinX <= mFinalX <= mMaxX
        mFinalX = Math.min(mFinalX, mMaxX);
        mFinalX = Math.max(mFinalX, mMinX);

        mFinalY = startY + (int) Math.round(totalDistance * coeffY);
        // Pin to mMinY <= mFinalY <= mMaxY
        mFinalY = Math.min(mFinalY, mMaxY);
        mFinalY = Math.max(mFinalY, mMinY);
    }

依然是為計算需要的各種變量賦值飞几。因為引入了加速度的概念所以變得相對復雜砚哆,首先先判斷了如果一次滑動未結(jié)束又觸發(fā)另一次滑動時独撇,是否需要累加加速度屑墨。然后是設置mModeFLING_MODE。然后根據(jù)velocityXvelocityY算出總的加速度velocity纷铣,緊接著算出這個加速度下可以滑動的距離mDistance卵史。最后再通過xy方向上的加速度比值以及我們設定的最大值和最小值來給mFinalXmFinalY賦值。賦值結(jié)束后搜立,通過調(diào)用invalidate()以躯,最終依然會調(diào)用computeScrollOffset()方法:

5.computeScrollOffset() 方法中 FLING_MODE 的實現(xiàn)


    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

        if (timePassed < mDuration) {
            switch (mMode) {
                case SCROLL_MODE:
                    ...
                    break;
                case FLING_MODE:
                    // 當前已滑動的時間與總滑動時間的比值
                    final float t = (float) timePassed / mDuration;
                    final int index = (int) (NB_SAMPLES * t);
                    // 距離系數(shù)
                    float distanceCoef = 1.f;
                    // 加速度系數(shù)
                    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 與mCurrY
                    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);

                    // 如果到達了終點 則結(jié)束
                    if (mCurrX == mFinalX && mCurrY == mFinalY) {
                        mFinished = true;
                    }

                    break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

由于fling()方法中將mMode賦值為FLING_MODE。所以我們直接來看FLING_MODE中的代碼啄踊∮巧瑁可以看出根據(jù)當前滑動時間與總滑動時間的比例。再根據(jù)一個SPLINE_POSITION數(shù)組計算出了距離系數(shù)distanceCoef與加速度系數(shù)velocityCoef颠通。再根據(jù)這兩個系數(shù)計算出當前加速度與當前的mCurrXmCurrY址晕。關于SPLINE_POSITION的初始化是在下面的靜態(tài)代碼塊里賦值的:

    static {
        float x_min = 0.0f;
        float y_min = 0.0f;
        for (int i = 0; i < NB_SAMPLES; i++) {
            final float alpha = (float) i / NB_SAMPLES;

            float x_max = 1.0f;
            float x, tx, coef;
            while (true) {
                x = x_min + (x_max - x_min) / 2.0f;
                coef = 3.0f * x * (1.0f - x);
                tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
                if (Math.abs(tx - alpha) < 1E-5) break;
                if (tx > alpha) x_max = x;
                else x_min = x;
            }
            SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;

            float y_max = 1.0f;
            float y, dy;
            while (true) {
                y = y_min + (y_max - y_min) / 2.0f;
                coef = 3.0f * y * (1.0f - y);
                dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
                if (Math.abs(dy - alpha) < 1E-5) break;
                if (dy > alpha) y_max = y;
                else y_min = y;
            }
            SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
        }
        SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
    }

我并沒有看懂這段代碼的實際意義。網(wǎng)上也沒有找到比較清晰的解釋顿锰。通過debug得知SPLINE_POSITION是一個長度為101并且從0-1遞增數(shù)組谨垃。猜想這應該是一個函數(shù)模型并且最終用于計算出滑動過程中的加速度與位置启搂。如果有同學能詳細解釋這段代碼的作用,歡迎在這篇文章留言刘陶。至此Scroller的兩個主要方法的實現(xiàn)我們就分析完了胳赌。

4.OverScroller解析

OverScroller是對Scroller的拓展,它在Scroller的基礎上拓展出了更多的方法匙隔。OverScrollerfling方法支持滑動到終點之后并超出一段距離并返回疑苫,類似于彈性效果。另外一個springBack()方法是指將指定的點平滑滾動到指定的終點上纷责。這個終點由設置的參數(shù)決定缀匕。原理我們就不再探究了,大家可以自行研究這兩個類的差別碰逸。最后具體的使用方法在文章最上面的demo里都有提供乡小。可以clone下來幫助理解饵史。

我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉(zhuǎn)發(fā)更多有用的知識給大家.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末满钟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子胳喷,更是在濱河造成了極大的恐慌湃番,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吭露,死亡現(xiàn)場離奇詭異吠撮,居然都是意外死亡,警方通過查閱死者的電腦和手機讲竿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門泥兰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人题禀,你說我怎么就攤上這事鞋诗。” “怎么了迈嘹?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵削彬,是天一觀的道長。 經(jīng)常有香客問我秀仲,道長融痛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任神僵,我火速辦了婚禮雁刷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘挑豌。我一直安慰自己安券,他們只是感情好墩崩,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著侯勉,像睡著了一般鹦筹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上址貌,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天铐拐,我揣著相機與錄音,去河邊找鬼练对。 笑死遍蟋,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的螟凭。 我是一名探鬼主播虚青,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼螺男!你這毒婦竟也來了棒厘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤下隧,失蹤者是張志新(化名)和其女友劉穎奢人,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淆院,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡何乎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了土辩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片支救。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖脯燃,靈堂內(nèi)的尸體忽然破棺而出搂妻,到底是詐尸還是另有隱情蒙保,我是刑警寧澤辕棚,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站邓厕,受9級特大地震影響逝嚎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜详恼,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一补君、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧昧互,春花似錦挽铁、人聲如沸伟桅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽楣铁。三九已至,卻和暖如春更扁,著一層夾襖步出監(jiān)牢的瞬間盖腕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工浓镜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留溃列,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓膛薛,卻偏偏與公主長得像听隐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子哄啄,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容