前幾天遇到一個ViewGroup.onDraw不會調用的問題,在網上查了一些資料,發現基本都混淆了onDraw
和draw
的區別,趁着十一假期有時間,簡單梳理了下這裏的邏輯。java
View.draw
和View.onDraw
的調用關係首先,View.draw
和View.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複製代碼
經過上述代碼可知:優化
View.draw
方法中會調用View.onDraw
dirtyOpaque
爲false(透明,非實心),纔會調用View.onDraw
方法。所以,若是但願ViewGroup.onDraw
方法被調用,那麼就必須知足兩個條件:spa
ViewGroup.draw
方法被調用draw
方法中的dirtyOpaque
爲false。既然談到了View.draw
和View.onDraw
,這裏簡單說下二者的區別。查看View源碼,可知View.draw
基本包含6個步驟:debug
View.onDraw
方法來實現,通常自定義View,就是經過該方法來繪製內容。得到Canvas後,能夠draw任何內容,實現個性化的定製。簡單來講,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
PFLAG_SKIP_DRAW
,那麼會跳過當前View的draw方法,直接調用dispatchDraw方法繪製當前View的子View。PFLAG_SKIP_DRAW
,那麼會調用當前View的draw方法,完成全部內容的繪製。那麼PFLAG_SKIP_DRAW
取決於哪些因素那?cdn
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
標識,必須知足兩個條件:
經過setWillNotDraw(true)
必定會對mViewFlags設置WILL_NOT_DRAW
標識。若是此時當前View沒有背景圖,那麼就會對mPrivateFlags設置PFLAG_SKIP_DRAW
標識。
可是若此時當前View有背景圖,那麼就會取消mPrivateFlags的PFLAG_SKIP_DRAW
標識,同時設置另一個PFLAG_ONLY_DRAWS_BACKGROUND
標識。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
方法的相關邏輯以下圖所示:
到這裏關於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標識;而要包含此標識,須要同時知足兩個條件:
- View.mViewFlags包含WILL_NOT_DRAW標識,可經過View.setWillNotDraw(true)設置該標識。
- 當前View沒有背景圖。
所以,若是咱們想讓ViewGroup.draw
被調用,只要破壞上述任何一個條件就能夠了。- 調用View.setWillNotDraw(false),取消View.mViewFlags中的WILL_NOT_DRAW標識
- 爲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_BACKGROUND
和PFLAG_OPAQUE_SCROLLBARS
標識,當前View纔是實心的。
該方法會在View中的不少地方被調用,以實時肯定View是不是實心的。
固然,若是isOpaque
方法的默認實現不符合咱們的需求,咱們能夠本身實現,這也是官方推薦的作法。
下面咱們經過一個Demo驗證上述邏輯:
上述Demo必須採用軟件繪製纔有效。在硬件繪製下,子ViewB調用invalidate方法,只會觸發子ViewB本身的draw方法,它的父View是不須要重繪的。
假如咱們對子ViewB設置了一個純色的背景(子ViewB變成實心了),那麼能夠獲得以下結論:
假如咱們沒有對子ViewB設置背景(子ViewB變成非實心了),那麼能夠獲得以下結論:
固然控制一個View是否實心,咱們也能夠直接重寫isOpaque
方法,不必像上面這麼麻煩。
總結一下,首次渲染View樹的時候,只要ViewGroup.draw方法被調用了,那麼ViewGroup.onDraw就會被調用。
可是後續子View.invalidate的時候,在ViewGroup.draw方法被調用的前提下,還要子View是非實心的,那麼ViewGroup.onDraw和ViewGroup.drawBackground纔會被調用。
最後用一張圖來總結下ViewGroup的draw和onDraw方法的調用邏輯圖。