English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Comprehensive Analysis of Android View Drawing Process (Draw)

Introduction

In the previous articles, the author has separately described the DecorView, measure, and layout processes, and will analyze the last process of the three major workflows in detail - the drawing process. The measurement process determines the size of the View, the layout process determines the position of the View, so the drawing process determines the appearance of the View, what a View should display is completed by the drawing process. The following source code is taken from the Android API 21.

Starting from performDraw

Previous articles have mentioned that the three major workflows begin with ViewRootImpl#performTraversals, where the methods performMeasure, performLayout, and performDraw are called respectively to complete the measurement, layout, and drawing processes. So let's start with the performDraw method, ViewRootImpl#performDraw:

private void performDraw() {
 //...
 final boolean fullRedrawNeeded = mFullRedrawNeeded;
 try {
  draw(fullRedrawNeeded);
 } finally {}}
  mIsDrawing = false;
  Trace.traceEnd(Trace.TRACE_TAG_VIEW);
 }
 //Omitted...
}

It calls the ViewRootImpl#draw method again and passes in the fullRedrawNeeded parameter, which is obtained from the mFullRedrawNeeded member variable. Its role is to determine whether to redraw all views. If it is the first time to draw the view, it is obvious that all views should be drawn. If for some reason, the view needs to be redrawn, there is no need to draw all views. Let's take a look at ViewRootImpl#draw:

private void draw(boolean fullRedrawNeeded) {
 ...
 //Obtain mDirty, which represents the area that needs to be redrawn
 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;
 }
 //If fullRedrawNeeded is true, set the dirty area to the entire screen, indicating that the entire view needs to be redrawn
 //The first drawing process requires drawing all views
 if (fullRedrawNeeded) {
  mAttachInfo.mIgnoreDirtyState = true;
  dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
 }
 //Omitted...
 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
    return;
  }
}

Some code has been omitted here; let's focus on the key code. First, we obtain the value of mDirty, which stores the information of the area that needs to be redrawn. The topic of view redrawing will be covered in a dedicated article later. For now, let's get familiar with it. Then, based on the value of fullRedrawNeeded, we determine whether to reset the dirty area. Finally, we call the ViewRootImpl#drawSoftware method and pass in the relevant parameters, including the dirty area. Let's take a look at the source code of this method:

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;
  //Lock the canvas area, determined by the dirty area
  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(scalingRequired63; mNoncompatDensity : 0);
   attachInfo.mSetIgnoreDirtyState = false;
   //start drawing formally
   mView.draw(canvas);
  }
 } 
 return true;
}

First, an instance of the Canvas object is created, then the area of the canvas is locked, determined by the dirty area, followed by a series of property assignments on the canvas, and finally, the method mView.draw(canvas) is called. As analyzed before, mView is DecorView, which means that the drawing starts from DecorView. All the work done before was preparation, and now it is the formal start of the drawing process.

View's drawing

Since ViewGroup does not override the draw method, all Views call the View#draw method, so let's look at its source code directly:

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:
  *
  *  1draw the background
  *  2if necessary, save the canvas' layers to prepare for fading
  *  3draw the view's content
  *  4draw children
  *  5if necessary, draw the fading edges and restore layers
  *  6draw decorations (scrollbars for instance)
  */
 // Step 1draw 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;
 }
 ...
}

It can be seen that the draw process is relatively complex, but the logic is very clear, and the official comments also clearly explain the method of each step. Let's first look at the initial mark dirtyOpaque, the role of which is to judge whether the current View is transparent. If the View is transparent, according to the logic below, some steps will not be executed, such as drawing the background, drawing the content, etc. This is easy to understand because if a View is transparent, there is no need to draw it. Then are the six steps of the drawing process. Here we first summarize what these six steps are, and then we will elaborate on them.

The six steps of the drawing process:
1draw the background of the View
2save the current layer information (can be skipped)
3draw the content of the View
4draw the subViews of the View (if there are subViews)
5draw the faded edge of the View, similar to a shadow effect (can be skipped)
6draw the decorations of the View (such as: scrollbars)
Among them, the2Step and the5This step can be skipped, we will not analyze it here, we will focus on analyzing the other steps.

Skip 1:Draw background

Here, View#drawBackground method is called, let's take a look at its source code:

private void drawBackground(Canvas canvas) {
 //mBackground is the background parameter of this View, such as background color
 final Drawable background = mBackground;
 if (background == null) {
  return;
 }
 //Determine the boundary of the background according to the four layout parameters of the View
 setBackgroundBounds();
 ...
 //Get the current View's mScrollX and mScrollY values
 final int scrollX = mScrollX;
 final int scrollY = mScrollY;
 if ((scrollX | scrollY) == 0) {
  background.draw(canvas);
 } else {
  //If scrollX and scrollY have values, then offset the canvas coordinates and draw the background
  canvas.translate(scrollX, scrollY);
  background.draw(canvas);
  canvas.translate(-scrollX, -scrollY);
 }
}

It can be seen that the offset parameters of the view, scrollX and scrollY, are considered here, and the background is drawn in the offset view.

Skip 3:Draw content

Here, View#onDraw method is called, and the method in View is an empty implementation because different Views have different contents, which requires us to implement it ourselves, that is, to overwrite the method in the custom View to implement it.

Skip 4: Draw child View

If the current View is a ViewGroup type, then it is necessary to draw its child View, here dispatchDraw is called, and the method in View is an empty implementation, actually ViewGroup overrides this method, so let's take a look at, 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);
  }
 }
 //Omitted...
}

The source code is quite long, so I will briefly explain it here. It mainly iterates over all child Views, and each child View calls the drawChild method. We find this method, ViewGroup#drawChild:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
  return child.draw(canvas, this, drawingTime);
}

It can be seen that the View's draw method is called here, but this method is not the one mentioned above because the parameters are different. Let's take a look at this method, View#draw:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
 //Omitted...
 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, and also restore
   int layerPaintAlpha = mLayerPaint.getAlpha();
   mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
   canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
   mLayerPaint.setAlpha(layerPaintAlpha);
  }
 }
}

我们主要来看核心部分,首先判断是否已经有缓存,即之前是否已经绘制过一次了,如果没有,则会调用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 != null63; 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);
 }
}

It can be seen that the logic is very clear, and it is very similar to the general drawing process. It is first set to draw the area, and then draw using canvas. There is no need to elaborate on this in detail. Those who are interested can continue to learn more.

So far, the drawing process of View has been explained, I hope this article will be helpful to you, thank you for reading.

More Reading
Comprehensive Analysis of Android View Measurement Process (Measure)
Comprehensive Analysis of Android View Layout Process (Layout)

That's all for this article. I hope it will be helpful to your study, and I also hope everyone will support the Nahan Tutorial more.

Declaration: The content of this article is from the Internet, the copyright belongs to the original author. The content is contributed and uploaded by Internet users spontaneously. This website does not own the copyright, has not been manually edited, and does not assume any relevant legal responsibility. If you find any content suspected of copyright infringement, please send an email to: notice#w3Please report via email to codebox.com (replace # with @) and provide relevant evidence. Once verified, this site will immediately delete the infringing content.

You May Also Like