A, cause

We ran into this problem when we overwrote the onDraw() method in a custom ViewGroup and drew the screen in onDraw, but when it ran, the screen did not work. Then I wrote a simple Demo, and printed the log to see the drawing process, as follows:

From the above picture, it can be clearly found that onMeasure, onLayout and dispatchDraw are used while onDraw method is not called.

Then I set a background color in the XML of the custom ViewGroup and run the print log

At this point you can see that the onDraw method is called before dispatchDraw.

Two custom viewgroups with the same code, just changing the background of the ViewGroup in XML, one will call onDraw and the other will not, so why not use the onDraw method without changing the ViewGroup? So how to solve this problem?

With questions in mind, we go to the source code to find the cause and the solution.

Second, analyze the reasons

As we all know, a ViewGroup inherits from a View, so just look at the View. ViewGroup OnDraw (Canvas Canvas) {draw(Canvas Canvas);

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: * * 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);
            drawAutofilledHighlight(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);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
Copy the code

In this code, it is clearly marked that drawing is divided into several steps, but today’s main character is not that. Let’s go straight to the following sentence of Step 3

 if(! dirtyOpaque) onDraw(canvas);Copy the code

The point! The point! Focus on onDraw before there is a condition! If dirtyOpaque is true, onDraw() will be executed. If the View subclass is true, onDraw() will be executed. So let’s go ahead and see, what is dirtyOpaque?

final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null| |! mAttachInfo.mIgnoreDirtyState);Copy the code

ViewGroup onDraw() ¶ dirtyOpaque is a Boolean that can be used to execute onDraw() when the background is opaque. The value of dirtyOpaque is determined by privateFlags, and privateFlags is assigned by a global variable called mPrivateFlags.

MPrivateFlags is initialized by finding a function computeOpaqueFlags in the last line of the View constructor.

   protected void computeOpaqueFlags() {
        // Opaque if:
        // - Has a background
        // - Background is opaque
        // - Doesn't have scrollbars or scrollbars overlay

        if(mBackground ! =null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
            mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
        } else{ mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND; }... }Copy the code

Code analysis If the background is not empty and is opaque, set mPrivateFlags to PFLAG_OPAQUE_BACKGROUND and vice versa. So the analysis here is roughly clear, we have a basic idea:

  1. ComputeOpaqueFlags () determines whether the background of the current view is empty and transparent, and assigns the corresponding value to mPrivateFlags;
  2. Then use mPrivateFlags in draw() to determine whether dirtyOpaque is true or flase;
  3. OnDraw is executed only if dirtyOpaque is false,That is, onDraw is executed when the background is not empty and not transparent.

So back to the question at the beginning of this article, why doesn’t ViewGroup go onDraw? Is the ViewGroup background empty or transparent? So let’s verify that.

In ViewGroup source code, ViewGroup constructor has to initialize the ViewGroup, and in this initialization shows the background of the ViewGroup. As follows:

 public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        initViewGroup();
        initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
    }

    private void initViewGroup() {
        // ViewGroup doesn't draw by default
        if (!debugDraw()) {
            setFlags(WILL_NOT_DRAW, DRAW_MASK);
        }
        ...
    }
Copy the code

(ViewGroup doesn’t draw by default, ViewGroup doesn’t draw by default.) You can also see in the code that setFlags() passes WILL_NOT_DRAW, which means the View will not be drawn and the onDraw method will not be called. This is where the reason comes in

By default, the ViewGroup’s background is transparent, whereas the draw method in the View means that only opaque onDraw is executed, which invalidates subsequent ViewGroup ondraws.

Third, solutions

3.1. Method 1

In Step 4 of the draw() code at the beginning of this article, dispatchDraw(Canvas) has no conditions to move processing from onDraw to dispatchDraw().

3.2. Method 2

In the source code, the initial background of the ViewGroup is set to be transparent by setFlags. According to setFlags, you can find a method setWillNotDraw,

public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
Copy the code

The setFlags source code only wraps a layer for external calls, so we can call this method in the ViewGroup constructor and pass in the argument false, i.e

setWillNotDraw(false);
Copy the code

3.3. Method three

Add a background directly under the ViewGroup XML.

Why is View not limited

There’s a little bit of code in the View constructor

 int viewFlagValues = 0; .if(viewFlagMasks ! =0) {
    setFlags(viewFlagValues, viewFlagMasks);
}
Copy the code

Flag is set opposite to ViewGroup during View initialization, resulting in View not being affected by default.