Android檢視載入流程(6)之View的詳細繪製流程Draw

weixin_34129145發表於2017-09-01
1971858-357c00ba9f8a1803
流程圖

Android檢視載入流程(5)之View的詳細繪製流程Layout

上一篇文章我們對View的測量(Measure)進行講解了。接著我們開始聊佈局(Layout),以下是我們熟悉的performTraversals方法。

private void performTraversals() {
    ......
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ......
    //本文重點
    canvas = mSurface.lockCanvas(dirty);
    mView.draw(canvas);
    ......
} 

我們看出ViewRootImpl建立一個canvas,然後mView(DecorView)呼叫draw方法並傳入canvas,此方法執行具體的繪製工作。與Measure和Layout類似的需要遞迴繪製。

理解圖 本文重點Draw

1971858-05b0f060fa096c4d

原始碼解讀

Step1 View

由於ViewGroup沒有重寫View的draw方法,我們看下View的draw方法。

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 1, draw the background, if needed
    ......
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    ......

    // Step 2, save the canvas' layers
    ......
        if (drawTop) {
            canvas.saveLayer(left, top, right, top + length, null, flags);
        }
    ......

    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

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

    // Step 5, draw the fade effect and restore layers
    ......
    if (drawTop) {
        matrix.setScale(1, fadeHeight * topFadeStrength);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, top, right, top + length, p);
    }
    ......

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

整個的繪製流程分成6步,通過註釋可以知道第2步和第5步(skip step 2 & 5 if possible (common case))可以忽略跳過,我們對剩餘4步就行分析。

Step2 View

第一步:對View的背景進行繪製

private void drawBackground(Canvas canvas) {
    //獲取xml中通過android:background屬性或者程式碼中setBackgroundColor()、setBackgroundResource()等方法進行賦值的背景Drawable
    final Drawable background = mBackground;
    ......
    //根據layout過程確定的View位置來設定背景的繪製區域
    if (mBackgroundSizeChanged) {
        background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
    ......
    //呼叫Drawable的draw()方法來完成背景的繪製工作
    background.draw(canvas);
    ......
}

draw方法通過調運drawBackground(canvas);方法實現了背景繪製。

Step3 View

第三步:對View的內容進行繪製

protected void onDraw(Canvas canvas) {
}

ViewGroup沒有重寫該方法,view的方法也緊緊是一個空方法而已。大家都知道不同的View是顯示不同的內容的,所以這塊必須是子類去實現具體邏輯。

Step4.1 View

第四步:對當前View的所有子View進行繪製

protected void dispatchDraw(Canvas canvas) {
}

View的dispatchDraw()方法是一個空方法。這個我們也比較好理解,本身View自身就沒有所謂的子檢視,而擁有子檢視的就是ViewGroup!所以我們可以看下ViewGroup的dispatchDraw

Step4.2 ViewGroup

@Override
protected void dispatchDraw(Canvas canvas) {
    ......
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    ......
    for (int i = 0; i < childrenCount; 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) {
        ......
        for (int i = disappearingCount; i >= 0; i--) {
            ......
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ......
}
    
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

可見,Viewgroup重寫了dispatchDraw()方法,該方法內部會遍歷每個子View,然後呼叫drawChild()方法。而drawChild方法內部是直接由子檢視呼叫draw()方法。

Step5 View

第六步:對View的滾動條進行繪製

protected final void onDrawScrollBars(Canvas canvas) {
    //繪製ScrollBars分析不是我們這篇的重點,所以暫時不做分析
    ......
}

可以看見其實任何一個View都是有(水平垂直)滾動條的,只是一般情況下沒讓它顯示而已。

總結 Summary

通過以上幾個步驟的分析,繪製(draw)的流程基本與measure和layout類似。通過迴圈呼叫draw來繪製各個子檢視。

  1. 如果物件為view就不用遍歷子檢視,如果物件為viewGroup就要遍歷子檢視
  2. View預設不會繪製任何內容,子類必須重寫onDraw方法
  3. View的繪製是藉助onDraw方法傳入的Canvas類來進行的
  4. 預設情況下子View的ViewGroup.drawChild繪製順序和子View被新增的順序一致,但是你也可以過載ViewGroup.getChildDrawingOrder()方法提供不同順序。

額外 extra

我們經常在自定義View的時候會遇到兩種方法invalidate和postinvalidate。我們來看看兩個方法與檢視的繪製有什麼樣的聯絡呢?

invalidate方法原始碼分析

由於ViewGroup並沒有重寫該方法,所以我們直接看View的invalidate

View
//public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的才有效,回撥onDraw方法,針對區域性View
public void invalidate(Rect dirty) {
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    //實質還是調運invalidateInternal方法
    invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
            dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}

//public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的才有效,回撥onDraw方法,針對區域性View
public void invalidate(int l, int t, int r, int b) {
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    //實質還是調運invalidateInternal方法
    invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
}

//public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的才有效,回撥onDraw方法,針對整個View
public void invalidate() {
    //invalidate的實質還是調運invalidateInternal方法
    invalidate(true);
}


//default的許可權,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的才有效,回撥onDraw方法,針對整個View
void invalidate(boolean invalidateCache) {
    //實質還是調運invalidateInternal方法
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

//這是所有invalidate的終極調運方法!!!!!!
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
        ......
        // Propagate the damage rectangle to the parent view.
        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);
            //傳遞調運Parent ViewGroup的invalidateChild方法
            p.invalidateChild(this, damage);
        }
        ......
}

View的invalidate方法最終調動invalidateInternal方法。而invalidateInternal方法是將要重新整理的區域傳遞給父檢視,並呼叫父檢視的invalidateChild。

ViewGroup
public final void invalidateChild(View child, final Rect dirty) {
    ViewParent parent = this;
    final AttachInfo attachInfo = mAttachInfo;
    ......
    do {
        ......
        //迴圈層層上級調運,直到ViewRootImpl會返回null
        parent = parent.invalidateChildInParent(location, dirty);
        ......
    } while (parent != null);
}

這個過程不斷的向上尋找父親檢視,當父檢視為空時才停止。所以我們可以聯想到根檢視的ViewRootImpl

ViewRootImpl
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    ......
    //View調運invalidate最終層層上傳到ViewRootImpl後最終觸發了該方法
    scheduleTraversals();
    ......
    return null;
}

返回為空。剛好符合上面的迴圈!接著我們看scheduleTraversals()這個方法是不是感覺很熟悉呢?
這就是Android檢視載入流程(3)之ViewRootImpl的UI重新整理機制的Step4.2。ViewRootImpl正準備呼叫繪製View檢視的程式碼。

ViewRootImpl
void scheduleTraversals() {
    mChoreographer.postCallback(
           Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
    
//實現了Runnable介面
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
    
void doTraversal() {
     performTraversals();
}
    
private void performTraversals() {
    //測量
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    //佈局
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    //繪製
    mView.draw(canvas);
} 

一組看下來是不是覺得很清晰呢。Viewdiaoyonginvalidate方法,其實是層層往上遞,直到傳遞到ViewRootImpl後出發sceheduleTraversals方法,然後整個View樹開始進行重繪製任務。

理解圖:

1971858-55ab8e0a8e744aa4

postInvalidate方法原始碼分析

上面也說道invalidate方法只能在UI執行緒中執行,其他需要postInvalidate方法

View
public void postInvalidate() {
    postInvalidateDelayed(0);
}
    
public void postInvalidateDelayed(long delayMilliseconds) {
    // 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;
    //核心,實質就是調運了ViewRootImpl.dispatchInvalidateDelayed方法
    if (attachInfo != null) {
        attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
    }
}

此方法必須是在檢視已經繫結到Window才能使用,即attachInfo是否為空。隨後呼叫ViewRootImpl的dispatchinvalidateDelayed

ViewRootImple
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
    Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
    mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

Handler亂入!此時ViewrootImpl類的Handler傳送了一條MSG_INVALIDATE訊息。哪裡接收這個訊息呢?

ViewRootImple
final class ViewRootHandler extends Handler {
    
    public void handleMessage(Message msg) {
        ......
        switch (msg.what) {
        case MSG_INVALIDATE:
            ((View) msg.obj).invalidate();
            break;
        ......
        }
        ......
    }
}

實質上還是在UI執行緒中呼叫了View的invalidate()方法。

postInvalidate是在子執行緒中發訊息,UI執行緒接收訊息並重新整理UI。

理解圖:

1971858-64638e524efef58f

常見的引起invalidate方法操作的原因主要有:

  • 直接呼叫invalidate方法.請求重新draw,但只會繪製呼叫者本身。
  • 觸發setSelection方法。請求重新draw,但只會繪製呼叫者本身。
  • 觸發setVisibility方法。 當View可視狀態在INVISIBLE轉換VISIBLE時會間接呼叫invalidate方法,繼而繪製該View。當View的可視狀態在INVISIBLE\VISIBLE 轉換為GONE狀態時會間接呼叫requestLayout和invalidate方法,同時由於View樹大小發生了變化,所以會請求measure過程以及draw過程,同樣只繪製需要“重新繪製”的檢視。
  • 觸發setEnabled方法。請求重新draw,但不會重新繪製任何View包括該呼叫者本身。
  • 觸發requestFocus方法。請求View樹的draw過程,只繪製“需要重繪”的View。

requestLayout方法原始碼分析

和invalidate類似,層層往上傳遞。

View
public void requestLayout() {
    ......
    if (mParent != null && !mParent.isLayoutRequested()) {
        //由此向ViewParent請求佈局
        //從這個View開始向上一直requestLayout,最終到達ViewRootImpl的requestLayout
        mParent.requestLayout();
    }
    ......
}

獲取父類物件,呼叫requestlayout(),最後到達ViewRootImpl

ViewRootImpl
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        //View調運requestLayout最終層層上傳到ViewRootImpl後最終觸發了該方法
        scheduleTraversals();
    }
}

與invalidate是不是很像呢?reqeustLayout()方法會呼叫measure和layout過程,不會呼叫draw過程,也不會重新繪製任何View,包括呼叫者本身。而invalidate則繪製draw為主。

至此一整塊的檢視載入流程結束了!


PS:本文整理自以下文章,若有發現問題請致郵 caoyanglee92@gmail.com

工匠若水 Android應用層View繪製流程與原始碼分析

相關文章