前言
在前面两篇文章中我们具体分析了View的measure和layout流程,当确定了View的大小和位置后,我们是如何把View显示到屏幕上的呢?本篇就来分析一下View的draw流程,文中源码基于 Android API 21。
View绘制流程
在由setContentView探究Activity界面加载流程及Activity、Window和DecorView的关系中,我们提到View三大工作流程是从ViewRootImpl#performTraversals开始的,其中performMeasure、performLayout、performDraw方法分别对应了View的测量、布局、绘制。因此我们从performDraw开始分析View绘制流程。
private void performDraw() {
...
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
//省略...
}
performDraw
方法内部会调用draw(boolean fullRedrawNeeded)
方法,参数fullRedrawNeeded
意为是否需要全部重新绘制视图。
private void draw(boolean fullRedrawNeeded) {
//省略...
//获取需要重新绘制的区域
final Rect dirty = mDirty;
if (mSurfaceHolder != null) {
// The app owns the surface, we won't draw.
dirty.setEmpty();//如果mSurfaceHolder != null则把绘制区域置为(0,0,0,0)。
if (animating) {
if (mScroller != null) {
mScroller.abortAnimation();
}
disposeResizeBuffer();
}
return;
}
//如果需要全部重绘,则把绘制区域置为整个屏幕的大小。
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
...
int xOffset = 0;
int yOffset = curScrollY;
final WindowManager.LayoutParams params = mWindowAttributes;
final Rect surfaceInsets = params != null ? params.surfaceInsets : null;
if (surfaceInsets != null) {
xOffset -= surfaceInsets.left;
yOffset -= surfaceInsets.top;
// Offset dirty rect for surface insets.
dirty.offset(surfaceInsets.left, surfaceInsets.right);
}
if (!dirty.isEmpty() || mIsAnimating) {
if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
...
dirty.setEmpty();
...
} else {
//省略...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
}
这个方法主要是确定dirty
的区域,即确定需要绘制的区域。继续看drawSoftware
方法。
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实例
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;
}
// TODO: Do this in native
canvas.setDensity(mDensity);
}
try {
// If this bitmap's format includes an alpha channel, we
// need to clear it before drawing so that the child will
// properly re-composite its drawing on a transparent
// background. This automatically respects the clip/dirty region
// or
// If we are applying an offset, we need to clear the area
// where the offset doesn't appear to avoid having garbage
// left in the blank areas.
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;
}
此方法主要是用来获取canvas实例,并对canvas 进行处理,最后执行mView.draw(canvas)
开始绘制。这里的mView
就是DecorView,绘制是从DecorView开始的。DecorView其实是个FrameLayout,不过ViewGroup并没有重写draw(Canvas canvas)
方法,所以来看下View#draw方法。
public void draw(Canvas canvas) {
...
/*
* 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);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we're done...
return;
}
...
}
此方法列出了绘制View的具体步骤,如下:
- 绘制背景
- 必要时,保存画布的图层为褪色做准备
- 绘制View自身内容
- 绘制子View的内容
- 必要时,绘制褪色边缘并恢复图层
- 绘制装饰(列如滚动条)
不过,对于第2&5步,源码中有一行注释skip step 2 & 5 if possible (common case)
,意为:一般情况下,如果可以的话,跳过第2&5步。因此我们只分析其余4个步骤。
绘制背景
这一步是在drawBackground
中执行的,看下View#drawBackground方法
private void drawBackground(Canvas canvas) {
//mBackground为此View设置的背景
final Drawable background = mBackground;
//如果该View没有设置背景,则返回
if (background == null) {
return;
}
if (mBackgroundSizeChanged) {
//指定背景将被绘制的区域
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
mPrivateFlags3 |= PFLAG3_OUTLINE_INVALID;
}
...
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
这个方法的作用就是将背景绘制到制定的画布上,并且考虑到了mScrollX、mScrollY不等于0的情况。
绘制View自身内容
这一步是在onDraw
中完成的,看下View#onDraw方法。
protected void onDraw(Canvas canvas) {
}
可以看到View#onDraw是个空实现,需要子类去做具体实现,因为View自身的内容各不相同。
绘制子View的内容
这一步是通过dispatchDraw
来完成的,View#dispatchDraw方法仍然是个空实现,不过ViewGroup对此方法进行了重写(对于开发人员来说,一般不需要重写此方法),因此我们来看看ViewGroup#dispatchDraw方法。
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
...
boolean more = false;
final long drawingTime = getDrawingTime();
if (usingRenderNodeProperties) canvas.insertReorderBarrier();
// Only use the preordered list if not HW accelerated, since the HW pipeline will do the
// draw reordering internally
final ArrayList preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
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);
}
}
if (preorderedList != null) preorderedList.clear();
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
final ArrayList disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
这个方法很长,其主要内容就是遍历子View,然后执行drawChild
方法。
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
在drawChild
方法内部,会调用View中的draw(canvas, this, drawingTime)
方法,注意这个方法和最开始的那个draw(canvas)
方法不同,参数不一样的哦。
对于draw(canvas, this, drawingTime)
这个方法我们先来看一段此方法的注释。
/**
* This method is called by ViewGroup.drawChild() to have each child view draw itself.
* This draw() method is an implementation detail and is not intended to be overridden or
* to be called from anywhere else other than ViewGroup.drawChild().
*/
翻译如下:
ViewGroup.drawChild()调用此方法让每个子视图绘制自身。这个draw()方法对细节做了具体实现,并且不能被覆盖或从除ViewGroup.drawChild()之外的其他地方调用。
这个方法它是用来绘制了每个子View的内容的,并且我们不能去修改其实现细节。具体细节这里就不做分析了。
绘制装饰
这一步是通过onDrawScrollBars
来完成的,
protected final void onDrawScrollBars(Canvas canvas) {
...
final int viewFlags = mViewFlags;
final boolean drawHorizontalScrollBar =
(viewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL;
final boolean drawVerticalScrollBar =
(viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL
&& !isVerticalScrollBarHidden();
if (drawVerticalScrollBar || drawHorizontalScrollBar) {
final int width = mRight - mLeft;
final int height = mBottom - mTop;
final ScrollBarDrawable scrollBar = cache.scrollBar;
final int scrollX = mScrollX;
final int scrollY = mScrollY;
final int inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;
int left;
int top;
int right;
int bottom;
if (drawHorizontalScrollBar) {
int size = scrollBar.getSize(false);
if (size <= 0) {
size = cache.scrollBarSize;
}
scrollBar.setParameters(computeHorizontalScrollRange(),
computeHorizontalScrollOffset(),
computeHorizontalScrollExtent(), false);
final int verticalScrollBarGap = drawVerticalScrollBar ?
getVerticalScrollbarWidth() : 0;
top = scrollY + height - size - (mUserPaddingBottom & inside);
left = scrollX + (mPaddingLeft & inside);
right = scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap;
bottom = top + size;
onDrawHorizontalScrollBar(canvas, scrollBar, left, top, right, bottom);
if (invalidate) {
invalidate(left, top, right, bottom);
}
}
if (drawVerticalScrollBar) {
int size = scrollBar.getSize(true);
if (size <= 0) {
size = cache.scrollBarSize;
}
scrollBar.setParameters(computeVerticalScrollRange(),
computeVerticalScrollOffset(),
computeVerticalScrollExtent(), true);
int verticalScrollbarPosition = mVerticalScrollbarPosition;
if (verticalScrollbarPosition == SCROLLBAR_POSITION_DEFAULT) {
verticalScrollbarPosition = isLayoutRtl() ?
SCROLLBAR_POSITION_LEFT : SCROLLBAR_POSITION_RIGHT;
}
switch (verticalScrollbarPosition) {
default:
case SCROLLBAR_POSITION_RIGHT:
left = scrollX + width - size - (mUserPaddingRight & inside);
break;
case SCROLLBAR_POSITION_LEFT:
left = scrollX + (mUserPaddingLeft & inside);
break;
}
top = scrollY + (mPaddingTop & inside);
right = left + size;
bottom = scrollY + height - (mUserPaddingBottom & inside);
onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom);
if (invalidate) {
invalidate(left, top, right, bottom);
}
}
}
}
}
此方法用来绘制滚动条,代码很长,但是逻辑清晰,根据滚动条的方向确定滚动条的left、top、right、bottom值,进而确定了滚动条的位置,然后调用onDrawHorizontalScrollBar
和或onDrawVerticalScrollBar
方法进行绘制。
protected void onDrawHorizontalScrollBar(Canvas canvas, Drawable scrollBar,
int l, int t, int r, int b) {
//指定scrollbar将被绘制的区域
scrollBar.setBounds(l, t, r, b);
//开始绘制
scrollBar.draw(canvas);
}
protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar,
int l, int t, int r, int b) {
scrollBar.setBounds(l, t, r, b);
scrollBar.draw(canvas);
}
至此,View的绘制流程就分析完了,希望能对您有所帮助,若文中有错误或表述不当的地方还望指出,互相交流,共同成长!
相关文章
由setContentView探究Activity界面加载流程及Activity、Window和DecorView的关系
Android View 测量流程(Measure)源码解析
Android View 布局流程(Layout)源码解析