Android View 繪制流程(Draw) 完全解析

前言

前幾篇文章卡儒,筆者分別講述了DecorView,measure俐巴,layout流程等骨望,接下來將詳細(xì)分析三大工作流程的最后一個(gè)流程——繪制流程。測(cè)量流程決定了View的大小欣舵,布局流程決定了View的位置擎鸠,那么繪制流程將決定View的樣子,一個(gè)View該顯示什么由繪制流程完成缘圈。以下源碼均取自Android API 21劣光。

從performDraw說起

前面幾篇文章提到,三大工作流程始于ViewRootImpl#performTraversals,在這個(gè)方法內(nèi)部會(huì)分別調(diào)用performMeasure糟把,performLayout绢涡,performDraw三個(gè)方法來分別完成測(cè)量,布局遣疯,繪制流程雄可。那么我們現(xiàn)在先從performDraw方法看起,ViewRootImpl#performDraw:

private void performDraw() {
    //...
    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    try {
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

    //省略...
}

里面又調(diào)用了ViewRootImpl#draw方法,并傳遞了fullRedrawNeeded參數(shù)数苫,而該參數(shù)由mFullRedrawNeeded成員變量獲取聪舒,它的作用是判斷是否需要重新繪制全部視圖,如果是第一次繪制視圖虐急,那么顯然應(yīng)該繪制所以的視圖过椎,如果由于某些原因,導(dǎo)致了視圖重繪戏仓,那么就沒有必要繪制所有視圖疚宇。我們來看看ViewRootImpl#draw

private void draw(boolean fullRedrawNeeded) {
    ...
    //獲取mDirty,該值表示需要重繪的區(qū)域
    final Rect dirty = mDirty;
    if (mSurfaceHolder != null) {
        // The app owns the surface, we won't draw.
        dirty.setEmpty();
        if (animating) {
            if (mScroller != null) {
                mScroller.abortAnimation();
            }
            disposeResizeBuffer();
        }
        return;
    }

    //如果fullRedrawNeeded為真赏殃,則把dirty區(qū)域置為整個(gè)屏幕敷待,表示整個(gè)視圖都需要繪制
    //第一次繪制流程,需要繪制所有視圖
    if (fullRedrawNeeded) {
        mAttachInfo.mIgnoreDirtyState = true;
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    }

    //省略...

    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
        }
}

這里省略了一部分代碼仁热,我們只看關(guān)鍵代碼榜揖,首先是先獲取了mDirty值,該值保存了需要重繪的區(qū)域的信息抗蠢,關(guān)于視圖重繪举哟,后面會(huì)有文章專門敘述,這里先熟悉一下迅矛。接著根據(jù)fullRedrawNeeded來判斷是否需要重置dirty區(qū)域妨猩,最后調(diào)用了ViewRootImpl#drawSoftware方法,并把相關(guān)參數(shù)傳遞進(jìn)去秽褒,包括dirty區(qū)域壶硅,我們接著看該方法的源碼:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

    // Draw with software renderer.
    final Canvas canvas;
    try {
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;

        //鎖定canvas區(qū)域,由dirty區(qū)域決定
        canvas = mSurface.lockCanvas(dirty);

        // The dirty rectangle can be modified by Surface.lockCanvas()
        //noinspection ConstantConditions
        if (left != dirty.left || top != dirty.top || right != dirty.right
                || bottom != dirty.bottom) {
            attachInfo.mIgnoreDirtyState = true;
        }

        canvas.setDensity(mDensity);
    } 

    try {
        
        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }

        dirty.setEmpty();
        mIsAnimating = false;
        attachInfo.mDrawingTime = SystemClock.uptimeMillis();
        mView.mPrivateFlags |= View.PFLAG_DRAWN;

        try {
            canvas.translate(-xoff, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            attachInfo.mSetIgnoreDirtyState = false;

            //正式開始繪制
            mView.draw(canvas);

        }
    } 
    return true;
}

可以看書销斟,首先是實(shí)例化了Canvas對(duì)象庐椒,然后鎖定該canvas的區(qū)域,由dirty區(qū)域決定蚂踊,接著對(duì)canvas進(jìn)行一系列的屬性賦值约谈,最后調(diào)用了mView.draw(canvas)方法,前面分析過犁钟,mView就是DecorView棱诱,也就是說從DecorView開始繪制,前面所做的一切工作都是準(zhǔn)備工作特纤,而現(xiàn)在則是正式開始繪制流程军俊。

View的繪制

由于ViewGroup沒有重寫draw方法,因此所有的View都是調(diào)用View#draw方法捧存,因此,我們直接看它的源碼:

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * 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
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

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

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // we're done...
        return;
    }
    ...
}

可以看到,draw過程比較復(fù)雜昔穴,但是邏輯十分清晰镰官,而官方注釋也清楚地說明了每一步的做法。我們首先來看一開始的標(biāo)記位dirtyOpaque吗货,該標(biāo)記位的作用是判斷當(dāng)前View是否是透明的泳唠,如果View是透明的,那么根據(jù)下面的邏輯可以看出宙搬,將不會(huì)執(zhí)行一些步驟笨腥,比如繪制背景、繪制內(nèi)容等勇垛。這樣很容易理解脖母,因?yàn)橐粋€(gè)View既然是透明的,那就沒必要繪制它了闲孤。接著是繪制流程的六個(gè)步驟谆级,這里先小結(jié)這六個(gè)步驟分別是什么,然后再展開來講讼积。

繪制流程的六個(gè)步驟:
1肥照、對(duì)View的背景進(jìn)行繪制
2、保存當(dāng)前的圖層信息(可跳過)
3勤众、繪制View的內(nèi)容
4舆绎、對(duì)View的子View進(jìn)行繪制(如果有子View)
5、繪制View的褪色的邊緣们颜,類似于陰影效果(可跳過)
6亿蒸、繪制View的裝飾(例如:滾動(dòng)條)
其中第2步和第5步是可以跳過的,我們這里不做分析掌桩,我們重點(diǎn)來分析其它步驟边锁。

Step 1 :繪制背景

這里調(diào)用了View#drawBackground方法,我們看它的源碼:

private void drawBackground(Canvas canvas) {

    //mBackground是該View的背景參數(shù)波岛,比如背景顏色
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }

    //根據(jù)View四個(gè)布局參數(shù)來確定背景的邊界
    setBackgroundBounds();

    ...

    //獲取當(dāng)前View的mScrollX和mScrollY值
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        //如果scrollX和scrollY有值茅坛,則對(duì)canvas的坐標(biāo)進(jìn)行偏移,再繪制背景
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

可以看出则拷,這里考慮到了view的偏移參數(shù),scrollX和scrollY贡蓖,繪制背景在偏移后的view中繪制。

Step 3:繪制內(nèi)容

這里調(diào)用了View#onDraw方法煌茬,View中該方法是一個(gè)空實(shí)現(xiàn)斥铺,因?yàn)椴煌腣iew有著不同的內(nèi)容,這需要我們自己去實(shí)現(xiàn)坛善,即在自定義View中重寫該方法來實(shí)現(xiàn)晾蜘。

Step 4: 繪制子View

如果當(dāng)前的View是一個(gè)ViewGroup類型邻眷,那么就需要繪制它的子View,這里調(diào)用了dispatchDraw剔交,而View中該方法是空實(shí)現(xiàn)肆饶,實(shí)際是ViewGroup重寫了這個(gè)方法,那么我們來看看岖常,ViewGroup#dispatchDraw:

protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;

    for (int i = 0; i < childrenCount; i++) {
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }
        int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    //省略...
    
}

源碼很長(zhǎng)驯镊,這里簡(jiǎn)單說明一下,里面主要遍歷了所以子View竭鞍,每個(gè)子View都調(diào)用了drawChild這個(gè)方法板惑,我們找到這個(gè)方法,ViewGroup#drawChild

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
     return child.draw(canvas, this, drawingTime);
}

可以看出偎快,這里調(diào)用了View的draw方法冯乘,但這個(gè)方法并不是上面所說的,因?yàn)閰?shù)不同滨砍,我們來看看這個(gè)方法往湿,View#draw

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

    //省略...
    
    if (!drawingWithDrawingCache) {
        if (drawingWithRenderNode) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // Fast path for layouts with no backgrounds
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchDraw(canvas);
            } else {
                draw(canvas);
            }
        }
    } else if (cache != null) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE) {
            // no layer paint, use temporary paint to draw bitmap
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            // use layer paint to draw the bitmap, merging the two alphas, but also restore
            int layerPaintAlpha = mLayerPaint.getAlpha();
            mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            mLayerPaint.setAlpha(layerPaintAlpha);
        }
    }

}

我們主要來看核心部分,首先判斷是否已經(jīng)有緩存惋戏,即之前是否已經(jīng)繪制過一次了领追,如果沒有,則會(huì)調(diào)用draw(canvas)方法响逢,開始正常的繪制绒窑,即上面所說的六個(gè)步驟,否則利用緩存來顯示舔亭。
這一步也可以歸納為ViewGroup繪制過程些膨,它對(duì)子View進(jìn)行了繪制,而子View又會(huì)調(diào)用自身的draw方法來繪制自身钦铺,這樣不斷遍歷子View及子View的不斷對(duì)自身的繪制订雾,從而使得View樹完成繪制。

Step 6 繪制裝飾

所謂的繪制裝飾矛洞,就是指View除了背景洼哎、內(nèi)容、子View的其余部分沼本,例如滾動(dòng)條等噩峦,我們看View#onDrawForeground:

public void onDrawForeground(Canvas canvas) {
    onDrawScrollIndicators(canvas);
    onDrawScrollBars(canvas);

    final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
    if (foreground != null) {
        if (mForegroundInfo.mBoundsChanged) {
            mForegroundInfo.mBoundsChanged = false;
            final Rect selfBounds = mForegroundInfo.mSelfBounds;
            final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

            if (mForegroundInfo.mInsidePadding) {
                selfBounds.set(0, 0, getWidth(), getHeight());
            } else {
                selfBounds.set(getPaddingLeft(), getPaddingTop(),
                        getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
            }

            final int ld = getLayoutDirection();
            Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                    foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
            foreground.setBounds(overlayBounds);
        }

        foreground.draw(canvas);
    }
}

可以看出,邏輯很清晰抽兆,和一般的繪制流程非常相似识补,都是先設(shè)定繪制區(qū)域,然后利用canvas進(jìn)行繪制辫红,這里就不展開詳細(xì)地說了凭涂,有興趣的可以繼續(xù)了解下去祝辣。

那么,到目前為止导盅,View的繪制流程也講述完畢了较幌,希望這篇文章對(duì)你們起到幫助作用揍瑟,謝謝你們的閱讀白翻。

更多閱讀
Android View 測(cè)量流程(Measure)完全解析
Android View 布局流程(Layout)完全解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市绢片,隨后出現(xiàn)的幾起案子滤馍,更是在濱河造成了極大的恐慌,老刑警劉巖底循,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巢株,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡熙涤,警方通過查閱死者的電腦和手機(jī)阁苞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來祠挫,“玉大人那槽,你說我怎么就攤上這事〉忍颍” “怎么了骚灸?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)慌植。 經(jīng)常有香客問我甚牲,道長(zhǎng),這世上最難降的妖魔是什么蝶柿? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任丈钙,我火速辦了婚禮,結(jié)果婚禮上交汤,老公的妹妹穿的比我還像新娘雏赦。我一直安慰自己,他們只是感情好蜻展,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布喉誊。 她就那樣靜靜地躺著,像睡著了一般纵顾。 火紅的嫁衣襯著肌膚如雪伍茄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天施逾,我揣著相機(jī)與錄音敷矫,去河邊找鬼例获。 笑死,一個(gè)胖子當(dāng)著我的面吹牛曹仗,可吹牛的內(nèi)容都是我干的榨汤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼怎茫,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼收壕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起轨蛤,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤蜜宪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后祥山,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體圃验,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年缝呕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了澳窑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡供常,死狀恐怖摊聋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情话侧,我是刑警寧澤栗精,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站瞻鹏,受9級(jí)特大地震影響悲立,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜新博,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一薪夕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧赫悄,春花似錦原献、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至倔撞,卻和暖如春讲仰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背痪蝇。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工鄙陡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留冕房,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓趁矾,卻偏偏與公主長(zhǎng)得像耙册,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子毫捣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容