提示:本文的源码均取自Android 7.0(API 24)
自定义View是Android进阶路线上必须攻克的难题,而在这之前就应该先对View的工作原理有一个系统的理解。本系列将分为4篇博客进行讲解,本文主要对View的绘制流程进行讲解。相关内容如下:
- Android View原理解析之基础知识(MeasureSpec、DecorView、ViewRootImpl)
- Android View原理解析之测量流程(measure)
- Android View原理解析之布局流程(layout)
- Android 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中要执行的步骤:
其中第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
方法完成了对默认焦点高亮状态的绘制。
说完了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