Android View 繪製流程(Draw) 完全解析
前言
前幾篇文章,筆者分別講述了DecorView,measure,layout流程等,接下來將詳細分析三大工作流程的最後一個流程——繪製流程。測量流程決定了View的大小,佈局流程決定了View的位置,那麼繪製流程將決定View的樣子,一個View該顯示什麼由繪製流程完成。以下原始碼均取自Android API 21。
從performDraw說起
前面幾篇文章提到,三大工作流程始於ViewRootImpl#performTraversals,在這個方法內部會分別呼叫performMeasure,performLayout,performDraw三個方法來分別完成測量,佈局,繪製流程。那麼我們現在先從performDraw方法看起,ViewRootImpl#performDraw:
private void performDraw() {
//...
final boolean fullRedrawNeeded = mFullRedrawNeeded;
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
//省略...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
裡面又呼叫了ViewRootImpl#draw方法,並傳遞了fullRedrawNeeded引數,而該引數由mFullRedrawNeeded成員變數獲取,它的作用是判斷是否需要重新繪製全部檢視,如果是第一次繪製檢視,那麼顯然應該繪製所以的檢視,如果由於某些原因,導致了檢視重繪,那麼就沒有必要繪製所有檢視。我們來看看ViewRootImpl#draw:
private void draw(boolean fullRedrawNeeded) {
...
//獲取mDirty,該值表示需要重繪的區域
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區域置為整個螢幕,表示整個檢視都需要繪製
//第一次繪製流程,需要繪製所有檢視
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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
這裡省略了一部分程式碼,我們只看關鍵程式碼,首先是先獲取了mDirty值,該值儲存了需要重繪的區域的資訊,關於檢視重繪,後面會有文章專門敘述,這裡先熟悉一下。接著根據fullRedrawNeeded來判斷是否需要重置dirty區域,最後呼叫了ViewRootImpl#drawSoftware方法,並把相關引數傳遞進去,包括dirty區域,我們接著看該方法的原始碼:
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區域,由dirty區域決定
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
可以看書,首先是例項化了Canvas物件,然後鎖定該canvas的區域,由dirty區域決定,接著對canvas進行一系列的屬性賦值,最後呼叫了mView.draw(canvas)方法,前面分析過,mView就是DecorView,也就是說從DecorView開始繪製,前面所做的一切工作都是準備工作,而現在則是正式開始繪製流程。
View的繪製
由於ViewGroup沒有重寫draw方法,因此所有的View都是呼叫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;
}
...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
可以看到,draw過程比較複雜,但是邏輯十分清晰,而官方註釋也清楚地說明了每一步的做法。我們首先來看一開始的標記位dirtyOpaque,該標記位的作用是判斷當前View是否是透明的,如果View是透明的,那麼根據下面的邏輯可以看出,將不會執行一些步驟,比如繪製背景、繪製內容等。這樣很容易理解,因為一個View既然是透明的,那就沒必要繪製它了。接著是繪製流程的六個步驟,這裡先小結這六個步驟分別是什麼,然後再展開來講。
繪製流程的六個步驟:
1、對View的背景進行繪製
2、儲存當前的圖層資訊(可跳過)
3、繪製View的內容
4、對View的子View進行繪製(如果有子View)
5、繪製View的褪色的邊緣,類似於陰影效果(可跳過)
6、繪製View的裝飾(例如:滾動條)
其中第2步和第5步是可以跳過的,我們這裡不做分析,我們重點來分析其它步驟。
Skip 1 :繪製背景
這裡呼叫了View#drawBackground方法,我們看它的原始碼:
private void drawBackground(Canvas canvas) {
//mBackground是該View的背景引數,比如背景顏色
final Drawable background = mBackground;
if (background == null) {
return;
}
//根據View四個佈局引數來確定背景的邊界
setBackgroundBounds();
...
//獲取當前View的mScrollX和mScrollY值
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
//如果scrollX和scrollY有值,則對canvas的座標進行偏移,再繪製背景
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
可以看出,這裡考慮到了view的偏移引數,scrollX和scrollY,繪製背景在偏移後的view中繪製。
Skip 3:繪製內容
這裡呼叫了View#onDraw方法,View中該方法是一個空實現,因為不同的View有著不同的內容,這需要我們自己去實現,即在自定義View中重寫該方法來實現。
Skip 4: 繪製子View
如果當前的View是一個ViewGroup型別,那麼就需要繪製它的子View,這裡呼叫了dispatchDraw,而View中該方法是空實現,實際是ViewGroup重寫了這個方法,那麼我們來看看,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);
}
}
//省略...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
原始碼很長,這裡簡單說明一下,裡面主要遍歷了所以子View,每個子View都呼叫了drawChild這個方法,我們找到這個方法,ViewGroup#drawChild:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
- 1
- 2
- 3
- 1
- 2
- 3
可以看出,這裡呼叫了View的draw方法,但這個方法並不是上面所說的,因為引數不同,我們來看看這個方法,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);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
我們主要來看核心部分,首先判斷是否已經有快取,即之前是否已經繪製過一次了,如果沒有,則會呼叫draw(canvas)方法,開始正常的繪製,即上面所說的六個步驟,否則利用快取來顯示。
這一步也可以歸納為ViewGroup繪製過程,它對子View進行了繪製,而子View又會呼叫自身的draw方法來繪製自身,這樣不斷遍歷子View及子View的不斷對自身的繪製,從而使得View樹完成繪製。
Skip 6 繪製裝飾
所謂的繪製裝飾,就是指View除了背景、內容、子View的其餘部分,例如滾動條等,我們看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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
可以看出,邏輯很清晰,和一般的繪製流程非常相似,都是先設定繪製區域,然後利用canvas進行繪製,這裡就不展開詳細地說了,有興趣的可以繼續瞭解下去。
那麼,到目前為止,View的繪製流程也講述完畢了,希望這篇文章對你們起到幫助作用,謝謝你們的閱讀。
相關文章
- Android進階(五)View繪製流程AndroidView
- View的繪製二:View的繪製流程View
- Android View繪製原理:繪製流程排程、測算等AndroidView
- Android原始碼分析之View繪製流程Android原始碼View
- Android View 原始碼解析(三) – View的繪製過程AndroidView原始碼
- View 繪製流程分析View
- 探究Android View 繪製流程,Canvas 的由來AndroidViewCanvas
- 探究 Android View 繪製流程,Activity 的 View 如何展示到螢幕AndroidView
- Android View繪製流程看這篇就夠了AndroidView
- Android系統原始碼分析--View繪製流程之-inflateAndroid原始碼View
- Android系統原始碼分析–View繪製流程之-setContentViewAndroid原始碼View
- Android系統原始碼分析--View繪製流程之-setContentViewAndroid原始碼View
- View繪製流程原始碼分析View原始碼
- Android高階進階之路【一】Android中View繪製流程淺析AndroidView
- Android View的繪製過程AndroidView
- Android自定義View之(一)View繪製流程詳解——向原始碼要答案AndroidView原始碼
- View的繪製-measure流程詳解View
- Android View繪製原始碼分析 MeasureAndroidView原始碼
- 2018.03.15、View 繪製流程學習 筆記View筆記
- Android原始碼完全解析——View的Measure過程Android原始碼View
- View繪製01-Android渲染系統中的ViewViewAndroid
- 每日一問:簡述 View 的繪製流程View
- Android UI繪製流程及原理AndroidUI
- Android View的Measure測量流程全解析AndroidView
- 基於原始碼分析 Android View 繪製機制原始碼AndroidView
- Android自定義View之Paint繪製文字和線AndroidViewAI
- Android自定義view-自繪ViewAndroidView
- RecyclerView 原始碼分析(一) —— 繪製流程解析View原始碼
- 【Android進階】RecyclerView之繪製流程(三)AndroidView
- View繪製——畫多大?View
- View繪製——畫在哪?View
- draw.io for Mac(流程圖繪製工具) v22.0.3中文版Mac流程圖
- 靈魂畫師,Android繪製流程——Android高階UIAndroidUI
- 繪製流程
- 使用joinjs繪製流程圖(五)-流程圖繪製JS流程圖
- Android Service完全解析Android
- Flutter 自定義繪製 ViewFlutterView
- View繪製——怎麼畫?View
- View 的繪製過程View