Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 帶你從源碼的角度解析Scroller的滾動實現原理

Android 帶你從源碼的角度解析Scroller的滾動實現原理

編輯:關於Android編程

轉帖請注明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/17483273),請尊重他人的辛勤勞動成果,謝謝!

今天給大家講解的是Scroller類的滾動實現原理,可能很多朋友不太了解該類是用來干嘛的,但是研究Launcher的朋友應該對他很熟悉,Scroller類是滾動的一個封裝類,可以實現View的平滑滾動效果,什麼是實現View的平滑滾動效果呢,舉個簡單的例子,一個View從在我們指定的時間內從一個位置滾動到另外一個位置,我們利用Scroller類可以實現勻速滾動,可以先加速後減速,可以先減速後加速等等效果,而不是瞬間的移動的效果,所以Scroller可以幫我們實現很多滑動的效果。

在介紹Scroller類之前,我們先去了解View的scrollBy() 和scrollTo()方法的區別,在區分這兩個方法的之前,我們要先理解View 裡面的兩個成員變量mScrollX, mScrollY,X軸方向的偏移量和Y軸方向的偏移量,這個是一個相對距離,相對的不是屏幕的原點,而是View的左邊緣,舉個通俗易懂的例子,一列火車從吉安到深圳,途中經過贛州,那麼原點就是贛州,偏移量就是 負的吉安到贛州的距離,大家從getScrollX()方法中的注釋中就能看出答案來

 /**
     * Return the scrolled left position of this view. This is the left edge of
     * the displayed part of your view. You do not need to draw any pixels
     * farther left, since those are outside of the frame of your view on
     * screen.
     *
     * @return The left edge of the displayed part of your view, in pixels.
     */
    public final int getScrollX() {
        return mScrollX;
    }
現在我們知道了向右滑動 mScrollX就為負數,向左滑動mScrollX為正數,接下來我們先來看看 scrollTo()方法的源碼

  /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                invalidate();
            }
        }
    }
從該方法中我們可以看出,先判斷傳進來的(x, y)值是否和View的X, Y偏移量相等,如果不相等,就調用onScrollChanged()方法來通知界面發生改變,然後重繪界面,所以這樣子就實現了移動效果啦, 現在我們知道了scrollTo()方法是滾動到(x, y)這個偏移量的點,他是相對於View的開始位置來滾動的。在看看scrollBy()這個方法的代碼

 /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

原來他裡面調用了scrollTo()方法,那就好辦了,他就是相對於View上一個位置根據(x, y)來進行滾動,可能大家腦海中對這兩個方法還有點模糊,沒關系,還是舉個通俗的例子幫大家理解下,假如一個View,調用兩次scrollTo(-10, 0),第一次向右滾動10,第二次就不滾動了,因為mScrollX和x相等了,當我們調用兩次scrollBy(-10, 0),第一次向右滾動10,第二次再向右滾動10,他是相對View的上一個位置來滾動的。

對於scrollTo()和scrollBy()方法還有一點需要注意,這點也很重要,假如你給一個LinearLayout調用scrollTo()方法,並不是LinearLayout滾動,而是LinearLayout裡面的內容進行滾動,比如你想對一個按鈕進行滾動,直接用Button調用scrollTo()一定達不到你的需求,大家可以試一試,如果真要對某個按鈕進行scrollTo()滾動的話,我們可以在Button外面包裹一層Layout,然後對Layout調用scrollTo()方法。


了解了scrollTo()和scrollBy()方法之後我們就了解下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.
     */
    public Scroller(Context context, Interpolator interpolator) {
        mFinished = true;
        mInterpolator = interpolator;
        float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
        mDeceleration = SensorManager.GRAVITY_EARTH   // g (m/s^2)
                      * 39.37f                        // inch/meter
                      * ppi                           // pixels per inch
                      * ViewConfiguration.getScrollFriction();
    }
只有兩個構造方法,第一個只有一個Context參數,第二個構造方法中指定了Interpolator,什麼Interpolator呢?中文意思插補器,了解Android動畫的朋友都應該熟悉
Interpolator,他指定了動畫的變化率,比如說勻速變化,先加速後減速,正弦變化等等,不同的Interpolator可以做出不同的效果出來,第一個使用默認的Interpolator(viscous)


接下來我們就要在Scroller類裡面找滾動的方法,我們從名字上面可以看出startScroll()應該是個滾動的方法,我們來看看其源碼吧

    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;
        // This controls the viscous fluid effect (how much of it)
        mViscousFluidScale = 8.0f;
        // must be set to 1.0 (used in viscousFluid())
        mViscousFluidNormalize = 1.0f;
        mViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
    }
在這個方法中我們只看到了對一些滾動的基本設置動作,比如設置滾動模式,開始時間,持續時間等等,並沒有任何對View的滾動操作,也許你正納悶,不是滾動的方法干嘛還叫做startScroll(),稍安勿躁,既然叫開始滾動,那就是對滾動的滾動之前的基本設置咯。

    /**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.  loc will be altered to provide the
     * new location.
     */ 
    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                float x = (float)timePassed * mDurationReciprocal;
    
                if (mInterpolator == null)
                    x = viscousFluid(x); 
                else
                    x = mInterpolator.getInterpolation(x);
    
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                float timePassedSeconds = timePassed / 1000.0f;
                float distance = (mVelocity * timePassedSeconds)
                        - (mDeceleration * timePassedSeconds * timePassedSeconds / 2.0f);
                
                mCurrX = mStartX + Math.round(distance * mCoeffX);
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);
                
                mCurrY = mStartY + Math.round(distance * mCoeffY);
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);
                
                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }
我們在startScroll()方法的時候獲取了當前的動畫毫秒賦值給了mStartTime,在computeScrollOffset()中再一次調用AnimationUtils.currentAnimationTimeMillis()來獲取動畫
毫秒減去mStartTime就是持續時間了,然後進去if判斷,如果動畫持續時間小於我們設置的滾動持續時間mDuration,進去switch的SCROLL_MODE,然後根據Interpolator來計算出在該時間段裡面移動的距離,賦值給mCurrX, mCurrY, 所以該方法的作用是,計算在0到mDuration時間段內滾動的偏移量,並且判斷滾動是否結束,true代表還沒結束,false則表示滾動介紹了,Scroller類的其他的方法我就不提了,大都是一些get(), set()方法。

看了這麼多,到底要怎麼才能觸發滾動,你心裡肯定有很多疑惑,在說滾動之前我要先提另外一個方法computeScroll(),該方法是滑動的控制方法,在繪制View時,會在draw()過程調用該方法。我們先看看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的繪制方法draw()

 public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * 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 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            final Drawable background = mBackground;
            if (background != null) {
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;

                if (mBackgroundSizeChanged) {
                    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
                    mBackgroundSizeChanged = false;
                }

                if ((scrollX | scrollY) == 0) {
                    background.draw(canvas);
                } else {
                    canvas.translate(scrollX, scrollY);
                    background.draw(canvas);
                    canvas.translate(-scrollX, -scrollY);
                }
            }
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);

            // we're done...
            return;
        }

        ......
        ......
        ......

我們只截取了draw()的部分代碼,這上面11-16行為我們寫出了繪制一個View的幾個步驟,我們看看第四步繪制孩子的時候會觸發dispatchDraw()這個方法,來看看源碼是什麼內容

 /**
     * Called by draw to draw the child views. This may be overridden
     * by derived classes to gain control just before its children are drawn
     * (but after its own view has been drawn).
     * @param canvas the canvas on which to draw the view
     */
    protected void dispatchDraw(Canvas canvas) {

    }
好吧,又是定義的一個空方法,給子類來重寫的方法,所以我們找到View的子類ViewGroup來看看該方法的具體實現邏輯

    @Override
    protected void dispatchDraw(Canvas canvas) {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;

        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;

            final boolean buildCache = !isHardwareAccelerated();
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, count);
                    bindLayoutAnimation(child);
                    if (cache) {
                        child.setDrawingCacheEnabled(true);
                        if (buildCache) {                        
                            child.buildDrawingCache(true);
                        }
                    }
                }
            }

            final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }

            controller.start();

            mGroupFlags &= ~FLAG_RUN_ANIMATION;
            mGroupFlags &= ~FLAG_ANIMATION_DONE;

            if (cache) {
                mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
            }

            if (mAnimationListener != null) {
                mAnimationListener.onAnimationStart(controller.getAnimation());
            }
        }

        int saveCount = 0;
        final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
        if (clipToPadding) {
            saveCount = canvas.save();
            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                    mScrollX + mRight - mLeft - mPaddingRight,
                    mScrollY + mBottom - mTop - mPaddingBottom);

        }

        // We will draw our child's animation, let's reset the flag
        mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;

        boolean more = false;
        final long drawingTime = getDrawingTime();

        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        } else {
            for (int i = 0; i < count; i++) {
                final View child = children[getChildDrawingOrder(count, i)];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        }

        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            final ArrayList disappearingChildren = mDisappearingChildren;
            final int disappearingCount = disappearingChildren.size() - 1;
            // Go backwards -- we may delete as animations finish
            for (int i = disappearingCount; i >= 0; i--) {
                final View child = disappearingChildren.get(i);
                more |= drawChild(canvas, child, drawingTime);
            }
        }

        if (debugDraw()) {
            onDebugDraw(canvas);
        }

        if (clipToPadding) {
            canvas.restoreToCount(saveCount);
        }

        // mGroupFlags might have been updated by drawChild()
        flags = mGroupFlags;

        if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
            invalidate(true);
        }

        if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
                mLayoutAnimationController.isDone() && !more) {
            // We want to erase the drawing cache and notify the listener after the
            // next frame is drawn because one extra invalidate() is caused by
            // drawChild() after the animation is over
            mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
            final Runnable end = new Runnable() {
               public void run() {
                   notifyAnimationListener();
               }
            };
            post(end);
        }
    }
這個方法代碼有點多,但是我們還是挑重點看吧,從65-79行可以看出 在dispatchDraw()裡面會對ViewGroup裡面的子View調用drawChild()來進行繪制,接下來我們來看看drawChild()方法的代碼

 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
	......
	......

    if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&
                (child.mPrivateFlags & DRAW_ANIMATION) == 0) {
            return more;
        }

        child.computeScroll();

        final int sx = child.mScrollX;
        final int sy = child.mScrollY;

        boolean scalingRequired = false;
        Bitmap cache = null;

	......
	......

}
只截取了部分代碼,看到child.computeScroll()你大概明白什麼了吧,轉了老半天終於找到了computeScroll()方法被觸發,就是ViewGroup在分發繪制自己的孩子的時候,會對其子View調用computeScroll()方法


整理下思路,來看看View滾動的實現原理,我們先調用Scroller的startScroll()方法來進行一些滾動的初始化設置,然後迫使View進行繪制,我們調用View的invalidate()或postInvalidate()就可以重新繪制View,繪制View的時候會觸發computeScroll()方法,我們重寫computeScroll(),在computeScroll()裡面先調用Scroller的computeScrollOffset()方法來判斷滾動有沒有結束,如果滾動沒有結束我們就調用scrollTo()方法來進行滾動,該scrollTo()方法雖然會重新繪制View,但是我們還是要手動調用下invalidate()或者postInvalidate()來觸發界面重繪,重新繪制View又觸發computeScroll(),所以就進入一個循環階段,這樣子就實現了在某個時間段裡面滾動某段距離的一個平滑的滾動效果
也許有人會問,干嘛還要調用來調用去最後在調用scrollTo()方法,還不如直接調用scrollTo()方法來實現滾動,其實直接調用是可以,只不過scrollTo()是瞬間滾動的,給人的用戶體驗不太好,所以Android提供了Scroller類實現平滑滾動的效果。為了方面大家理解,我畫了一個簡單的調用示意圖

\

好了,講到這裡就已經講完了Scroller類的滾動實現原理啦,不知道大家理解了沒有,Scroller能實現很多滾動的效果,由於考慮到這篇文章的篇幅有點長,所以這篇文章就不帶領大家來使用Scroller類了,我將在下一篇文章將會帶來Scroller類的使用,希望大家到時候關注下,有疑問的同學在下面留言,我會為大家解答的!


很榮幸我能夠成為CSDN 2013年度博客之星評選的候選人,希望繼續得到大家的支持與鼓勵,看到的朋友幫我投上寶貴的一票吧!

投票地址:http://vote.blog.csdn.net/blogstaritem/blogstar2013/xiaanming



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