Android View 繪制流程(Draw)全面解析
前言
前幾篇文章,筆者分別講述了DecorView,measure,layout流程等,接下來將詳細(xì)分析三大工作流程的最后一個(gè)流程——繪制流程。測量流程決定了View的大小,布局流程決定了View的位置,那么繪制流程將決定View的樣子,一個(gè)View該顯示什么由繪制流程完成。以下源碼均取自Android API 21。
從performDraw說起
前面幾篇文章提到,三大工作流程始于ViewRootImpl#performTraversals,在這個(gè)方法內(nèi)部會分別調(diào)用performMeasure,performLayout,performDraw三個(gè)方法來分別完成測量,布局,繪制流程。那么我們現(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)于視圖重繪,后面會有文章專門敘述,這里先熟悉一下。接著根據(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對象,然后鎖定該canvas的區(qū)域,由dirty區(qū)域決定,接著對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ù)下面的邏輯可以看出,將不會執(zhí)行一些步驟,比如繪制背景、繪制內(nèi)容等。這樣很容易理解,因?yàn)橐粋€(gè)View既然是透明的,那就沒必要繪制它了。接著是繪制流程的六個(gè)步驟,這里先小結(jié)這六個(gè)步驟分別是什么,然后再展開來講。
繪制流程的六個(gè)步驟:
1、對View的背景進(jìn)行繪制
2、保存當(dāng)前的圖層信息(可跳過)
3、繪制View的內(nèi)容
4、對View的子View進(jìn)行繪制(如果有子View)
5、繪制View的褪色的邊緣,類似于陰影效果(可跳過)
6、繪制View的裝飾(例如:滾動(dòng)條)
其中第2步和第5步是可以跳過的,我們這里不做分析,我們重點(diǎn)來分析其它步驟。
Skip 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有值,則對canvas的坐標(biāo)進(jìn)行偏移,再繪制背景 canvas.translate(scrollX, scrollY); background.draw(canvas); canvas.translate(-scrollX, -scrollY); } }
可以看出,這里考慮到了view的偏移參數(shù),scrollX和scrollY,繪制背景在偏移后的view中繪制。
Skip 3:繪制內(nèi)容
這里調(diào)用了View#onDraw方法,View中該方法是一個(gè)空實(shí)現(xiàn),因?yàn)椴煌腣iew有著不同的內(nèi)容,這需要我們自己去實(shí)現(xiàn),即在自定義View中重寫該方法來實(shí)現(xiàn)。
Skip 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); } } //省略... }
源碼很長,這里簡單說明一下,里面主要遍歷了所以子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)繪制過一次了,如果沒有,則會調(diào)用draw(canvas)方法,開始正常的繪制,即上面所說的六個(gè)步驟,否則利用緩存來顯示。
這一步也可以歸納為ViewGroup繪制過程,它對子View進(jìn)行了繪制,而子View又會調(diào)用自身的draw方法來繪制自身,這樣不斷遍歷子View及子View的不斷對自身的繪制,從而使得View樹完成繪制。
Skip 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的繪制流程也講述完畢了,希望這篇文章對你們起到幫助作用,謝謝你們的閱讀。
更多閱讀
Android View 測量流程(Measure)全面解析
Android View 布局流程(Layout)全面解析
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android實(shí)現(xiàn)發(fā)送短信功能實(shí)例詳解
這篇文章主要介紹了Android實(shí)現(xiàn)發(fā)送短信功能的方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了Android實(shí)現(xiàn)發(fā)送短信的原理、步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-02-02Android框架Volley之利用Imageloader和NetWorkImageView加載圖片的方法
這篇文章主要介紹了Android框架Volley之利用Imageloader和NetWorkImageView加載圖片的實(shí)現(xiàn)方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-05-05學(xué)習(xí)使用Material Design控件(二)使用DrawerLayout實(shí)現(xiàn)側(cè)滑菜單欄效果
這篇文章主要為大家介紹了學(xué)習(xí)使用Material Design控件的詳細(xì)教程,使用DrawerLayout和NavigationView實(shí)現(xiàn)側(cè)滑菜單欄效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07Android 擴(kuò)大 View 的點(diǎn)擊區(qū)域的方法
這篇文章主要介紹了Android 擴(kuò)大 View 的點(diǎn)擊區(qū)域的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04Android編程實(shí)現(xiàn)webview將網(wǎng)頁打包成apk的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)webview將網(wǎng)頁打包成apk的方法,以打包HTML5為例分析了webview打包apk的相關(guān)方法、屬性與事件操作技巧,需要的朋友可以參考下2017-08-08Android基于ViewPager實(shí)現(xiàn)類似微信頁面切換效果
這篇文章主要介紹了Android基于ViewPager實(shí)現(xiàn)類似微信頁面切換效果,通過Fragment適配器實(shí)現(xiàn)頁面切換效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05Android實(shí)現(xiàn)梯形TextView效果
TextView(文本框),用于顯示文本的一個(gè)控件,Android開發(fā)中經(jīng)常使用,本文講述如何實(shí)現(xiàn)一個(gè)梯形的TextView2021-05-05Android Viewpager實(shí)現(xiàn)輪播廣告圖
這篇文章主要為大家詳細(xì)介紹了Android Viewpager實(shí)現(xiàn)輪播廣告圖,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05