Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android應用開發Scroller詳解及源碼淺析

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

編輯:關於Android編程

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_DURATION
public 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;
}

可以看見該方法的作用其實就是實時計算滾動的偏移量(也是一個工具方法),同時判斷滾動是否結束(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就結束了,相關問題可以自行腦補,相信有了該篇的幫助你的自定義之路又暫時明朗了一段。

這裡寫圖片描述

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved