preface

Draw process involves more knowledge, divided into three chapters

  • Android custom View Draw process (part 1)
  • Android custom View Draw process (in)
  • Android custom View Draw process (part 2)

This article will analyze hardware acceleration drawing and software drawing in depth from the perspective of code. Through this article, you will learn:

1. Software drawing process 2. Hardware accelerated drawing process 3. Influence of LayerType on drawing 4

Software drawing process

In View otimpl -> Draw (XX) software drawing and hardware accelerated drawing diverging:


drawSoftware(xx)

#ViewRootImpl.java private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, Boolean scalingRequired, Rect dirty, Rect surfaceInsets) {// Hold Canvas final Canvas Canvas; . try { ... Canvas = msurface.lockCanvas (dirty); canvas = msurface.lockCanvas (dirty); // Set the density canvas.setDensity(mDensity); } catch (Surface.OutOfResourcesException e) { ... } catch (IllegalArgumentException e) { ... return false; } finally { ... } try {// Whether canvas needs to be moved canvas.translate(-xoff, -yoff); DecorView // RootView draw() method mview.draw (canvas);  } the finally {try {/ / the content of the submitted drawing to the Surface Surface. UnlockCanvasAndPost (canvas); } catch (IllegalArgumentException e) { ... } } return true; }Copy the code

The key functions of the above methods are as follows:

1. Apply for Canvas object from Surface, whose Canvas is of type CompatibleCanvas. Call View.draw(Canvas) and start drawing RootView. 3. After drawing the entire ViewTree, submit the content to Surface

Note: RootView is just a name, not the name of a View. Some common RootView please move: Android input events to the source of water (1)

View.draw(xx) method in: Android custom View draw process (on) has done a detailed analysis, combined with the above code, with the following figure:

It can be seen that software drawing has the following characteristics:

  • The draw(xx) method of the sublayout is recursively called from RootView until every qualifying View is drawn
  • During drawing, all views hold the same Canvas object

Introducing question 1: Since all views have the same Canvas, how are the starting and ending points of each View drawn determined? This problem will be discussed later.

Hardware speeds up the drawing process

The profile

Software drawing is to write a series of operations of Canvas into Bitmap, while for hardware-accelerated drawing, each View has a RenderNode. When drawing is needed, a RecordingCanvas is obtained from the RenderNode, just like software drawing. It also calls the Canvas set of apis, but those apis are recorded as a set of actions stored in the DisplayList. When a View is finished recording, the DisplayList is passed to the RenderNode. At this point, the rendering steps are recorded in RenderNode, and the hardware of the individual View is drawn. This process is also known as the DisplayList construction process.

Call procedure analysis

Take a look at the hardware acceleration entry:

#ThreadedRenderer.java void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) { ... UpdateRootDisplayList (rootRenderNode) updateRootDisplayList(rootRenderNode) callbacks); Render --> render --> render --> render --> render --> render --> render --> render --> The proxy in turn is associated with rootRenderNode // so the last recorded drawing operation is finally handed over to a separate thread to render int syncResult = syncAndDrawFrame(choreograph.mframeInfo); . }Copy the code

Focus on the recording process and then analyze it:

Private void updateRootDisplayList(View View, DrawCallbacks DrawCallbacks) { Build the DisplayList updateViewTreeDisplayList (view); / / when ViewTree DisplayList mRootNode build finished / / the beginning is not DisplayList if (mRootNodeNeedsUpdate | |! Mrootnode.hasdisplaylist ()) {// Apply Canvas RecordingCanvas Canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight); try { ... / / the updateDisplayListIfDirty () returns the RootView associated renderNode / / will now RootView renderNode on canvas, It can connect all the renderNode canvas. DrawRenderNode (the updateDisplayListIfDirty ()); . mRootNodeNeedsUpdate = false; } finally {// Hang DisplayList under renderNode mrootNode.endrecording (); }}} private void updateViewTreeDisplayList View (View) {/ / tag this View has been drawn over the mPrivateFlags | = the PFLAG_DRAWN; //mRecreateDisplayList --> indicates whether the DisplayList needs to be rebuilt, and whether the DisplayList needs to be re-recorded. That is the View need to refresh, you may need to rebuild the mRecreateDisplayList = (View. MPrivateFlags & View. PFLAG_INVALIDATED) = = the PFLAG_INVALIDATED; MPrivateFlags &= ~ view. pflag_inflag_validated; / / if necessary, update the View DisplayList the updateDisplayListIfDirty (); / / the View is rebuilding, no need to rebuild the mRecreateDisplayList = false; }Copy the code

This calls the method in the View: updateDisplayListIfDirty(). As the name implies, update the View’s DisplayList if necessary.

RenderNode updateDisplayListIfDirty() {RenderNode is created for each View :mRenderNode, RenderNode = mRenderNode; / / support hardware acceleration, by judging the AttachInfo mThreadedRenderer if (! canHaveDisplayList()) { return renderNode; Render node does not have DisplayList yet. Render node has DisplayList, but needs to update. Conditions to block the if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) = = 0 | |! RenderNode. HasDisplayList () | | (mRecreateDisplayList)) {/ / if the DisplayList DisplayList and without update, Shows the View does not need to go if the Draw process (renderNode. HasDisplayList () &&! MRecreateDisplayList) {/ / tag this View has been mapped and the cache is valid mPrivateFlags | = PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; // Continue to see if the sublayout needs to build DisplayList dispatchGetDisplayList(); //---------(1) return renderNode; MRecreateDisplayList = true; mRecreateDisplayList = true; mRecreateDisplayList = true; // Width = mright-mleft; // Width = mright-mleft; int height = mBottom - mTop; Int layerType = getLayerType(); // This Canvas is of the RecordingCanvas type, Simple as the Canvas is used to record the final RecordingCanvas Canvas = renderNode. BeginRecording (width, height); If (layerType == LAYER_TYPE_SOFTWARE) {//---------(2) // Build the cache buildDrawingCache(true); Bitmap cache = getDrawingCache(true); if (cache ! Canvas. DrawBitmap (cache, 0, 0, mLayerPaint); }} else {// If the software draw cache is not set // generally with Scroller sliding use computeScroll(); // When mScrollX is positive, the canvas moves to the left and the contents are drawn to the left. That's why the scroll is positive. Canvas. Translate (-mscrollx, -mscrolly); mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; //---------(3) if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {// This View does not need to draw its own content (including content, foreground, background, etc.) dispatchDraw(canvas); drawAutofilledHighlight(canvas); if (mOverlay ! = null && ! mOverlay.isEmpty()) { mOverlay.getOverlayView().draw(canvas); } if (debugDraw()) { debugDrawFocus(canvas); }} else {// Draw your own draw(canvas); RenderNode //---------(4) renderNode.endrecording (); renderNode.endrecording (); setDisplayListProperties(renderNode); }} else {/ / does not meet the three conditions that have been mapped mPrivateFlags | = PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; } / / return renderNode onto a layer / / -- -- -- -- -- -- -- -- - (5) return renderNode; }Copy the code

(1) dispatchGetDisplayList() this method is not implemented in View, but is implemented in ViewGroup:

#ViewGroup.java protected void dispatchGetDisplayList() { final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; I ++) {// Final View child = children[I]; if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() ! DisplayList recreateChildDisplayList(child); }}... } private void recreateChildDisplayList (View child) {/ / determine whether need to rebuild the child. MRecreateDisplayList = (child. MPrivateFlags & PFLAG_INVALIDATED) ! = 0; child.mPrivateFlags &= ~PFLAG_INVALIDATED; / / call child layout reconstruction method child. UpdateDisplayListIfDirty (); child.mRecreateDisplayList = false; }Copy the code

DispatchGetDisplayList does:

Iterate over the child layouts and call their rebuild methods

In this case, we recursively call updateDisplayListIfDirty() from RootView. If the child layout needs to rebuild the DisplayList, we record the drawing operation again, otherwise we continue to find whether the child layout needs to rebuild the DisplayList.

BuildDrawingCache (xx) is used to draw the off-screen cache, more on that later.

Android ViewGroup onDraw why not call

(4) Hardware-accelerated drawing with start, record and end marks:

RenderNode generates Canvas > beginRecording to begin with. RenderNode drawXX() : renderNode drawXX() : renderNode drawXX() : renderNode drawXX() : renderNode drawXX() : renderNode drawXX DisplayList, and assign the result to renderNode, which is the end of recording

(5) As can be seen from point 4, the recorded result has been stored in RenderNode, and the RenderNode needs to be returned. The RenderNode will be hung on the parent Canvas, that is, the parent Canvas already holds the DisplayList recorded by the child.

To simplify the hardware-accelerated drawing process by graphing a single View:

ViewTree hardware acceleration process:

The effect of LayerType on drawing

The above describes the process of software rendering and hardware accelerated rendering respectively. The starting point of analysis is whether the Window supports hardware acceleration and goes to different branches. All descendant views from RootView through are either software-drawn or hardware-accelerated. What if you disable hardware acceleration for a View in the middle of a hardware-accelerated drawing? We mentioned earlier that hardware acceleration is disabled by setting View->LayerType, and we’ll examine how LayerType affects the drawing process. From Android custom View Draw process (above) analysis can be seen: regardless of software drawing or hardware accelerated drawing, will go through a set of common processes:

draw(xx)->dispatchDraw(xx)->draw(x1,x2,x3)->draw(xx)...
Copy the code

This is also a recursive call. For a single View, where does software rendering and hardware acceleration diverge? The answer is: draw(x1,x2,x3) method

View’s harddraw bifurcation point

#View.java boolean draw(Canvas canvas, ViewGroup parent, Long drawingTime) {/ / / / canvas support hardware acceleration is the default canvas does not support hardware acceleration / / RecordingCanvas support hardware acceleration final Boolean hardwareAcceleratedCanvas  = canvas.isHardwareAccelerated(); RenderNode (RenderNode, RenderNode, RenderNode, RenderNode) Canvas supports hardware acceleration + This Window supports hardware acceleration Boolean drawingWithRenderNode = mAttachInfo! = null && mAttachInfo.mHardwareAccelerated && hardwareAcceleratedCanvas; // Animation related... If (hardwareAcceleratedCanvas) {/ / canvas support hardware acceleration, DisplayList mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED)! = 0; mPrivateFlags &= ~PFLAG_INVALIDATED; } RenderNode renderNode = null; Bitmap cache = null; Int LayerType = getLayerType(); //------>(1) if (layerType == LAYER_TYPE_SOFTWARE || ! DrawingWithRenderNode) {// If (layerType! LAYER_TYPE_NONE) {LAYER_TYPE_SOFTWARE = LAYER_TYPE_SOFTWARE; LAYER_TYPE_SOFTWARE = LAYER_TYPE_SOFTWARE; //------>(2) buildDrawingCache(true); } getDrawingCache(true); } if (drawingWithRenderNode) {//----->(3) // This View supports hardware acceleration // try to build DisplayList, RenderNode = updateDisplayListIfDirty(); if (! RenderNode. HasDisplayList ()) {/ / general rarely go this renderNode = null; drawingWithRenderNode = false; } } int sx = 0; int sy = 0; if (! drawingWithRenderNode) { computeScroll(); Sx = mScrollX; sx = mScrollX; sy = mScrollY; // Handle a Final Boolean drawingCache = Case of cache! // Case of hardware deposite (); // Handle a Final Boolean drawingcache = case of cache! = null && ! drawingWithRenderNode; If hardware acceleration is not supported, final Boolean offsetForScroll = cache == null &&! drawingWithRenderNode; If (offsetForScroll) {//------>(4) Canvas. Translate (mleft-sx, mtop-sy); } else { if (! DrawingWithRenderNode) {//------>(5) // If hardware acceleration is not supported, it is possible to draw with software cache. Canvas. Translate (mLeft, mTop); }... }... if (! DrawingWithRenderNode) {hardware acceleration is not supported if ((parentFlags & viewGroup.flag_clip_children)! If (offsetForScroll) {if (offsetForScroll) {if (offsetForScroll) { //------>(6) canvas. ClipRect (sx, sy, sx + getWidth(), sy + getHeight()); } else {// If (! scalingRequired || cache == null) { canvas.clipRect(0, 0, getWidth(), getHeight()); } else { canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight()); }}}... } if (! Drawingcache) {// Not drawing if (drawingWithRenderNode) {// Support hardware speed mPrivateFlags &= ~PFLAG_DIRTY_MASK; // Attach the View's renderNode to the parent Canvas, where a connection is established ((RecordingCanvas) Canvas). DrawRenderNode (renderNode); } else {dispatchDraw(canvas) & draw(canvas); //------>(7) if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas); } else { draw(canvas); } } } else if (cache ! MPrivateFlags &= ~PFLAG_DIRTY_MASK; if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) { ... DrawBitmap (cache, 0.0f, 0.0f, cachePaint); drawBitmap(cache, 0.0f, 0.0f, cachePaint); } else { ... Canvas. DrawBitmap (cache, 0.0 f, 0.0 f, mLayerPaint); }}... MRecreateDisplayList = false; return more; }Copy the code

The judgment in this method is rather messy, and 7 important points are extracted: (1) As long as off-screen software cache is set or hardware acceleration is not supported, software cache is needed to draw.

(3) Hardware acceleration is used as long as hardware acceleration is supported. Is it a little paradoxical to combine PI (1)? Consider one of the cases where condition (1) is met: off-screen software caching is enabled, hardware acceleration is also supported, and software cache drawing is enabled, according to the logic of condition (1). So (3) isn’t it unnecessary to continue with hardware-accelerated drawing? Review the clip in updateDisplayListIfDirty() :

        if (layerType == LAYER_TYPE_SOFTWARE) {
            ...
            //软件缓存绘制
            buildDrawingCache(true);
        } else {
            //硬件绘制
            ...
        }
Copy the code

Here we’re making a judgment again.

Canvas displacement For software drawing, Canvas displacement is carried out, and the displacement distance takes into account the offset of View itself and the offset of View content. For software cache drawing, the Canvas is shifted, and only the View itself is considered. For hardware-accelerated drawing, no Canvas displacement is seen. In fact, for software cache drawing and hardware accelerated drawing, Canvas displacement includes both View itself offset and View content offset. Just not in the code above. For software cache drawing:

In buildDrawingCacheImpl(xx) -> Canvas. Translate (-mscrollx, -mscrolly); The content is offset.

For hardware-accelerated drawing:

In the layout (xx) – > mRenderNode. SetLeftTopRightBottom (mLeft, mTop, mRight, mBottom) has carried on the migration of the View itself. In updateDisplayListIfDirty (xx) – > canvas. Translate (mScrollX, — mScrollY); The content is offset.

Therefore, no matter software drawing/software cache drawing/hardware accelerated drawing, all three have shifted Canvas, including the offset of View itself and the offset of content.

So that explains question 1.

Canvas clipping For software drawing, Canvas clipping includes View content offset. For software cache drawing, the Canvas draws into the Bitmap. For hardware accelerated rendering, the setDisplayListProperties (xx) – > renderNode. SetClipToBounds cut (xx).

(7) If it is software drawing, then directly call dispatchDraw(xx)/draw(xx) to initiate drawing.

The draw(x1,x2,x3) method determines which View to draw:

1. Hardware acceleration rendering; 2. Software rendering; 3

Software cache drawing

Let’s see how to build a software cache:

Java public void buildDrawingCache(Boolean autoScale) {// If ((mPrivateFlags &) PFLAG_DRAWING_CACHE_VALID) == 0 || (autoScale ? MDrawingCache == NULL: mUnscaledDrawingCache == null)) {try {// Build cache buildDrawingCacheImpl(autoScale); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } } private void buildDrawingCacheImpl(boolean autoScale) { int width = mRight - mLeft; int height = mBottom - mTop; . boolean clear = true; Bitmap bitmap = autoScale ? mDrawingCache : mUnscaledDrawingCache; // The bitmap does not exist or the size of the bitmap is inconsistent with that of the View. Canvas canvas; if (attachInfo ! = null) { canvas = attachInfo.mCanvas; If (canvas == null) {// For the first time, there is no canvas canvas = new Canvas(); } // Associate bitmap canvas.setbitmap (bitmap); attachInfo.mCanvas = null; } else {// seldom walk this canvas = new canvas (bitmap); } computeScroll(); final int restoreCount = canvas.save(); Canvas. Translate (-mscrollx, -mscrolly); mPrivateFlags |= PFLAG_DRAWN; if (mAttachInfo == null || ! mAttachInfo.mHardwareAccelerated || mLayerType ! = LAYER_TYPE_NONE) {/ / branding, explain software rendering cache is effective mPrivateFlags | = PFLAG_DRAWING_CACHE_VALID; } // Again, call the public method if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas); drawAutofilledHighlight(canvas); if (mOverlay ! = null && ! mOverlay.isEmpty()) { mOverlay.getOverlayView().draw(canvas); } } else { draw(canvas); } canvas.restoreToCount(restoreCount); canvas.setBitmap(null); if (attachInfo ! = null) {attachInfo.mcanvas = canvas; }}Copy the code

As a result, the software cache is built and the results are stored in a Bitmap, which can be retrieved as follows:

Java public Bitmap getDrawingCache(Boolean autoScale) {if ((mViewFlags &) is disabled by default WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) { return null; } if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) {// Software cache drawing is enabled, // Build cache buildDrawingCache(autoScale) is disabled by default; } // Return the cache to autoScale? mDrawingCache : mUnscaledDrawingCache; }Copy the code

This method can be used to get the page of the View. Make a summary:

In the beginning, the hardware acceleration drawing process and the software drawing process go separately. 1. When software drawing is used, the off-screen cache type is set: software cache, software drawing is invalid and only software cache is used for drawing. Set the hardware cache type to also draw as software cache. 2. When hardware accelerated drawing is used, the off-screen cache type is set: software cache. If hardware accelerated drawing is invalid, only software cache is used for drawing. This is why setting software caching can disable hardware acceleration. 3. The result of software cache drawing is saved in bitmap, which will be drawn to the Canvas of the parent layout.

No matter which drawing type is used, the common call method is draw(xx)/dispatchDraw(xx). Therefore, the draw type is transparent to our rewriting onDraw(xx).

Where does Canvas come from and where does it go

Software drawing starts from Viewrotimpl ->drawSoftware(xx) by:

canvas = mSurface.lockCanvas(dirty);
Copy the code

Canvas is generated. This Canvas is passed to all sublayouts via the view.draw (xx) method, so the entire ViewTree shares the same Canvas in this case. Canvas type: CompatibleCanvas. Hardware-accelerated drawing starts from View->updateDisplayListIfDirty(xx) by:

final RecordingCanvas canvas = renderNode.beginRecording(width, height);
Copy the code

Canvas is generated. As you can see, Canvas is regenerated for each View that supports hardware acceleration. Canvas type: RecordingCanvas.

Software cache drawing starts from View->buildDrawingCacheImpl(xx) with:

canvas = new Canvas();
Copy the code

The Canvas is generated and recorded in AttachInfo to be used next time the View software cache is built again. As you can see, a new Canvas is generated for each View that uses the software cache, which can be reused if AttachInfo has it.

The above three Canvas without View have a common feature: the generated Canvas is eventually connected with the Surface, so the content drawn by these Canvas can finally be displayed on the screen. Is it possible to construct a Canvas that is separate from the View? The answer is yes.

private void buildCanvas(int width, int height) { Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(); canvas.setBitmap(bitmap); / / drawing canvas. DrawXX (xx); . }Copy the code

As shown above, create a Canvas and a Bitmap and associate the two. Finally, call the Canvas drawing API, and the drawing result will be saved in Bitmap. This process is actually how software cache drawing is used. Of course, once we’ve got the Bitmap, we want it to be relatively easy to display, just associate it with the View and display it on the screen. Associating with a View is essentially using the Canvas associated with the View to draw the generated Bitmap on it,

Draw process family photo

Diagram the drawing process:

Pure software drawing and hardware accelerated drawing:

Set the software cache when drawing:

This concludes the Draw process series. This article is based on Android 10.