Android視圖加載流程(6)之View的詳細繪制流程Draw

流程圖

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創(chuàng)建一個canvas,然后mView(DecorView)調用draw方法并傳入canvas伐脖,此方法執(zhí)行具體的繪制工作。與Measure和Layout類似的需要遞歸繪制断凶。

理解圖 本文重點Draw

源碼解讀

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位置來設置背景的繪制區(qū)域
    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類似酒朵。通過循環(huán)調用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的終極調運方法O踉怼W魈贰!1椿颉_浣薄农猬!
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;
            //設置刷新區(qū)域
            damage.set(l, t, r, b);
            //傳遞調運Parent ViewGroup的invalidateChild方法
            p.invalidateChild(this, damage);
        }
        ......
}

View的invalidate方法最終調動invalidateInternal方法。而invalidateInternal方法是將要刷新的區(qū)域傳遞給父視圖揖闸,并調用父視圖的invalidateChild。

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

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

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

返回為空饿悬。剛好符合上面的循環(huán)狡恬!接著我們看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);
} 

一組看下來是不是覺得很清晰呢。View調用invalidate方法锚国,其實是層層往上遞血筑,直到傳遞到ViewRootImpl后出發(fā)sceheduleTraversals方法豺总,然后整個View樹開始進行重繪制任務喻喳。

理解圖:


postInvalidate方法源碼分析

上面也說道invalidate方法只能在UI線程中執(zhí)行表伦,其他需要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發(fā)送了一條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是在子線程中發(fā)消息羊苟,UI線程接收消息并刷新UI蜡励。

理解圖:

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

  • 直接調用invalidate方法.請求重新draw凉倚,但只會繪制調用者本身稽寒。
  • 觸發(fā)setSelection方法杏糙。請求重新draw宏侍,但只會繪制調用者本身谅河。
  • 觸發(fā)setVisibility方法绷耍。 當View可視狀態(tài)在INVISIBLE轉換VISIBLE時會間接調用invalidate方法锨天,繼而繪制該View。當View的可視狀態(tài)在INVISIBLE\VISIBLE 轉換為GONE狀態(tài)時會間接調用requestLayout和invalidate方法赘阀,同時由于View樹大小發(fā)生了變化,所以會請求measure過程以及draw過程幅慌,同樣只繪制需要“重新繪制”的視圖胰伍。
  • 觸發(fā)setEnabled方法骂租。請求重新draw渗饮,但不會重新繪制任何View包括該調用者本身互站。
  • 觸發(fā)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后最終觸發(fā)了該方法
        scheduleTraversals();
    }
}

與invalidate是不是很像呢?requestLayout()會分別調用measure和layout過程冤今,在layout過程的時候視圖如果有位置變化闺兢,如長寬變化,此時會調用draw的過程重新繪制戏罢,如果視圖沒有位置變化屋谭,則不會調用draw的過程

至此一整塊的視圖加載流程結束了!


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

工匠若水 Android應用層View繪制流程與源碼分析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末校摩,一起剝皮案震驚了整個濱河市互妓,隨后出現的幾起案子摹芙,更是在濱河造成了極大的恐慌伏嗜,老刑警劉巖挣轨,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摩幔,死亡現場離奇詭異,居然都是意外死亡封断,警方通過查閱死者的電腦和手機衣陶,發(fā)現死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門拯欧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來藏姐,“玉大人杨蛋,你說我怎么就攤上這事曙寡】眨” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵署尤,是天一觀的道長。 經常有香客問我铜幽,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任翩迈,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘涛漂。我一直安慰自己悠轩,他們只是感情好忙菠,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布傍睹。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪传趾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天劫恒,我揣著相機與錄音,去河邊找鬼。 笑死令境,一個胖子當著我的面吹牛,可吹牛的內容都是我干的亲配。 我是一名探鬼主播玷犹,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼撤奸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了强胰?” 一聲冷哼從身側響起牵寺,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤势腮,失蹤者是張志新(化名)和其女友劉穎玄渗,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體炕倘,經...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡乓土,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了询张。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡恋拷,死狀恐怖蔬顾,靈堂內的尸體忽然破棺而出舷胜,到底是詐尸還是另有隱情纱新,我是刑警寧澤遇汞,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布蟀瞧,位于F島的核電站条摸,受9級特大地震影響悦污,放射性物質發(fā)生泄漏。R本人自食惡果不足惜钉蒲,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一切端、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧子巾,春花似錦帆赢、人聲如沸小压。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怠益。三九已至仪搔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蜻牢,已是汗流浹背烤咧。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留抢呆,地道東北人煮嫌。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像抱虐,于是被迫代替她去往敵國和親昌阿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內容