Android應用開發Scroller詳解及源碼淺析

來源:互聯網
上載者:User

Android應用開發Scroller詳解及源碼淺析
1 背景

大家都知道Android View提供了scrollTo()與scrollBy()方法來供我們進行View的滾動,但是有個問題就是他的滾動很蛋疼,疼在是瞬時挪動到指定位置的,這種對於追求使用者體驗的今天來說簡直是硬傷啊;為瞭解決這個問題Google給我們提供了一個牛叉的工具類Scroller,下面我們就深入淺出的來開戰這一工具類,將其玩爆,以便日後自訂控制項時如魚得水。

Scroller可以讓我們的滾動變得十分優雅,可以瞬間提升我們自訂控制項的逼格,但是瞭解該篇之前請先吃飽《Android應用座標系統全面詳解》一文,因為他們關係十分密切;當然喏,當你看完本文如果想看看Google自己對Scroller高端的使用則還可以繼續看看《Android應用ViewDragHelper詳解及部分源碼淺析》一文,哈哈。

PS:要過年了,公司一片動蕩。。。。。。

2 Scroller基礎執行個體

和以前博文一樣,開始源碼分析前先給出一個使用的基本例子作為引導,否則都不知道自己在看啥。這裡我們給出一個比較常見的東東—–側滑拉出收合(類似QQ List列表Item的效果)。如下是控制項Demo效果(請原諒我Ubuntu gif):

樣本源碼點我下載,不過只是Demo給出思路,細節沒有處理,也沒有進行完善,只是作為Scroller的Demo。下面是該控制項實現的核心代碼:

public class HorizontalFlingLayout extends LinearLayout {    private Scroller mScroller;    private View mLeftView;    private View mRightView;    private float mInitX, mInitY;    private float mOffsetX, mOffsetY;    public HorizontalFlingLayout(Context context) {        this(context, null);    }    public HorizontalFlingLayout(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public HorizontalFlingLayout(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        this.setOrientation(LinearLayout.HORIZONTAL);        mScroller = new Scroller(getContext(), null, true);    }    @Override    protected void onFinishInflate() {        super.onFinishInflate();        if (getChildCount() != 2) {            throw new RuntimeException("Only need two child view! Please check you xml file!");        }        mLeftView = getChildAt(0);        mRightView = getChildAt(1);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        switch (ev.getActionMasked()) {            case MotionEvent.ACTION_DOWN:                mInitX = ev.getX();                mInitY = ev.getY();                super.dispatchTouchEvent(ev);                return true;            case MotionEvent.ACTION_MOVE:                //>0為手勢向右下                mOffsetX = ev.getX() - mInitX;                mOffsetY = ev.getY() - mInitY;                //橫向手勢跟隨移動                if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > ViewConfiguration.getTouchSlop()) {                    int offset = (int) -mOffsetX;                    if (getScrollX() + offset > mRightView.getWidth() || getScrollX() + offset < 0) {                        return true;                    }                    this.scrollBy(offset, 0);                    mInitX = ev.getX();                    mInitY = ev.getY();                    return true;                }                break;            case MotionEvent.ACTION_CANCEL:            case MotionEvent.ACTION_UP:                //鬆手時刻滑動                int offset = ((getScrollX() / (float)mRightView.getWidth()) > 0.5) ? mRightView.getWidth() : 0;//                this.scrollTo(offset, 0);                mScroller.startScroll(this.getScrollX(), this.getScrollY(), offset-this.getScrollX(), 0);                invalidate();                mInitX = 0;                mInitY = 0;                mOffsetX = 0;                mOffsetY = 0;                break;        }        return super.dispatchTouchEvent(ev);    }    @Override    public void computeScroll() {        if (mScroller.computeScrollOffset()) {            this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());            postInvalidate();        }    }}

簡單吧,使用Scroller就能這麼優雅的滑動,不解釋,簡單的Demo,哈哈;有了這個基本映像我們直接高速——源碼探測,搞清源碼基本原理流程就能用的順手嘍。

3 Scroller源碼淺析

通過上面執行個體我們可以發現在自訂View的過程中使用Scroller的流程如所示:

既然有了這麼明確的流程圖,那我們下面就來依據這個流程簡單分析下Scroller的源碼。可以發現Scroller這類的代碼不多哇,確實是一個工具類,哈哈,我們先看下構造方法:

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}

可以看見,構造方法沒啥特殊的,只是一些基礎的設定,唯一要重點關注可能自訂的也就動畫插值器那個參數了,預設是ViscousFluidInterpolator的,我們可以自訂修改。兩參構造方法中其實也就是對第三個參數做了HONEYCOMB相容性處理,三參是所有構造方法最終調運的方法,其實也就是初始化了一些變數而已,沒啥重要的。

下面我們看看與Scroller相關的startScroll()和fling()方法,源碼如下:

//在我們想要滾動的地方調運,準備開始滾動,預設滾動時間為DEFAULT_DURATIONpublic void startScroll(int startX, int startY, int dx, int dy) {    startScroll(startX, startY, dx, dy, DEFAULT_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;}//在快速滑動鬆開的基礎上開始慣性滾動,滾動距離取決於fling的初速度public void fling(int startX, int startY, int velocityX, int velocityY,    int minX, int maxX, int minY, int maxY) {    ......    mMode = FLING_MODE;    mFinished = false;    ......    mStartX = startX;    mStartY = startY;    ......    mDistance = (int) (totalDistance * Math.signum(velocity));    mMinX = minX;    mMaxX = maxX;    mMinY = minY;    mMaxY = maxY;    ......    mFinalY = Math.min(mFinalY, mMaxY);    mFinalY = Math.max(mFinalY, mMinY);}

可以看見,上面這幾個美其名曰滑動的Scroller方法其實都只是一個幌子,沒有進行滑動,而是初始化了一堆成員變數;譬如滾動模式、開始時間、期間等,也就是說他們都只是工具方法而已,實質的滑動其實是需要我們在他後面手動調運View的invalidate()進行重新整理,然後在View進行重新整理時又會調運自己的View.computeScroll()方法(不瞭解View繪製的請看《Android應用程式層View繪製流程與源碼分析》一文),在View.computeScroll()方法中進行Scroller.computeScrollOffset()判斷與觸發View的滑動方法。

既然這樣那我們粗略給出View的繪製流程,詳細的請看《Android應用程式層View繪製流程與源碼分析》一文。當我們調運invalidate()會觸發View的如下方法:

public void draw(Canvas canvas) {    ......    /*     * Draw traversal performs several drawing steps which must be executed     * in the appropriate order:     *     *      1. Draw the background     *      2. If necessary, save the canvas' layers to prepare for fading     *      3. Draw view's content     *      4. Draw children     *      5. If necessary, draw the fading edges and restore layers     *      6. Draw decorations (scrollbars for instance)     */    ......    // Step 4, draw the children    dispatchDraw(canvas);    ......}

可以發現,View的draw()方法被觸發時總共會進行6步,最重要的一步我們看第四步,下面是第四步dispatchDraw()方法源碼:

protected void dispatchDraw(Canvas canvas) {}

可以看見,View的該方法為空白方法,那我們看下他子類ViewGroup的該方法,如下:

protected void dispatchDraw(Canvas canvas) {    ......    for (int i = 0; i < childrenCount; i++) {        ......        more |= drawChild(canvas, child, drawingTime);        ......    }    ......}

可以發現,ViewGroup的dispatchDraw()方法實質又跑到了drawChild()方法,如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {    return child.draw(canvas, this, drawingTime);}

額額,實質又是child的另一個draw()方法而已,我們回到View去看下這個方法,如下:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {    ......    if (!drawingWithRenderNode) {        computeScroll();        sx = mScrollX;        sy = mScrollY;    }    ......}

額額,這就解釋了為何View調運invalidate()就會觸發computeScroll()方法了。而ViewGroup最終調運scrollTo()方法都只能滾動內部子View的問題其實是因為ViewGroup它本身並沒有任何可畫的東西,它是一個透明的控制項,所以一般不會觸發onDraw()方法,但是當你給他設定背景等就會調用onDraw方法了,可是走的是繪製背景流程。

View相關的扯完了,下面我們來看看Scroller的computeScrollOffset()方法,下面我們簡單分析這個方法,如下:

//判斷滾動是否還在繼續,true繼續,false結束public boolean computeScrollOffset() {    //mFinished為true表示已經完成了滑動,直接返回為false    if (mFinished) {        return false;    }    //mStartTime為開始時的時間戳記,timePassed就是當前滑動期間    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);    //mDuration為我們設定的期間,噹噹前已滑動耗時timePassed小於總設定期間時才進入if    if (timePassed < mDuration) {        //mMode有兩中,如果調運startScroll()則為SCROLL_MODE模式,調運fling()則為FLING_MODE模式        switch (mMode) {        case SCROLL_MODE:        //根據Interpolator插值器計算在該時間段裡移動的距離賦值給mCurrX和mCurrY        final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);        mCurrX = mStartX + Math.round(x * mDeltaX);        mCurrY = mStartY + Math.round(x * mDeltaY);        break;        case FLING_MODE:        //各種數學運算擷取mCurrY、mCurrX,實質類似上面SCROLL_MODE,只是這裡時慣性的        ......        // 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 {        //認為滑動結束,mFinished置位true,標記結束,下一次再觸發該方法時一進來就判斷返回false了        mCurrX = mFinalX;        mCurrY = mFinalY;        mFinished = true;    }    return true;}

可以看見該方法的作用其實就是Realtime Compute滾動的位移量(也是一個工具方法),同時判斷滾動是否結束(true代表沒結束,false代表結束)。

到此整個Scroller就分析完了,剩下的全是各種getXXX、setXXX方法就沒啥意思了。

4 Scroller總結

基於上面的例子和分析我們進行如下總結:

public class Scroller  {    ......    public Scroller(Context context) {}    public Scroller(Context context, Interpolator interpolator) {}    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {}    //設定滾動期間    public final void setFriction(float friction) {}    //返復原動是否結束    public final boolean isFinished() {}    //強制終止滾動    public final void forceFinished(boolean finished) {}        //返復原動期間    public final int getDuration() {}    //返回當前滾動的位移量    public final int getCurrX() {}    public final int getCurrY() {}    //返回當前的速度    public float getCurrVelocity() {}    //返復原動起始點位移量    public final int getStartX() {}    public final int getStartY() {}        //返復原動結束位移量    public final int getFinalX() {}    public final int getFinalY() {}    //即時調用該方法擷取座標及判斷滑動是否結束,返回true動畫沒結束    public boolean computeScrollOffset() {}    //滑動到指定位置    public void startScroll(int startX, int startY, int dx, int dy) {}    public void startScroll(int startX, int startY, int dx, int dy, int duration) {}    //快速滑動鬆開手勢慣性滑動    public void fling(int startX, int startY, int velocityX, int velocityY,            int minX, int maxX, int minY, int maxY) {}    //終止動畫,滾到最終的x、y位置    public void abortAnimation() {}    //延長滾動的時間    public void extendDuration(int extend) {}    //返復原動開始經過的時間    public int timePassed() {}    //設定終止時位移量    public void setFinalX(int newX) {}    public void setFinalY(int newY) {}}

至此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.