Android 淺談scrollTo和scrollBy原始碼

那個人發表於2017-11-16

一.寫在前面

在View的幾種移動方法中我相信Scorller+scrollTo或者scrollBy是大家比較接受.我們再使用的時候總是會碰到一些奇怪的問題,可以得出以下幾點:

  • scrollTo和scrollBy只是移動自己的內容.
    也就是如果ViewGroup設定scrollTo或者scrollBy的話,只有它的子View會有位移效果.如果是TextView設定scrollTo或者scrollBy的話只會讓它內部的文字發生位移.

  • scrollBy還是呼叫的scrollTo,但scrollBy的起始座標是相對於上次結束時的mScrollX和mScrollY.scroolTo的起始座標是相對於父佈局的左上角,之後起始座標是不會變的.如下程式碼,Button:scroolto的內容只能發生一次位移.Button:scroolby的內容可以多次位移.

Button scroolto,scroolby;

 scroolto = findViewById(R.id.scroolto);
 scroolby = findViewById(R.id.scroolby);

item.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                scroolto.scrollTo(-100,-100);
                scroolby.scrollBy(-100,-100);
            }
        });複製程式碼
  • 當ViewGroup設定scrollTo或者scrollBy的時候,它的子View發生位移但是子View的getX()和getY()是不會發生變化的.子View的位於螢幕中的位置會發生變化,如下程式碼:
        //獲取位於螢幕中的位置
        int[] location = new int[2];
        scroolto.getLocationOnScreen(location);
        Log.e("WANG","ScreenX"+location[0]);
        Log.e("WANG","ScreenY"+location[1]);

        //獲取位於父佈局中的位置
        Log.e("WANG","ViewX"+scroolto.getX());
        Log.e("WANG","ViewY"+scroolto.getY());複製程式碼
  • 最後一點就是Android中View檢視是沒有邊界的,Canvas是沒有邊界的.

這些就是小編大概的瞭解了,可能不太全面,希望讀者們能指點一二.接下來我們會有個疑問,為什麼再使用scrollTo(100,100)或者scrollBy(100,100)的時候,我們的內容是往負方向移動的呢?設定成-100,-100的時候是往正方向移動的呢,大家可以去看一下Android座標系?

這裡呢我們要知道,控制元件的x和y座標的增值是正值的話就是往正方向移動,負值就是往負方向移動.這是一般的規則.然後scrollTo和scrollBy卻是例外.我們之後去看下原始碼了,這裡呢會涉及到View的繪製過程,也就是從Activity的setContentView()到View出現在手機螢幕中的過程.這裡就大概說一下,下一篇會詳細的介紹.

二.原始碼分析

 /**
     * 為你的View設定滾動的位置,這裡會呼叫{@link #onScrollChanged(int, int, int, int)} 隨後這個View將呼叫失效.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
public void scrollTo(int x, int y) {
//這裡是不是就可以解釋為什麼scrollTo只能呼叫一次.
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            //我們可以監聽到的方法
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            //這裡有個判斷的條件,也就是postInvalidateOnAnimation這個方法無論scrollbars是否開始繪製都會執行.
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }複製程式碼

這裡面的awakenScrollBars()方法呢是去喚醒ScrollBar的繪製.任何View都是有ScrollBar的.scrollBar執行繪製流程的時候該方法會返回true,這樣的話將不會執行scrollTo裡面的postInvalidateOnAnimation方法,如何返回fasle則相反.再awakenScrollBars()裡面也會呼叫到postInvalidateOnAnimation這個方法.我們來看下原始碼.

 /**
     * <p>
     * 觸發scrollbars去繪製.當呼叫這個方法的時候將開啟一個延遲動畫去隱藏scrollbars,如果有個子類提供了滑動的動畫,那麼這個延遲的時間要和子類動畫執行的實行進行對比.
     * </p>
     *
     * <p>
     * 只有在scrollbars可用的時候才會開啟動畫.呼叫 {@link #isHorizontalScrollBarEnabled()} 和
     * {@link #isVerticalScrollBarEnabled()}.
     當動畫執行的時候該方法會返回true, 其他情況將返回false. 如何動畫執行將呼叫到invalidate()方法區重繪.
     * @see #scrollBy(int, int)
     * @see #scrollTo(int, int)
     * @see #isHorizontalScrollBarEnabled()
     * @see #isVerticalScrollBarEnabled()
     * @see #setHorizontalScrollBarEnabled(boolean)
     * @see #setVerticalScrollBarEnabled(boolean)
     */
    protected boolean awakenScrollBars(int startDelay, boolean invalidate) {
        final ScrollabilityCache scrollCache = mScrollCache;

        if (scrollCache == null || !scrollCache.fadeScrollBars) {
            return false;
        }

        if (scrollCache.scrollBar == null) {
        //初始化了繪製scrollBar所需的一些引數,這裡沒初始化的時候再draw(canvas方法裡面將不會開啟繪製流程)
            scrollCache.scrollBar = new ScrollBarDrawable();
            scrollCache.scrollBar.setState(getDrawableState());
            scrollCache.scrollBar.setCallback(this);
        }

        if (isHorizontalScrollBarEnabled() || isVerticalScrollBarEnabled()) {
        //這裡就是呼叫重新繪製的方法.invalidate的值為true.
            if (invalidate) {
                // Invalidate to show the scrollbars
                postInvalidateOnAnimation();
            }

            if (scrollCache.state == ScrollabilityCache.OFF) {
                // FIXME: this is copied from WindowManagerService.
                // We should get this value from the system when it
                // is possible to do so.
                final int KEY_REPEAT_FIRST_DELAY = 750;
                startDelay = Math.max(KEY_REPEAT_FIRST_DELAY, startDelay);
            }

            //開啟動畫
            long fadeStartTime = AnimationUtils.currentAnimationTimeMillis() + startDelay;
            scrollCache.fadeStartTime = fadeStartTime;
            scrollCache.state = ScrollabilityCache.ON;

            // Schedule our fader to run, unscheduling any old ones first
            if (mAttachInfo != null) {
                mAttachInfo.mHandler.removeCallbacks(scrollCache);
                mAttachInfo.mHandler.postAtTime(scrollCache, fadeStartTime);
            }

            return true;
        }

        return false;
    }複製程式碼

一切的結果都將呼叫到postInvalidateOnAnimation()那我們就跟著去看下該方法.


    public void postInvalidateOnAnimation() {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this);
        }
    }複製程式碼

attachInfo再View跟window連線上的時候就開始賦值,這裡是不為null的,然後將呼叫mViewRootImpl裡面的方法.我們先來看下ViewRootImpl類.

/**
 * 在試圖層級的頂層, 實現了View和Windowmanage質檢需要的協議,內部實現的細節要去看這個類{@link WindowManagerGlobal}.
 */
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {複製程式碼

對於該類的定義我們再下節詳細講解.該類呢實現了ViewPaent的介面,ViewParent的方法都在該類中得到重寫.那就去看下dispatchInvalidateOnAnimation(this)方法.

 public void dispatchInvalidateOnAnimation(View view) {
        mInvalidateOnAnimationRunnable.addView(view);
    }複製程式碼

這裡面呢mInvalidateOnAnimationRunnable是一個實現了Runable介面的內部類.這裡呢將會呼叫View的invalidate()方法,這個方法我們在寫自定義View的時候都用過吧,呼叫之後講會通知View進行draw.才InvalidateOnAnimationRunnable的addView中將會呼叫到postIfNeededLocked()方法.

 private void postIfNeededLocked() {
 //mPosted初始值是false,這裡保證該方法只被執行一次.再run方法執行之後會被重新賦值成false.
            if (!mPosted) {
            //這裡將執行mInvalidateOnAnimationRunnable這個實現類.
                mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
                mPosted = true;
            }
        }複製程式碼

mInvalidateOnAnimationRunnable執行的時候將會呼叫自己的run方法裡面將遍歷儲存View的集合,挨個的呼叫自己的invalidate()方法然後將儲存的View物件釋放掉.

for (int i = 0; i < viewCount; i++) {
                mTempViews[i].invalidate();
                mTempViews[i] = null;
            }複製程式碼

下面是mInvalidateOnAnimationRunnable的原始碼:

final class InvalidateOnAnimationRunnable implements Runnable {
        private boolean mPosted;
        private final ArrayList<View> mViews = new ArrayList<View>();
        private final ArrayList<AttachInfo.InvalidateInfo> mViewRects =
                new ArrayList<AttachInfo.InvalidateInfo>();
        private View[] mTempViews;
        private AttachInfo.InvalidateInfo[] mTempViewRects;

        public void addView(View view) {
            synchronized (this) {
                mViews.add(view);
                postIfNeededLocked();
            }
        }

        public void addViewRect(AttachInfo.InvalidateInfo info) {
            synchronized (this) {
                mViewRects.add(info);
                postIfNeededLocked();
            }
        }

        public void removeView(View view) {
            synchronized (this) {
                mViews.remove(view);

                for (int i = mViewRects.size(); i-- > 0; ) {
                    AttachInfo.InvalidateInfo info = mViewRects.get(i);
                    if (info.target == view) {
                        mViewRects.remove(i);
                        info.recycle();
                    }
                }

                if (mPosted && mViews.isEmpty() && mViewRects.isEmpty()) {
                    mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, this, null);
                    mPosted = false;
                }
            }
        }

        @Override
        public void run() {
            final int viewCount;
            final int viewRectCount;
            synchronized (this) {
                mPosted = false;

                viewCount = mViews.size();
                if (viewCount != 0) {
                    mTempViews = mViews.toArray(mTempViews != null
                            ? mTempViews : new View[viewCount]);
                    mViews.clear();
                }

                viewRectCount = mViewRects.size();
                if (viewRectCount != 0) {
                    mTempViewRects = mViewRects.toArray(mTempViewRects != null
                            ? mTempViewRects : new AttachInfo.InvalidateInfo[viewRectCount]);
                    mViewRects.clear();
                }
            }
             //這裡將呼叫View的繪製步驟.
            for (int i = 0; i < viewCount; i++) {
                mTempViews[i].invalidate();
                mTempViews[i] = null;
            }
           //這裡並不會執行.
            for (int i = 0; i < viewRectCount; i++) {
                final View.AttachInfo.InvalidateInfo info = mTempViewRects[i];
                info.target.invalidate(info.left, info.top, info.right, info.bottom);
                info.recycle();
            }
        }

        private void postIfNeededLocked() {
            if (!mPosted) {
                mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
                mPosted = true;
            }
        }
    }複製程式碼

最後還是呼叫到了View的invalidate()方法,那就去看下這個方法吧.

 /**
     * 使整個試圖無效,如果試圖是可見的,
     * {@link #onDraw(android.graphics.Canvas)}之後將會呼叫這個方法
     */
 public void invalidate() {
        invalidate(true);
    }


public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
     ................省略程式碼..........

            // 將矩形的資訊給父類.
            //damage:損壞,髒.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }
         ................省略程式碼..........
        }
    }複製程式碼

這裡又呼叫到了ViewParent.invalidateChild方法,因為ViewParent是一個介面,也就是呼叫了ViewRootlmpl類重寫的invalidateChild方法.我們跟一下原始碼.

 @Override
    public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        ..........
        invalidateRectOnScreen(dirty);

        return null;
    }

    private void invalidateRectOnScreen(Rect dirty) {
        .......
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        //這個是啟動View繪製三部曲的入口,measure,layout,draw.
        //牽扯到View的繪製原始碼.
            scheduleTraversals();
        }
    }複製程式碼

當呼叫到scheduleTraversals()方法的時候將會呼叫到View繪製的入口,也就是從佈局的measure,layout,draw方法最開始被呼叫的地方,這裡其實我們現在不必要去了解,我們只需要去看下原始碼就好了,最終肯定會呼叫到View的draw(canvas)方法,canvas物件是在scheduleTraversals()這一系列方法中建立然後傳給View的.我們直接看下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) {
            drawBackground(canvas);
        }

        // 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);
            //這個一般是ViewGroup才會重寫的方法.
            // Step 4, draw the children
            dispatchDraw(canvas);
           ...................
            // Step 6, draw decorations (foreground, scrollbars)
            //這裡面繪製了scrollBar
            onDrawForeground(canvas);

            // we're done...
            return;
        }
        .............
    }複製程式碼

draw方法裡面可以看出很多都是空的方法需要子類自己去實現繪製邏輯,只有那個onDrawForeground(canvas)繪製前景,這裡面才用到了mScrollX和mScrollY變數.跟著走原始碼:

public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);
        ........
        }複製程式碼

接著看原始碼:

 protected final void onDrawScrollBars(Canvas canvas) {
        // scrollbars are drawn only when the animation is running
        //再scrollTo方法裡面已經給ScrollabilityCache賦值了.
        final ScrollabilityCache cache = mScrollCache;

        if (cache != null) {

            int state = cache.state;

            if (state == ScrollabilityCache.OFF) {
                return;
            }

            boolean invalidate = false;

           .......

                if (drawHorizontalScrollBar) {
                    scrollBar.setParameters(computeHorizontalScrollRange(),
                            computeHorizontalScrollOffset(),
                            computeHorizontalScrollExtent(), false);
                    final Rect bounds = cache.mScrollBarBounds;
                    getHorizontalScrollBarBounds(bounds, null);
                    onDrawHorizontalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
                            bounds.right, bounds.bottom);
                    if (invalidate) {
                    //看到這個方法我們就發現了希望.
                        invalidate(bounds);
                    }
                }

                if (drawVerticalScrollBar) {
                    scrollBar.setParameters(computeVerticalScrollRange(),
                            computeVerticalScrollOffset(),
                            computeVerticalScrollExtent(), true);
                    final Rect bounds = cache.mScrollBarBounds;
                    getVerticalScrollBarBounds(bounds, null);
                    onDrawVerticalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
                            bounds.right, bounds.bottom);
                    if (invalidate) {
                        invalidate(bounds);
                    }
                }
            }
        }
    }複製程式碼

最會我們發現onDrawScrollBars裡再次呼叫了invalidate方法,不過引數是一個矩形的物件.繼續跟原始碼:

 public void invalidate(int l, int t, int r, int b) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
    }複製程式碼

這裡我們能大致的看到矩形的left 是 l - scrollx等等,這就是為什麼再scrollto或者scrollBy裡面設定正直的話就是往反方向走,設定父值得話就是往正確的方向.我們繼續跟著原始碼走的話可以看到這裡又重新的呼叫了ViewRootlmpl類的invalidateChild方法,又重複了之前的繪製過程.好吧原始碼勉強就看到這把,其實遺留了很多的問題小編也是無奈的,Android原始碼太強大!

結語

這裡面就簡單的跟著scrollTo的原始碼走了一遍,還有很多的問題沒有解決.這裡就算是給大家指出一條看原始碼的思路吧.隨後還會繼續更新這裡面沒有解決的幾個問題,什麼問題呢我想跟著走一遍原始碼的時候你自己就會發現了.

歡迎大家的糾正~
感覺可以就給個贊支援一下吧~

歡迎關注我的掘金
歡迎關注我的簡書
歡迎關注我的CSDN

相關文章