你真的瞭解Android ViewGroup的draw和onDraw的調用時機嗎

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

View.drawView.onDraw的調用關係

首先,View.drawView.onDraw是兩個不一樣的方法,只有View.draw被調用,View.onDraw纔有可能被調用。在View.draw中有下面一段代碼:canvas

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方法被調用,那麼就必須知足兩個條件:spa

  1. 設法讓ViewGroup.draw方法被調用
  2. draw方法中的dirtyOpaque爲false。

既然談到了View.drawView.onDraw,這裏簡單說下二者的區別。查看View源碼,可知View.draw基本包含6個步驟:debug

  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則只負責繪製自己相關的內容,是一個子集。3d

ViewGroup.draw的調用時機

其實也是View.draw的調用時機,經過查看View源碼可知:單參數的View.draw方法會在三個參數的View.draw方法中被調用,以下所示:rest

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。code

因此,咱們的ViewGroup.draw方法會不會被調用,徹底取決於mPrivateFlags是否是包含PFLAG_SKIP_DRAW標誌:orm

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

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

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方法的調用邏輯圖
相關文章
相關標籤/搜索