1. Introduction

In the development, we often encounter a variety of views, some of which are provided by the system, some of which are our own View, we can see the importance of View in the development, so understand the Android View drawing process for us to better understand the working principle of View and custom View is quite beneficial. This article will explore the drawing process of View according to Android source code (API=30), deepen our understanding and cognition of it.

2.View Overview of drawing process

A page of the application is composed of a variety of views, which can be presented on the screen according to our expectations and achieve our needs. Behind it is a complex drawing process, which mainly involves the following three processes:

  1. Measure: As the name implies, it means to measure. In this stage, the main work is to measure the size of the View and save it.

  2. Layout: This is the layout stage, in this stage is mainly based on the size of the View obtained in the last measurement stage and the View itself parameter Settings to determine the position of the View should be placed.

  3. Draw: This stage is quite important and mainly performs the task of drawing. It completes the drawing of the View based on the results of measurement and layout, so that we can see the colorful interface.

    Fortunately, the system handles a lot of such work for us. When we need to implement a custom View, the system also provides onMeasure(), onLayout(), onDraw() methods. Generally speaking, we rewrite these methods. By adding our own business logic, we can implement our custom View requirements.

3.View drawn entrance

The performTraversals() method in ViewRootImpl involves performMeasure(), performLayout() and performDraw(). PerformMeasure () starts from the root of the ViewTree, and performLayout() starts from the root of the ViewTree. The performDraw() method performs the task of drawing the View from the root of the ViewTree, which is a DecorView. The performTraversals() method is quite long and the following is only part of the code.

//ViewRootImpl
private void performTraversals(a) {
    finalView host = mView; .int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    intchildHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); . performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); . performLayout(lp, mWidth, mHeight); . performDraw(); }Copy the code

4. Measure phase

Measure is the first stage of the drawing process. In this stage, the size of View is determined by measurement.

4.1 introduce MeasureSpec

  1. A MeasureSpec encapsulates the layout requirements that are passed from a parent View to a child View. A MeasureSpec consists of a size and a mode, which may have three modes.
  2. UNSPECIFIED mode: The parent View imposes no constraints on its child views, which can be of any size it wishes.
  3. EXACTLY mode: The parent View has determined the exact size for the child View, no matter what size the View wants, it needs to be within the parent View’s exact size range.
  4. AT_MOST mode: Within the size range specified by the parent View, the child View can be as large as it wants.

4.2 Relevant methods of View measurement

  1. ViewRootImpl. PerformMeasure () method

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
            if (mView == null) {
                return;
            }
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
            try {
                mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            } finally{ Trace.traceEnd(Trace.TRACE_TAG_VIEW); }}Copy the code

    In performMeasure(), the measure() operation is iterated from the root layout DecorView.

  2. The measure () method

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);
            if(optical ! = isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets();int oWidth  = insets.left + insets.right;
                intoHeight = insets.top + insets.bottom; widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); }...if (forceLayout || needsLayout) {
                // first clears the measured dimension flag
                mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    
                resolveRtlPropertiesIfNeeded();
    
                int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } else {
                    long value = mMeasureCache.valueAt(cacheIndex);
                    // Casting a long to int drops the high 32 bits, no mask needed
                    setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; }... }... }Copy the code

    This method is called to find out how big the View should be. The parent View provides constraint information in the width and height parameters, where widthMeasureSpec is the horizontal space requirement imposed by the parent View and heightMeasureSpec is the vertical space requirement imposed by the parent View. This is a final method, and the actual measurement work is performed by calling the onMeasure() method, so only the onMeasure() method can be overridden by subclasses.

  3. The onMeasure () method

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    Copy the code

    This method measures views and their contents to determine the width and height of the measurement. This method is called by measure() and should be overridden by subclasses to accurately and efficiently measure their contents. When overridden, The setMeasuredDimension() method must be called to store the measured width and height of the View.

  4. The setMeasuredDimension () method

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
            boolean optical = isLayoutModeOptical(this);
            if(optical ! = isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets();int opticalWidth  = insets.left + insets.right;
                int opticalHeight = insets.top  + insets.bottom;
    
                measuredWidth  += optical ? opticalWidth  : -opticalWidth;
                measuredHeight += optical ? opticalHeight : -opticalHeight;
            }
            setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    Copy the code

    The setMeasuredDimension() method must be called by the onMeasure() method to store the measured width and height, and an exception will be raised if the setMeasuredDimension() method fails to execute while measuring.

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
            mMeasuredWidth = measuredWidth;
            mMeasuredHeight = measuredHeight;
    
            mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
    Copy the code

    The setMeasuredDimensionRaw() method is called by the setMeasuredDimension() method to set the measured width and height to the View variables mMeasuredWidth and mMeasuredHeight.

    public static int getDefaultSize(int size, int measureSpec) {
            int result = size;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            }
            return result;
    }
    Copy the code

    Size is the default size of the View, and measureSpec is the parent View’s constraint on the child View. Calculate the size of the View. If measureSpec does not impose the constraint, use the provided size. SpecSize is used for MeasureSpec.AT_MOST or MeasureSpec.EXACTLY mode.

    protected int getSuggestedMinimumWidth(a) {
            return (mBackground == null)? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }Copy the code

    The getSuggestedMinimumWidth() method returns the minimum width that the View should use, and this return value is the larger of the View’s minimum width and the background’s minimum width. When used within the onMeasure() method, the caller should still ensure that the width returned matches the parent View’s requirements.

4.3 Relevant methods of ViewGroup measurement

A ViewGroup is a special View that inherits from a View and can contain other child views. MeasureChildren (), measureChild(), and getChildMeasureSpec() are the most important methods for measuring a View.

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if((child.mViewFlags & VISIBILITY_MASK) ! = GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); }}}Copy the code

The measureChildren() method requires the child views of the View to measure themselves; a child View in the GONE state will not perform the measureChild() method.

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
Copy the code

The measureChild() method requires a child View to measure itself, taking into account both the parent layout’s MeasureSpec requirements and its own padding.

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
Copy the code

This method does the complicated work of measuring the child View, calculating the MeasureSpec passed to a particular child node. The goal is to get the most likely result based on the information from MeasureSpec and the LayoutParams information from the child View.

4.4 DecorView measurement

DecorView inherits FrameLayout, which in turn inherits ViewGroup, overrides the onMeasure() method and calls the parent onMeasure() method in a traversal loop to measure its child View. SetMeasuredDimension () was then called.

//DecorView
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
     final boolean isPortrait = getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
  
     final int widthMode = getMode(widthMeasureSpec);
     final intheightMode = getMode(heightMeasureSpec); .super.onMeasure(widthMeasureSpec, heightMeasureSpec); . }Copy the code
//FrameLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   intcount = getChildCount(); .for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if(mMeasureAllChildren || child.getVisibility() ! = GONE) { measureChildWithMargins(child, widthMeasureSpec,0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if(lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); }}}}... setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); . }Copy the code

5. Layout stage

After the measure stage is completed, it will enter the layout stage and determine the position of the View according to the View measurement results and other parameters.

5.1 performLayout () method

Once the measurement is complete, in the performTraverserals() method, the performLayout() method is executed to begin the layout process.

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
             int desiredWindowHeight) {
  	mScrollMayChange = true;
    mInLayout = true;
    final View host = mView;
    if (host == null) {
        return; }... host.layout(0.0, host.getMeasuredWidth(), host.getMeasuredHeight()); . }Copy the code

5.2 the layout () method

//ViewGroup
@Override
    public final void layout(int l, int t, int r, int b) {
        if(! mSuppressLayout && (mTransition ==null| |! mTransition.isChangingLayout())) {if(mTransition ! =null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true; }}Copy the code

The ViewGroup layout() method, which is final, calls the parent View layout() method inside.

//View
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {...boolean changed = isLayoutModeOptical(mParent) ?
                 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);  
         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
             onLayout(changed, l, t, r, b);
           
             if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); }}else {
                mRoundScrollbarRenderer = null; }... }... }Copy the code

The layout() method of a View assigns size and position to itself and its descendants. Derived classes should not override this method. Derived classes with child views should override onLayout().

5.3 setFrame () method

//View
protected boolean setFrame(int left, int top, int right, int bottom) {
         boolean changed = false; .if(mLeft ! = left || mRight ! = right || mTop ! = top || mBottom ! = bottom) { changed =true;
           
             // Remember our drawn bit
             int drawn = mPrivateFlags & PFLAG_DRAWN;
           
             int oldWidth = mRight - mLeft;
             int oldHeight = mBottom - mTop;
             int newWidth = right - left;
             int newHeight = bottom - top;
             booleansizeChanged = (newWidth ! = oldWidth) || (newHeight ! = oldHeight);// Invalidate our old position
             invalidate(sizeChanged);
 
             mLeft = left;
             mTop = top;
             mRight = right;
             mBottom = bottom;
             mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
           
             mPrivateFlags |= PFLAG_HAS_BOUNDS;
           
           
           	if(sizeChanged) { sizeChange(newWidth, newHeight, oldWidth, oldHeight); }... }return changed;
}
Copy the code

The setFrame() method is called within the Layout () method of the View, which assigns a size and position to the View. If the new size and position are different from the original, the return value is true.

5.4 onLayout () method

//View
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}Copy the code

The View’s onLayout() method is an empty method with no internal code implementation, and a derived class with child nodes should override this method and call Layout on each of its child nodes.

//ViewGroup
@Override
protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);
Copy the code

The ViewGroup onLayout() method is abstract, so classes that directly inherit from the ViewGroup need to override it.

//DecorView
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom); . }Copy the code

5.5 DecorView layout

//DecorView
@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (mApplyFloatingVerticalInsets) {
            offsetTopAndBottom(mFloatingInsets.top);
        }
        if (mApplyFloatingHorizontalInsets) {
            offsetLeftAndRight(mFloatingInsets.left);
        }

        // If the application changed its SystemUI metrics, we might also have to adapt
        // our shadow elevation.
        updateElevation();
        mAllowUpdateElevation = true;

        if(changed && (mResizeMode == RESIZE_MODE_DOCKED_DIVIDER || mDrawLegacyNavigationBarBackground)) { getViewRootImpl().requestInvalidateRootRenderNode(); }}Copy the code

DecorView overrides the onLayout() method and calls the onLayout() method of its FrameLayout parent.

//FrameLayout
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
     final int count = getChildCount();
  
     final int parentLeft = getPaddingLeftWithForeground();
     final int parentRight = right - left - getPaddingRightWithForeground();

     final int parentTop = getPaddingTopWithForeground();
     final int parentBottom = bottom - top - getPaddingBottomWithForeground();
  
     for (int i = 0; i < count; i++) {
         final View child = getChildAt(i);
         if(child.getVisibility() ! = GONE) {final LayoutParams lp = (LayoutParams) child.getLayoutParams();

             final int width = child.getMeasuredWidth();
             final intheight = child.getMeasuredHeight(); . child.layout(childLeft, childTop, childLeft + width, childTop + height); }}}Copy the code

In FrameLayout’s onLayout() method, we call the layoutChildren() method, which opens the loop and lets the child View call Layout() to complete the layout.

6. The draw phase

Once the measurement and layout phases are complete, it is time to move to the drawing phase, where the View is drawn onto the canvas.

6.1 performDraw () method

In the performTraverserals() method, the performDraw() method is executed to begin the drawing process.

private void performDraw(a) {
       if(mAttachInfo.mDisplayState == Display.STATE_OFF && ! mReportNextDraw) {return;
       } else if (mView == null) {
            return; }...booleancanUseAsync = draw(fullRedrawNeeded); . }private boolean draw(boolean fullRedrawNeeded) {
        Surface surface = mSurface;
        if(! surface.isValid()) {return false; }...if(! drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) {return false; }... }private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
  		 // Draw with software renderer.
       finalCanvas canvas; . mView.draw(canvas); . }Copy the code

6.2 the draw () method

//View
@CallSuper
public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        // Step 1, draw the background, if needed
        intsaveCount; drawBackground(canvas); .// Step 3, draw the content
        onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        finalShader fade = scrollabilityCache.shader; . canvas.restoreToCount(saveCount); 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(isShowingLayoutBounds()) { debugDrawFocus(canvas); }}Copy the code

The View’s draw() method, which renders the View and its children to the given canvas, needs to complete the layout before it can be called.

6.3 ontouch () method

//View
protected void onDraw(Canvas canvas) {}Copy the code

View’s onDraw() method is an empty method with no internal code implementation. By overriding this method, we can do our own drawing.

6.4 dispatchDraw () method

//View
protected void dispatchDraw(Canvas canvas) {}Copy the code

The View’s dispatchDraw() method is an empty method that is called by Draw() to draw the child View, which can be overridden by the derived class until the child View of the derived class is drawn, so that the derived class gains control.

//ViewGroup
@Override
protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        finalView[] children = mChildren; .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; }}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); }}... }Copy the code

The ViewGroup dispatchDraw() method overrides the View’s dispatchDraw() method and calls the drawChild() method inside the loop to draw its child View.

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

The drawChild() method draws a child View of the ViewGroup.

6.5 DecorView drawing

//DecorView
@Override
public void draw(Canvas canvas) {
     super.draw(canvas);

     if(mMenuBackground ! =null) { mMenuBackground.draw(canvas); }}Copy the code

DecorView overrides draw() and calls super.draw(canvas) inside it, because neither FrameLayout nor ViewGroup overrides draw(), So super.draw(canvas) calls the View’s draw() method, and after onDraw() calls dispatchDraw() to traverse the drawing subview.

7. To summarize

The drawing process of Android View goes through three stages: Measure, layout and draw. PerformTraversals () in ViewRootImpl is where the drawing process starts. PerformTraversals () includes performMeasure(), performLayout(), and performDraw(). The measurement, layout, and drawing are all performed from the DecorView at the root of the ViewTree. When customizing the View, we override the onMeasure(), onLayout(), and onDraw() methods to add our own business logic to the requirements.