提示:本文的源碼均取自Android 7.0(API 24)
前言
自定義View是Android進階路線上必須攻克的難題,而在這之前就應該先對View的工作原理有一個系統的理解。本系列將分爲4篇博客進行講解,本文主要對View的繪製流程進行講解。相關內容如下:
從View的角度看draw流程
在本系列的第一篇文章中講到整個視圖樹(ViewTree)的根容器是DecorView,ViewRootImpl通過調用DecorView的draw方法開啓佈局流程。draw是定義在View中的方法,我們先從View的角度來看看佈局過程中發生了什麼。
首先來看一下draw
方法中的邏輯,關鍵代碼如下:
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called.
*
* View的子類不應該重寫這個方法,而應該重寫onDraw方法繪製自己的內容
*
* @param canvas The Canvas to which the View is rendered.
*/
@CallSuper
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;
/*
* 完整地繪製流程將按順序執行以下6步
* 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 繪製子View
* 5. If necessary, draw the fading edges and restore layers 恢復保存的圖層
* 6. Draw decorations (scrollbars for instance) 繪製裝飾(比如滑動條)
*/
// ① Step 1, 繪製背景(如果有必要的話)
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 通常情況下會跳過第2步和第5步
// 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, 繪製內容
if (!dirtyOpaque) onDraw(canvas);
// ③ Step 4, 繪製子View
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// ④ Step 6, 繪製View的裝飾 (foreground, scrollbars)
onDrawForeground(canvas);
// ⑤ Step 7, 繪製默認的焦點高亮
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
/* 以下會完整執行所有繪製步驟(一般不會執行到這裏)
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
........
}
這個方法的邏輯非常清晰,這裏咱們再來總結一下在draw中要執行的步驟:
- 繪製背景
- 保存Canvas圖層信息(如果有必要的話)
- 繪製View的內容
- 繪製子View
- 繪製保存的Canvas圖層信息(如果有必要的話)
- 繪製View的裝飾(比如滑動條)
其中第2步和第5步在通常情況下是不會執行的,所以我們也就不再深究它們了。首先在代碼①的位置,調用了drawBackground
方法繪製View的背景,那讓我們首先來看看在這個方法中做了些什麼:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
// Attempt to use a display list if requested.
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mThreadedRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.isValid()) {
setBackgroundRenderNodeProperties(renderNode);
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
return;
}
}
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
// 繪製View背景
background.draw(canvas);
} else {
// 如果View發生了移動,先移動畫布,再繪製View背景
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
在這個方法中的background
其實就是個Drawable對象,繪製背景的時候只要調用Drawable#draw
方法就行了。當然,如果View的位置發生了移動(scrollX或scrollY不爲0),需要先平移畫布,再繪製background。
然後在View#draw
代碼②的位置,調用了onDraw
方法繪製自己的內容,這個方法的代碼如下:
/**
* Implement this to do your drawing.
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
這是個空方法,意味着View的子類需要自己負責繪製內容。如果通過繼承View實現自定義View,就應該重寫onDraw方法,並在這個方法中繪製自身的內容。View#draw
方法的註釋也提到,View的子類不應該直接重寫draw方法,而應該重寫onDraw方法。
隨後在View#draw
代碼③的位置,調用了dispatchDraw
方法繪製自己的子View,這個方法同樣是空實現。因爲一個純粹的View是沒有子View的,自然也沒必要執行相應的繪製邏輯。代碼如下:
/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
*/
protected void dispatchDraw(Canvas canvas) {
}
緊接着在View#draw
代碼④的位置,調用了onDrawForeground
方法繪製View的裝飾,比如前景、滑動條等,代碼如下:
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);
}
}
在這個方法中會依次調用onDrawScrollIndicators
和onDrawScrollbars
繪製滑動條的指示器和滑動條,最後繪製View的前景色。代碼中的foreground
是一個Drawable對象,因此只需要調用Drawable#draw
方法就完成了對View前景色的繪製。
最後在View#draw
代碼⑤的位置,調用drawDefaultFocusHighlight
方法繪製View的默認焦點高亮狀態,代碼如下:
private void drawDefaultFocusHighlight(Canvas canvas) {
if (mDefaultFocusHighlight != null) {
if (mDefaultFocusHighlightSizeChanged) {
mDefaultFocusHighlightSizeChanged = false;
final int l = mScrollX;
final int r = l + mRight - mLeft;
final int t = mScrollY;
final int b = t + mBottom - mTop;
mDefaultFocusHighlight.setBounds(l, t, r, b);
}
mDefaultFocusHighlight.draw(canvas);
}
}
mDefaultFocusHighlight同樣是一個Drawable對象,這裏調用Drawable#draw
方法完成了對默認焦點高亮狀態的繪製。
從ViewGroup的角度看draw流程
說完了View的繪製流程,接下來再從ViewGroup角度看看繪製過程中發生了什麼。
ViewGroup並沒有重寫draw方法,說明ViewGroup也是遵循上文提到的繪製步驟的。此外,ViewGroup也沒有重寫onDraw方法,說明ViewGroup默認也不會繪製自身的內容。如果我們通過繼承ViewGroup實現自定義View,且有繪製自身的需求,就應該重寫onDraw方法。
ViewGroup重寫了dispatchDraw
方法,這個方法將負責繪製子View,關鍵代碼如下:
@Override
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
.......
boolean more = false;
final long drawingTime = getDrawingTime();
if (usingRenderNodeProperties) canvas.insertReorderBarrier();
final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
int transientIndex = transientCount != 0 ? 0 : -1;
// Only use the preordered list if not HW accelerated, since the HW pipeline will do the
// draw reordering internally
final ArrayList<View> preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
// 循環處理子View
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) {
// 繪製子View
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
// 繪製子View
more |= drawChild(canvas, child, drawingTime);
}
}
.......
}
在這個方法中會循環處理子View,並調用drawChild
方法完成對子View的繪製。一般而言,如果通過繼承ViewGroup實現自定義View,是不用重寫dispatchDraw方法的,直接維持ViewGroup的默認實現邏輯就好了。接下來,就讓我們來看一下在drawChild中又做了些什麼:
/**
* Draw one child of this View Group. This method is responsible for getting
* the canvas in the right state. This includes clipping, translating so
* that the child's scrolled origin is at 0, 0, and applying any animation
* transformations.
*
* @param canvas The canvas on which to draw the child
* @param child Who to draw
* @param drawingTime The time at which draw is occurring
* @return True if an invalidate() was issued
*/
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
這個方法的實現邏輯很簡單,直接調用View#draw
方法完成了繪製邏輯。但是要注意,這裏調用的並不是上文提到的draw方法,這裏是View#draw的重載版本,關鍵代碼如下:
/**
* This method is called by ViewGroup.drawChild() to have each child view draw itself.
*
* This is where the View specializes rendering behavior based on layer type,
* and hardware acceleration.
*/
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
/* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
*
* If a view is dettached, its DisplayList shouldn't exist. If the canvas isn't
* HW accelerated, it can't handle drawing RenderNodes.
*/
boolean drawingWithRenderNode = mAttachInfo != null
&& mAttachInfo.mHardwareAccelerated
&& hardwareAcceleratedCanvas;
boolean more = false;
........
final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
........
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
} else {
// ① 判斷是否需要跳過對自身的繪製流程
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
// 跳過View的繪製流程,直接調用dispatchView繪製子View
dispatchDraw(canvas);
} else {
// 繪製View
draw(canvas);
}
}
} else if (cache != null) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
// 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();
if (alpha < 1) {
mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
}
canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
if (alpha < 1) {
mLayerPaint.setAlpha(layerPaintAlpha);
}
}
}
........
return more;
}
這個方法中的代碼很多,這裏就只保留了關鍵部分的代碼。在代碼①的位置會先判斷是否需要跳過對自己的繪製流程:如果當前的View是ViewGroup,並且不需要繪製背景時,就會直接調用dispatchDraw
方法繪製子View;否則會調用自身的draw
方法,後續步驟就和View中的繪製流程一致了。
到了這裏,ViewGroup的繪製流程也分析完畢了,整體看來還是比較簡單的。如果要繼承ViewGroup實現自定義View,繪製流程其實完全可以保持默認實現。除非我們有一些特殊的繪製邏輯,比如像LinearLayout一樣在子View之間繪製分割線,那就可以通過重寫onDraw方法實現。
整體的流程圖
上面分別從View和ViewGroup的角度講解了繪製流程,這裏再以流程圖的形式歸納一下整個draw過程,便於加深記憶:
小結
繪製流程中的許多工作已經被系統完成了,相比前兩個步驟還是比較容易的。但是如果想要獲得更好的學習效果,最好還是打開AndroidStudio,循着本文的脈絡試着一步步探索源碼中的邏輯。
參考資料
https://blog.csdn.net/lfdfhl/article/details/51435968
https://blog.csdn.net/a553181867/article/details/51570854