你真的瞭解Android ViewGroup的draw和onDraw的呼叫時機嗎

ltlovezh發表於2017-10-09

前幾天遇到一個ViewGroup.onDraw不會呼叫的問題,在網上查了一些資料,發現基本都混淆了onDrawdraw的區別,趁著十一假期有時間,簡單梳理了下這裡的邏輯。

View.drawView.onDraw的呼叫關係

首先,View.drawView.onDraw是兩個不同的方法,只有View.draw被呼叫,View.onDraw才有可能被呼叫。在View.draw中有下面一段程式碼:

final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);//是否是實心控制元件

if (!dirtyOpaque) {
    drawBackground(canvas);//繪製背景
}

...

// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);//呼叫onDraw複製程式碼

通過上述程式碼可知:

  1. View.draw方法中會呼叫View.onDraw
  2. 只有dirtyOpaque為false(透明,非實心),才會呼叫View.onDraw方法。

因此,如果希望ViewGroup.onDraw方法被呼叫,那麼就必須滿足兩個條件:

  1. 設法讓ViewGroup.draw方法被呼叫
  2. draw方法中的dirtyOpaque為false。

既然談到了View.drawView.onDraw,這裡簡單說下兩者的區別。檢視View原始碼,可知View.draw基本包含6個步驟:

  1. Draw the background,通過View.drawBackground方法來實現。
  2. If necessary, save the canvas’ layers to prepare for fading,如果需要,儲存畫布層(Canvas.saveLayer)為淡入或淡出做準備。
  3. draw the content,通過View.onDraw方法來實現,一般自定義View,就是通過該方法來繪製內容。獲得Canvas後,可以draw任何內容,實現個性化的定製。
  4. draw the children,通過View.dispatchDraw方法來實現,ViewGroup都會實現該方法,來繪製自己的子View。
  5. If necessary, draw the fading edges and restore layers,如果需要,繪製淡入淡出的相關內容並恢復之前儲存的畫布層(layer)。
  6. draw decorations (scrollbars),通過View.onDrawScrollBars方法來實現,繪製滾動條的操作就是在這裡實現的。

簡單來說,View.draw負責繪製當前View的所有內容以及子View的內容,是一個全集。而View.onDraw則只負責繪製本身相關的內容,是一個子集。

ViewGroup.draw的呼叫時機

其實也是View.draw的呼叫時機,通過檢視View原始碼可知:單引數的View.draw方法會在三個引數的View.draw方法中被呼叫,如下所示:

if (!hasDisplayList) { //軟體繪製
    // Fast path for layouts with no backgrounds
    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {      
        //跳過當前View的繪製,直接繪製子view
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        dispatchDraw(canvas);
    } else {                            
        //此時座標系已經切換到View自身座標系了,可以純碎的繪製當前view了,又回到了draw(canvas)
        draw(canvas);
    }
}複製程式碼

在軟體繪製下,三引數的View.draw負責把View座標系從父View那裡切換到當前View,然後再交給當前View去繪製。一般情況下,交給當前View去繪製就是通過呼叫單引數的View.draw方法來實現。
但是,這裡有一個優化邏輯:如果當前View不需要繪製(打上了PFLAG_SKIP_DRAW標誌),那麼會通過dispatchDraw方法直接繪製當前View的子View。

所以,我們的ViewGroup.draw方法會不會被呼叫,完全取決於mPrivateFlags是不是包含PFLAG_SKIP_DRAW標誌:

  1. 若mPrivateFlags包含PFLAG_SKIP_DRAW,那麼會跳過當前View的draw方法,直接呼叫dispatchDraw方法繪製當前View的子View。
  2. 若mPrivateFlags不包含PFLAG_SKIP_DRAW,那麼會呼叫當前View的draw方法,完成所有內容的繪製。

那麼PFLAG_SKIP_DRAW取決於哪些因素那?

setWillNotDraw

View中有一個setWillNotDraw方法,從註釋上來看,就是控制是否要跳過View.draw方法,以進行優化的。我們看一下該方法:

public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}複製程式碼

該方法很簡單,我們繼續看下setFlags方法:

void setFlags(int flags, int mask) {
int old = mViewFlags;
//設定flags
mViewFlags = (mViewFlags & ~mask) | (flags & mask);
int changed = mViewFlags ^ old;
//若mViewFlags前後沒有變化,則直接返回
if (changed == 0) {
    return;
}
int privateFlags = mPrivateFlags;

...

if ((changed & DRAW_MASK) != 0) {
    if ((mViewFlags & WILL_NOT_DRAW) != 0) {
        //mViewFlags設定了WILL_NOT_DRAW標誌
        if (mBseackground != null) {
            //如果當前View有背景,那麼取消mPrivateFlags的PFLAG_SKIP_DRAW標誌,但是設定另外一個PFLAG_ONLY_DRAWS_BACKGROUND標誌
            mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;
        } else {
            //如果當前View沒有背景,那麼直接設定PrivateFlags的PFLAG_SKIP_DRAW標誌
            mPrivateFlags |= PFLAG_SKIP_DRAW;
        }
    } else {
        //因為mViewFlags沒有設定WILL_NOT_DRAW標誌,所以取消mPrivateFlags的PFLAG_SKIP_DRAW標誌
        mPrivateFlags &= ~PFLAG_SKIP_DRAW;
    }
    requestLayout();
    invalidate(true);
    }
}複製程式碼

通過上述程式碼可知,要想對mPrivateFlags設定PFLAG_SKIP_DRAW標識,必須滿足兩個條件:

  1. 針對mViewFlags,設定WILL_NOT_DRAW標誌
  2. 當前View沒有背景圖

通過setWillNotDraw(true)一定會對mViewFlags設定WILL_NOT_DRAW標識。如果此時當前View沒有背景圖,那麼就會對mPrivateFlags設定PFLAG_SKIP_DRAW標識。
但是若此時當前View有背景圖,那麼就會取消mPrivateFlags的PFLAG_SKIP_DRAW標識,同時設定另外一個PFLAG_ONLY_DRAWS_BACKGROUND標識。setWillNotDraw方法的相關邏輯如下圖所示:

setWillNotDraw
setWillNotDraw

設定背景

那這裡就有一個疑問,如果我們在執行過程中,取消了當前View的背景圖,那麼當前View還會重新為mPrivateFlags設定PFLAG_SKIP_DRAW標誌嗎?
答案:會,這也正是PFLAG_ONLY_DRAWS_BACKGROUND標誌的作用。

我們看下View.setBackgroundDrawable方法的實現:

public void setBackgroundDrawable(Drawable background) {
if (background == mBackground) {
    return;
}
if (background != null) {
    ...
    mBackground = background;
    if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
        //若當前View既設定PFLAG_SKIP_DRAW,又新增了背景,那麼只能取消mPrivateFlags的PFLAG_SKIP_DRAW標誌,同時替換成PFLAG_ONLY_DRAWS_BACKGROUND,這和setFlags方法裡面的邏輯一致
        mPrivateFlags &= ~PFLAG_SKIP_DRAW;
        mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;
    }
}else{
    //這裡取消了背景圖
    mBackground = null;
    if ((mPrivateFlags & PFLAG_ONLY_DRAWS_BACKGROUND) != 0){
        /*
        * This view ONLY drew the background before and we're removing
        * the background, so now it won't draw anything
        * (hence we SKIP_DRAW)
        */
        //如果mPrivateFlags包含PFLAG_ONLY_DRAWS_BACKGROUND標誌,說明之前mViewFlags設定了WILL_NOT_DRAW標誌,但是因為之前當前View有背景圖,那麼只能先設定PFLAG_ONLY_DRAWS_BACKGROUND標誌。現在當前View的背景圖取消了,所以可以重新對mPrivateFlags設定PFLAG_SKIP_DRAW了
        mPrivateFlags &= ~PFLAG_ONLY_DRAWS_BACKGROUND;
        mPrivateFlags |= PFLAG_SKIP_DRAW;
    }
}
}複製程式碼

上述程式碼裡的註釋已經說的很清楚了。如果取消了當前View的背景圖,系統會把mPrivateFlags的PFLAG_ONLY_DRAWS_BACKGROUND標誌重新替換為PFLAG_SKIP_DRAW標誌。setBackgroundDrawable方法的相關邏輯如下圖所示:

setBackgroundDrawable
setBackgroundDrawable

到這裡關於PFLAG_SKIP_DRAW標誌的分析已經結束了。回到我們開頭的問題:為什麼預設情況下,ViewGroup.draw(ViewGroup.onDraw)方法不會被呼叫。對照上面的分析,可知:肯定是ViewGroup的mPrivateFlags打上了PFLAG_SKIP_DRAW標誌,那麼究竟是在哪裡設定的該標誌那?
原來預設情況下,ViewGroup在初始化的時候,會通過下面的程式碼為為mViewFlags設定WILL_NOT_DRAW標誌。並且預設情況下,ViewGroup也沒有背景圖,所以就為ViewGroup的mPrivateFlags打上了PFLAG_SKIP_DRAW標誌。導致ViewGroup.draw方法不會被呼叫,那麼ViewGroup.onDraw方法就更不會被呼叫了。

 private void initViewGroup() {
    // ViewGroup doesn't draw by default
    if (!debugDraw()) {
        setFlags(WILL_NOT_DRAW, DRAW_MASK);
    }

    ...
}複製程式碼

總結一下,決定View.draw方法是否被呼叫的直接因素是:View.mPrivateFlags是否包含PFLAG_SKIP_DRAW標識;而要包含此標識,需要同時滿足兩個條件:

  1. View.mViewFlags包含WILL_NOT_DRAW標識,可通過View.setWillNotDraw(true)設定該標識。
  2. 當前View沒有背景圖。
    因此,如果我們想讓ViewGroup.draw被呼叫,只要破壞上述任何一個條件就可以了。
  3. 呼叫View.setWillNotDraw(false),取消View.mViewFlags中的WILL_NOT_DRAW標識
  4. 為ViewGroup設定背景圖

ViewGroup.onDraw的呼叫時機

由上文可知,即使ViewGroup.draw被呼叫了,ViewGroup.onDraw也不一定會被呼叫。必須滿足不是實心控制元件(View.mPrivateFlags沒有打上PFLAG_DIRTY_OPAQUE標識),ViewGroup.onDraw才會被呼叫。

實心控制元件:控制元件的onDraw方法能夠保證此控制元件的所有區域都會被其所繪製的內容完全覆蓋。換句話說,通過此控制元件所屬的區域無法看到此控制元件之下的內容,也就是既沒有半透明也沒有空缺的部分。

那麼View.mPrivateFlags在什麼情況下會被打上PFLAG_DIRTY_OPAQUE標識那。通過檢視原始碼,發現相關邏輯在ViewGroup.invalidateChild方法中:

//這裡的child表示直接呼叫invalidate的子View。
public final void invalidateChild(View child, final Rect dirty) {
//計運算元View是否是實心的
final boolean isOpaque = child.isOpaque() && !drawAnimation && child.getAnimation() == null && childMatrix.isIdentity();
//PFLAG_DIRTY和PFLAG_DIRTY_OPAQUE是互斥的
int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;

do { //迴圈遍歷到ViewRootImpl為止
    View view = null;//父View
    if (parent instanceof View) {
        view = (View) parent;
    }
    if (view != null) { //給當前父View打上相應的flag
        //父View若包含FADING_EDGE_MASK標識,那麼只能打上FLAG_DIRTY標識,表示會呼叫ViewGroup.onDraw方法
        if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
                            view.getSolidColor() == 0) {
            opaqueFlag = PFLAG_DIRTY;
        }
        if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
            //PFLAG_DIRTY和PFLAG_DIRTY_OPAQUE是互斥的
            view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
        }
    }
    ...
}複製程式碼

通過上述程式碼可知:View.invalidate方法會向上回溯到ViewRootImpl,在此過程中,若子控制元件是實心的,則會將當前父控制元件標記為PFLAG_DIRTY_OPAQUE,否則為PFLAG_DIRTY
對於包含PFLAG_DIRTY_OPAQUE標識的控制元件,在繪製過程中,會跳過drawBackground方法(繪製背景)和onDraw方法(繪製自身內容)。

決定一個View是否實心完全取決於isOpaque方法,該方法的預設實現是檢查View.mPrivateFlags是否包含PFLAG_OPAQUE_MASK標識。PFLAG_OPAQUE_MASK標識(實心)又由PFLAG_OPAQUE_BACKGROUND(背景實心)和PFLAG_OPAQUE_SCROLLBARS(滾動條實心)組成。即:只有View同時滿足背景實心和滾動條實心,那麼它才是opaque的。
真正計算View是否實心的方法是computeOpaqueFlags,如下所示:

 protected void computeOpaqueFlags() {
    // Opaque if:
    //   - Has a background
    //   - Background is opaque
    //   - Doesn't have scrollbars or scrollbars overlay
    //若View包含背景,且背景是不透明的,則打上PFLAG_OPAQUE_BACKGROUND標識
    if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
        mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
    } else {
        mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
    }

    final int flags = mViewFlags;
    //若沒有橫豎滾動條,或者滾動條是OVERLAY型別的,則打上PFLAG_OPAQUE_SCROLLBARS標識
    if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
                (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
                (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
        mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;
    } else {
        mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
    }
}複製程式碼

只有同時打上了PFLAG_OPAQUE_BACKGROUNDPFLAG_OPAQUE_SCROLLBARS標識,當前View才是實心的。
該方法會在View中的很多地方被呼叫,以實時確定View是否是實心的。
當然,如果isOpaque方法的預設實現不符合我們的需求,我們可以自己實現,這也是官方推薦的做法。

Demo驗證

下面我們通過一個Demo驗證上述邏輯:

  1. 設定一個自定義父ViewGroupA和子ViewB。
  2. 對父ViewGroupA呼叫setWillNotDraw(false),保證父ViewGroupA的draw方法會被呼叫。
  3. 對子ViewB設定一個Click事件,具體實現就是呼叫子ViewB.invalidate方法。
  4. 通過點選子ViewB,觀察父ViewGroupA和子ViewB的draw和onDraw方法是否會被呼叫。

上述Demo必須採用軟體繪製才有效。在硬體繪製下,子ViewB呼叫invalidate方法,只會觸發子ViewB自己的draw方法,它的父View是不需要重繪的。

假如我們對子ViewB設定了一個純色的背景(子ViewB變成實心了),那麼可以得到如下結論:

  1. 在View樹第一次渲染的時候,父ViewGroupA和子ViewB的draw和onDraw方法都會被呼叫。
  2. 在後續點選子ViewB的時候,子ViewB的draw和onDraw方法都會被呼叫,父ViewGroupA的draw方法也會被呼叫,但是父ViewGroupA的onDraw方法不會被呼叫

假如我們沒有對子ViewB設定背景(子ViewB變成非實心了),那麼可以得到如下結論:

  1. 在View樹第一次渲染的時候,父ViewGroupA和子ViewB的draw和onDraw方法都會被呼叫。
  2. 在後續點選子ViewB的時候,父ViewGroupA和子ViewB的draw和onDraw方法都會被呼叫。

當然控制一個View是否實心,我們也可以直接重寫isOpaque方法,沒必要像上面這麼麻煩。

總結一下,首次渲染View樹的時候,只要ViewGroup.draw方法被呼叫了,那麼ViewGroup.onDraw就會被呼叫
但是後續子View.invalidate的時候,在ViewGroup.draw方法被呼叫的前提下,還要子View是非實心的,那麼ViewGroup.onDraw和ViewGroup.drawBackground才會被呼叫

總結

最後用一張圖來總結下ViewGroup的draw和onDraw方法的呼叫邏輯圖。

ViewGroup的draw和onDraw方法的呼叫邏輯圖
ViewGroup的draw和onDraw方法的呼叫邏輯圖

相關文章