Draw process of Android custom View

preface

Android showcase Trilogy:

Measure -- > layout -- > Draw

We have already analyzed:

The two main tasks are to determine the rectangular area that can be drawn by View/ViewGroup.
Next, we will analyze how to draw the desired graph in this given area.

The Draw process involves a lot of knowledge and is divided into three parts

Through this article, you will learn:

1. Why customize the View
2. A simple Demo
3. View Draw process
4. ViewGroup Draw procedure
5. Analysis of common methods of View/ViewGroup

Why customize the View

Android provides two basic classes about View:

ViewGroup and View

However, the ViewGroup does not specify how its internal sub views are laid out. Are they superimposed together? Or horizontal placement, vertical placement, etc. Similarly, the View does not specify what its display content is, rectangle, circle, triangle, a picture, a paragraph of text, or irregular shape? Do we have to realize all this by ourselves?
Not necessarily. It is gratifying that Android has taken into account the above requirements. Some commonly used viewgroups and views have been prefabricated for development convenience.
For example:
Subclass inherited from ViewGroup

The sub views in FrameLayout -- > are stacked
The child views in LinearLayout -- > can be arranged vertically / horizontally
Relativelayout -- > the child views inside can be arranged relatively
The child views in recyclerview -- > are displayed in list form
Wait

Subclass inherited from View

Textview -- > used to draw a piece of text
ImageView -- > used to draw a picture
EditText -- > used to draw the input box
Button -- > user draw button
Wait

Although the above derived View/ViewGroup subclass has greatly facilitated us, it is only a general control in a general scene. If we want to achieve some more complex effects, such as wave shaped progress bar, luminous sphere, etc., these system controls are powerless, and there is no need to prefabricate strange controls. To achieve this effect, we need to customize the View/ViewGroup.
Generally speaking, there are several types of custom View/ViewGroup:

1. If you think the ViewGroup subclass provided by the system basically meets your needs, but you want to package some functions into a component, you can directly inherit FrameLayout, LinearLayout, etc. In this way, they inherit their characteristics and encapsulate their own logic.
2. If you think the View subclass provided by the system basically meets your needs, but you want to package some functions into a control, such as displaying Emoji, you can directly inherit from TextView(AppCompatTextView compatible).
3. If you despise the ViewGroup subclass prefabricated by the system and directly inherit from ViewGroup, you need to override onMeasure(xx), onLayout(xx) and other methods.
4. If you don't want to use the View subclass prefabricated by the system to directly inherit from View, you need to draw the content yourself and override the onDraw(xx) method.

3. Generally, it is not used much unless the layout is special. 1, 2 and 4 are our common means. For the "custom View" we often say, it generally refers to 4.
Next, let's see how 4 is implemented.

A simple Demo

public class MyView extends View {

    private Paint paint;

    public MyView(Context context) {
        super(context);
        init();
    }

    //This method is called when MyView is loaded from xml
    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //Paint red
        canvas.drawColor(Color.RED);

        //The brush is set to yellow
        paint.setColor(Color.YELLOW);
        //Draw a solid circle
        canvas.drawCircle(getWidth()/2, getHeight()/2, 30, paint);
    }

Reference MyView in xml

    <com.fish.myapplication.MyView
        android:id="@+id/myview"
        android:layout_width="100px"
        android:layout_height="100px">
    </com.fish.myapplication.MyView>

The effects are as follows:

The black part is the background of its parent layout.
The red rectangle + yellow circle is the content drawn by MyView.
The above is the simplest implementation of custom View. The key points we extract are summarized as follows:

1. Inherited from View
2. Rewrite the onDraw(xx) method (usually onMeasure(xx) also needs to be rewritten, which is ignored here to highlight the key points)

View Draw process

View onDraw(xx)

From the above Demo, we only need to draw the desired graph in the rewritten onDraw(xx) method.
Let's take a look at the default onDraw(xx) method of View:

#View.java
    protected void onDraw(Canvas canvas) {
    }

It is found that it is an empty implementation, so the class inherited from View must override the onDraw(xx) method to realize drawing. The passed in parameters of this method are: Canvas type.
Canvas is generally translated as canvas. After we get the canvas object in the rewritten onDraw(xx), we also need a pen with canvas. This pen is Paint, which is generally translated as paintbrush. When the two are combined, we can Paint (Paint) happily.
You may find that it is called in the Demo

canvas.drawColor(Color.RED);

No Paint is passed in. Is Paint not necessary? In fact, after calling this method, the underlying layer will automatically generate Paint objects.

#SkCanvas.cpp
void SkCanvas::drawColor(SkColor c, SkBlendMode mode) {
    SkPaint paint;
    paint.setColor(c);
    paint.setBlendMode(mode);
    this->drawPaint(paint);
}

You can see that the bottom layer initializes Paint, and the color set to it is the color set in the Java layer.

View Draw(xx)

onDraw(xx) is relatively simple. It starts with a Canvas, and the effect depends on painting.
Imagine how the Canvas came from, in other words, who called onDraw(xx). Let's play the association function. It was mentioned in the process of Measure and Layout that the two routines are very similar:

measure(xx) and layout(xx) generally do not need to be rewritten
onMeasure(xx), onLayout(xx)[View not required] need to be rewritten
onMeasure(xx) is called in measure(xx)
onLayout(xx) is called in layout(xx)

So is the drawing process the same routine? If you see On draw(xx), is drawing (XX) still far away?
Yes, there is a draw(xx) method:

#View.java
    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        //Marked as drawn
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        int saveCount;
        //Step 1 draw the background
        drawBackground(canvas);

        final int viewFlags = mViewFlags;
        //Check whether the edge gradient effect is set horizontally and vertically
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        
        //Conditional branch A
        if (!verticalEdges && !horizontalEdges) {
            //Step 3: call onDraw(xx) to draw the View content
            onDraw(canvas);(1)

            //Step 4 distribute Draw and Draw sub layout
            dispatchDraw(canvas); (2)
            //Draw auto filled highlights (not drawn by default)
            drawAutofilledHighlight(canvas);

            //Msoverlay is drawn above the content and below the foreground color (3)
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            //Step 6: draw decoration, such as foreground, scroll bar, etc. (4)
            onDrawForeground(canvas);

            //Step 7: draw the default highlight, which basically does not take effect in touch mode
            drawDefaultFocusHighlight(canvas);
            //For debugging, it can be ignored
            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            //After drawing, return directly
            return;
        }

        //Conditional branch B
        //There are a lot of source code below, which mainly does one thing: drawing edge gradient
        //I won't come here in most cases
        //The drawing steps are generally divided into seven steps, and only 1, 3, 4, 6 and 7 are listed above. The remaining steps are completed here
        //If the edge gradient is set, the drawing steps will be two more steps than when it is not set. The extra steps are: 2 and 5
        //Give a brief overview in words
        //1 --- > draw background
        //2 --- > canvas. Getsavecount(); record the canvas status to prepare for drawing edge gradient (canvas coordinates need to be changed, so save first)
        //3 --- > drawing content
        //4 - > distribute Draw and Draw sub layout
        //5 --- > Draw edge gradient
        //6 --- > Draw decoration
        //7 --- > Draw default highlight
    }

It can be seen that draw(xx) is mainly divided into two parts:

  • Conditional branch A – > this branch will be taken in most cases
  • Conditional branch B - > this branch will be used in very few cases
  • Branch B has 2 more steps than branch A to draw the edge gradient effect

Whether it is branch A or branch B, it has been drawn in several steps.
Generally speaking, the hierarchy of a single View is divided into:

Background – > content – > foreground

What is painted later may obscure what is painted earlier.
For a ViewGroup, the hierarchy is divided into:

Background – > content – > hierarchy of sub layouts – > foreground

Take a look at the four points marked on branch A:
(1)
onDraw(canvas)
As analyzed earlier, onDraw(xx) is an empty implementation for a single View, which needs to be drawn by us.
For the ViewGroup, there is no specific implementation. If onDraw(xx) is rewritten in the custom ViewGroup, will it execute? It will not be executed by default. Please move to the following step for related analysis:
Why didn't Android ViewGroup onDraw call

(2)
dispatchDraw(canvas) to see the implementation in View.java:

    protected void dispatchDraw(Canvas canvas) {

    }

It is found that it is an empty implementation. Look at the implementation in ViewGroup.java:

    protected void dispatchDraw(Canvas canvas) {
        ...
        //Traverse the sub layout and initiate the Draw process
        ...
    }

In other words, for a single View, there is no sub layout, so it is not necessary to distribute Draw. For ViewGroup, it is necessary to trigger its sub layout to initiate the Draw process (subsequent analysis of this process), which can be similar to the processing of event distribution process View and ViewGroup. If you are interested, please move to:
Android input event to the end View disk man (3)

(3)
OverLay, as its name implies, is "over something". Here, it is after drawing the content and before drawing the foreground. How?

        View viewGroup = findViewById(R.id.myviewgroup);
        //Specify a Drawable for overLay
        Drawable drawable = ContextCompat.getDrawable(this, R.drawable.shapeme);
        //Set the size of Drawable
        drawable.setBounds(0, 0, 400, 58);
        //Add Drawable for overLay
        viewGroup.getOverlay().add(drawable);

The above is to set overLay for a ViewGroup. The effect is as follows:

The black part is the ViewGroup background
Red rectangle + yellow circle is the sub layout
The yellow rectangle is the overLay added for the ViewGroup. You can see that the overLay is drawn on the content.
(4)
onDrawForeground(xx)
Draw the foreground as follows:

        View viewGroup = findViewById(R.id.myviewgroup);
        Drawable drawable = ContextCompat.getDrawable(this, R.drawable.shapeme);
        drawable.setBounds(0, 0, 400, 58);
        viewGroup.setForeground(drawable);

You may find that this is similar to setting overLay. In fact, there is still a difference. In ondrawforegroup (XX), the size of the Drawable will be adjusted again, which is consistent with the size of the View. The size previously set for the Drawable will become invalid. The operation effect is as follows:

It can be seen that the ViewGroup is covered by the foreground.
Let's look at the focus of branch B: edge gradient effect
Let's take a look at the edge gradient effect of TextView:

This is a TextView, displayed in the form of a running lantern.
Add the edge gradient effect to its horizontal direction. As shown above, both sides are gradient.
How?

    //Horizontal or vertical
    android:requiresFadingEdge="horizontal"
    //Length of gradient
    android:fadingEdgeLength="100dp"

Add these two parameters.
In fact, this effect is also used in some controls of the system, such as NumberPicker and YearPickerView

The above is the effect of NumberPicker, which can be seen as a gradient in the vertical direction.

ViewGroup Draw procedure

The onDraw(xx) and draw(xx) in View.java are not rewritten in ViewGroup.java.
For dispatchDraw(xx), it is an empty implementation in View.java. Initiate the drawing of sub layout in ViewGroup.java.

ViewGroup dispatchDraw(xx)

#ViewGroup.java
    @Override
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;
        //Animation related
        ...
        int clipSaveCount = 0;
        //After padding is set, the sub layout drawn cannot exceed padding (1)
        final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
        if (clipToPadding) {
            //Therefore, it is necessary to transform the canvas coordinates and save its state first
            clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                    mScrollX + mRight - mLeft - mPaddingRight,
                    mScrollY + mBottom - mTop - mPaddingBottom);
        }

        //Reset related tags
        mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
        ...
        for (int i = 0; i < childrenCount; i++) {
            ...
            //Traverse the sub layout and start the sub layout drawing
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime); (2)
            }
        }
        ...
    }

Take a look at the 2 points marked:
(1)
The purpose of setting padding is to leave a certain gap for the sub layout. Therefore, when padding is set, the canvas of the sub layout needs to be cut according to the padding. The judgment mark is:

(flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK

protected static final int CLIP_TO_PADDING_MASK = FLAG_CLIP_TO_PADDING | FLAG_PADDING_NOT_NULL;

FLAG_CLIP_TO_PADDING is set to true by default
FLAG_PADDING_NOT_NULL as long as padding is not 0, the tag will be marked.
That is, as long as padding is not set to 0, the sub layout display area needs to be reduced.
Can you not let the sub layout reduce the display area?
The answer is yes.
Consider a scenario: when using RecyclerView, we need to set paddingTop = 20px. The effect is that when the RecyclerView Item is displayed, it is 20px away from the top, but it will never roll to the top when scrolling. It doesn't look so friendly. This is why the above reduction works. You need to prohibit this action. By setting:

setClipToPadding(false)

Of course, you can also set in xml:

android:clipToPadding="false"

(2)
drawChild(xx)

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

From the method name, it calls the sub layout to draw.
There are two situations in child.draw(x1,x2,x3):

First, hardware accelerated rendering
The second is software drawing

The specific functions and differences between the two will be analyzed in the next article. Whether it is hardware accelerated rendering or software accelerated rendering, it will eventually call the View.draw(xx) method, which has been analyzed above.
Note that draw(x1,x2,x3) is different from draw(xx). Don't confuse it.

Analysis of common methods of View/ViewGroup

It is shown in Figure:

Contact of View/ViewGroup Draw process:

Generally speaking, we usually customize the View and override its onDraw(xx) method. Is there a ViewGroup requirement for drawing content?
Yes, for example, you can take a look at the drawing of RecyclerView ItemDecoration, which uses the drawing sequence of ViewGroup draw(xx), ViewGroup onDraw(xx) and View onDraw(xx) to realize the functions of dividing lines and group head hovering.

This article is based on Android 10.0

Tags: Android

Posted by king arthur on Thu, 07 Oct 2021 00:05:40 +0530