The following code is based on ANDROID SDK 28, also known as ANDROID 9.

View drawing is a commonplace problem, as an Android developer this is the basic necessary knowledge, understand the View drawing process can also be more skilled in mastering the skills of custom controls. I use a little superficial knowledge of the following analysis, if there is wrong, please point out.

The first drawing of view

The view is first drawn in the handleResumeActivity of an activityThread, a very important class that operates on an activity. Let me just say a few words here.

  • First, when the whole system starts up (boot_loader)
  • An init process appears first
  • The init process forks a Zygote process
  • The zygote process forks system_server
  • After system_server is started, many system processes such as AMS,WMS, and PMS are started by this process
  • The process launcher starts and triggers startActivity, which tells system_server through its binder mechanism
  • When the system_server receives a message to start the activity, AMS in system_server tells the Zygote process through the socket to fork out the app process.
  • The app process executes the Main entry for activityThread and initializes ApplicationThread (which inherits the activityThread and implements the IBinder interface) to interact with AMS
  • ApplicationThread uses the Binder mechanism to tell system_server that I want to bind AMS. The System_server receives notification and sends the handleBindApplication request to the app process. And scheduleLaunchActivity requests.
  • After receiving the request, the APP process sends the corresponding Message to the activityThread through the handler and executes the corresponding message. Finally, the corresponding life cycle method onCreate/onResume is completed.

If you don’t know anything about activityThread, remember it. It’s really good to get off topic… Let’s look at what’s going on in the activityThread’s handleResumeActivity.

@Override public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { if (r.window == null && ! A.finished &&willBeVisible) {// Get a window object R.window = R.acty.getwindow (); // getDecorView, top-level view decor = r.window.getdecorview (); // Set decor. SetVisibility (view.invisible) to INVISIBLE. // Get viewManger ViewManager wm = a.getwinDowManager (); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (r.mPreserveWindow) { a.mWindowAdded = true; r.mPreserveWindow = false; // Normally the ViewRoot sets up callbacks with the Activity // in addView->ViewRootImpl#setView. If we are instead Reusing // The decor view we have to notify the view root that the callbacks may have changed. DecorView owners! ViewRootImpl impl = decor.getViewRootImpl(); if (impl ! = null) { impl.notifyChildRebuilt(); } } if (a.mVisibleFromClient) { if (! a.mWindowAdded) { a.mWindowAdded = true; // Added, it added! Here it is, and you can see, it's our viewManager! Added decorView wm. AddView (decor, L); } else { // The activity will get a callback for this {@link LayoutParams} change // earlier. However, at that time the decor will not be set (this is set // in this method), so no action will be taken. This call ensures the // callback occurs with the decor set. a.onWindowAttributesChanged(l);  }}}}Copy the code

You can see that the decorView is added in the handleResumeActivity via WM to AddView, wm is Windows Manager Impl. WindowmangerImpl. AddView – > windowManagerGlobal. AddView – > ViewRootImpl. SetView – > ViewRootImpl. RequestLayout it triggered the first view of the drawing.

This is the first drawing of the view, isn’t it easy… Let’s go back to the first time… As we all know, an activity must be in onCreate setContentview to see the interface. Why? Continue to look at

SetContentView source code analysis

After the activity is started, setContentView is used in onCreate.

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
Copy the code

GetWindow gets Window, window is an abstract class that only implements PhoneWindow. Go to phoneWindow and see how setContentView is done;

Public void setContentView(int layoutResID) {if (this.mContentParent == null) {// Determine if decorView is null, if null, create one; this.installDecor(); } else if (! This.hasfeature (FEATURE_CONTENT_TRANSITIONS)) {FEATURE_CONTENT_TRANSITIONS: animation properties of FEATURE_CONTENT_TRANSITIONS; this.mContentParent.removeAllViews(); }... }Copy the code

Focus on the line mlayOutinflater.inflate (layoutResID, this.mContentparent); Yes, it is a common way to load XML. By reading the source code, you can conclude:

  • 1. If root is null, attachRoot is meaningless.
  • 2. Root is not null and attachRoot is true to add a parent layout, root.
  • 3. Root is not null and attachRoot is false. The outermost layout properties are set to take effect when added to the parent view.
  • 4. Root is not null. If no attachRoot attribute is set, the default value is true.

How are Layoutinflaters loaded? Here’s a quick overview.

public View inflate(XmlPullParser parser, ViewGroup root, Boolean attachToRoot) {synchronized(this.mconstructorargs) {Context inflaterContext = this.mcontext; AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context)this.mConstructorArgs[0]; this.mConstructorArgs[0] = inflaterContext; Object result = root; try { InflateException ie; try { int type; while((type = parser.next()) ! = XmlPullParser.START_TAG && type ! = xmlpullParser. END_DOCUMENT) {// If the start and end tags are not processed} if (type! = XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!" ); } String name = parser.getName(); / / to deal with the merge tag if first (" merge ". The equals (name)) {if (root = = null | |! AttachToRoot) {// Root is empty or not attached to root. So the merge tag can only make throw new InflateException InflateException("<merge /> can be used only with a valid viewgroup and attachroot is true ViewGroup root and attachToRoot=true"); } // you can see the XML tag merge, and parent is root this.rinflate (parser, root, inflaterContext, attrs, false); } else {// Create a view with a tag view temp = this.createViewFromTag(root, name, inflaterContext, attrs); LayoutParams params = null; if (root ! Mr = null) {/ / into params params = root. GenerateLayoutParams (attrs); if (! attachToRoot) { temp.setLayoutParams(params); } } this.rInflateChildren(parser, temp, attrs, true); if (root ! = null && attachToRoot) {// addView root.addView(temp, params); } if (root == null || ! attachToRoot) { result = temp; } } } catch (XmlPullParserException var19) { ie = new InflateException(var19.getMessage(), var19); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } catch (Exception var20) { ie = new InflateException(parser.getPositionDescription() + ": " + var20.getMessage(), var20); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } } finally { this.mConstructorArgs[0] = lastContext; this.mConstructorArgs[1] = null; Trace.traceEnd(8L); } return (View)result; }}Copy the code

View temp = this.createViewFromTag(root, name, inflaterContext, attrs); How do you create a view from a tag?

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { ... try { View view; if (mFactory2 ! = null) { view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory ! = null) { view = mFactory.onCreateView(name, context, attrs); } else { view = null; } if (view == null && mPrivateFactory ! = null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); } if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; Try {if (-1 == name.indexof ('.')) {view = onCreateView(parent, name, attrs); } else {createView = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; . } protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { CreateView (name, "android.view.", attrs); } public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { Constructor<? extends View> constructor = sConstructorMap.get(name); if (constructor ! = null && ! verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class<? extends View> clazz = null; try { if (constructor == null) { // Class not found in the cache, see if it's real, Clazz = McOntext.getclassloader ().loadClass(prefix! = null ? (prefix + name) : name).asSubclass(View.class); if (mFilter ! = null && clazz ! = null) { boolean allowed = mFilter.onLoadClass(clazz); if (! allowed) { failNotAllowed(name, prefix, attrs); Constructor = clazz.getconstructor (mConstructorSignature); constructor.setAccessible(true); Sconstructormap. put(name, constructor); } else { ... } Object lastContext = mConstructorArgs[0]; if (mConstructorArgs[0] == null) { // Fill in the context if not already within inflation. mConstructorArgs[0] = mContext; } Object[] args = mConstructorArgs; args[1] = attrs; // Use constructor to create view final View view = constructive.newinstance (args); If (view instanceof ViewStub) {// Process ViewStub // Use the same context when inflating ViewStub later.final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(cloneInContext((Context) args[0])); } mConstructorArgs[0] = lastContext; return view; } catch (Exception e) {} finally {}}Copy the code

I got rid of a little bit of code, but notice the three factories, they call onCreateView, and they create the view. Ok, it is relatively clear. Now the question is what are these three factories and when were they created? These three factories have been set in the Activity. Attach.

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);
        mFragments.attachHost(null /*parent*/);
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
            }
Copy the code

SetPrivateFactory is invisible and cannot be called externally. The other two external users can be called. And factory2 is the createView method of the called fragment. So factory and factory2 are the methods given by the system hook. If neither of them is set, the default view creation process will be followed. The default view creation process, ultimately through reflection, is to get the class constructor and then create a new view.

This is how setContentView loads a view, so let’s briefly summarize the process from activity creation to view creation:

  • 1. After the app starts, execute the activity’s onCreate method, which setContentView;
  • 2. Execute the Inflate method of LayoutInfalter to load the layout by calling the PhoneWindow setContentView;
  • 3. In the inflate method, parse the XML with XmlPullParser;
  • 4. During the parsing process, you can manually set the factory, if not set, go to the default process of creating a view, that is, through the reflection call class construction to create a new view.

View drawing process

The viewRootImpl calls requestLayout to trigger the first drawing of the View. So let’s keep looking at it from here.

public void requestLayout() { if (! MHandlingLayoutInLayoutRequest) {/ / check whether in the main thread checkThread (); mLayoutRequested = true; scheduleTraversals(); }}Copy the code

Ok, this method checks the thread first, sets the variable to true, and then performs scheduleTraversals

 void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
    
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
            performTraversals();
            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }
Copy the code

Ok. At last performTraversals. Traversals -> performMeasure -> performLayout -> performDraw performMeasure Call measure if necessary, same with the other two methods.

View measurement -onMeasure

You iterate from the top down. The measure method takes two main parameters widthMeasureSpec and heightMeasureSpec. These two values are computed by the superview and passed to the child, and these two parameters are obtained by getRoot method in view wroot. The root graph is always full of global values. Here’s measure’s core code:

/ / final method, Public final void measure(int widthMeasureSpec, int heightMeasureSpec) { // Suppress sign extension for the low bytes long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; // Optimize layout by avoiding an extra EXACTLY pass when the view is // already measured as the correct size. In API 23  and below, this // extra pass is required to make LinearLayout re-distribute weight. final boolean specChanged = widthMeasureSpec ! = mOldWidthMeasureSpec || heightMeasureSpec ! = mOldHeightMeasureSpec; final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY; final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec) && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec); final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || ! isSpecExactly || ! matchesSpecSize); 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; } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; }}Copy the code

The measure() method is used to measure how big a view is. Its actual measurement is done in onMeasure. Its two parameters widthMeasureSpec and heightMeasureSpec are passed in from the parent view. For child views, the size is determined by both parent and child views. Two important parameters:

MeasureSpec

MeasureSpec, which represents the width and height requirements, is a 32-bit parameter of type INT. The first two represent mode and the last 30 represent size. There are three modes:

  • UNSPECIFIED does not specify the size;
  • B: EXACTLY.
  • AT_MOST Maximum mode;

MakeMeasureSpec encapsulates mode and size into a MeasureSpec of int;

LayoutParams

LayoutParams are used by the view to tell the parent layout how it wants to be wrapped.

  • MATCH_PARENT: This view wants to be the same size as the PARENT layout.
  • WRAP_CONTENT: The view wants to wrap its content.

Ok. Moving on to onMeasure.

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

The onMeasure method is simply a call to setMeasuredDimension(). This method actually ends up assigning the width and height values to the global variables and saves them, so once called it means that the view is finished measuring. Note the statement in the comment that setMeasureDimension must be overridden or measure will throw an exception. Continue to see getDefaultSize ().

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

If specMode is AT_MOST and EXACTLY, then the size is specSize. The default specSize is passed in by the parent layout.

protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }
Copy the code

The minimum recommended width and height is determined by the bg of the view and the size of the Settings.

The measurement of ViewGroup

ViewGroup measurements from MeasureChilderen actual internal is the recursive call MeasureChild (amount of contingency = = = = attribute is gone), let’s see MeasureChild this method directly:

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 parent layout’s MeasureSpec and the child view’s width and height Params, etc., then call getChildMeasureSpec to adjust the child view’s MeasureSpec, and finally call the child view’s measure method to process. How is getChildMeasureSpec adjusted

public static int getChildMeasureSpec(int spec, int padding, Int childDimension) {// Get the mode of the current parent view and size int specMode = MeasureSpec. int specSize = MeasureSpec.getSize(spec); // Get the difference between parent size and padding, which is the remaining size of parent. 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) {resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == layoutparams.wrap_content) {childDimension == layoutparams.wrap_content; resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; . //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }Copy the code

(childDimension is the width and height, padding is the border size) If childDimension is found to be a specific value >=0 then set it to exactly, if it is match_parent, resultmode is set to exactly; If it is wrap_content, set it to AT_MOST; Ok. So that’s the viewGroup measurement. The view size can be obtained using the View’s getMeasureWidth/Height, but must be obtained after the onMeasure process ends.

View layout onLayout

Since layout is also recursive, let’s look directly at the ViewGroup layout method:

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

It is also final, and subclasses cannot be overridden by inheritance. Call layout of the parent class, which is the View layout method.

public void layout(int l, int t, int r, int b) { ... onLayout(changed, l, t, r, b); . }Copy the code

OnLayout is called. ViewGroup onLayout is an abstract method! That is, to create a subclass of ViewGroup, you must override the onLayout method.

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

The view’s onLayout method is empty… It ‘s fine. Let’s look at a casual example of how frameLayout is implemented:

@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) {
...
        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 int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
Copy the code

The display position of the child view in the parent view will be arranged by referring to the width and height calculated in the measure process. The measureWidth and measureHeight of each view are measured by the measureWidth and measureHeight of each view. After the layout operation is completed, the left and right equivalents of each view position are obtained.

View draw analysis

Viewgroup does not override the draw method, so view draw is also directly analyzed here

public void draw(Canvas canvas) { ... /* * 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

The comments are very clear, first draw the background, save the layer if necessary, then draw the content onDraw, pass the draw event, draw some other widgets and so on. (Step 2 and step 5 can be ignored.) OK. Let’s focus on the remaining four steps:

  • 1. Draw drawBackground for the view background
  • 2. Draw the content of the view, that is, onDraw, this is different for each control, view inside the onDraw is an empty implementation.
  • 3. DispatchView. That’s drawing all the children of the current view. It is an empty implementation and needs to be overridden by the control itself. For example, a FrameLayout control that inherits the ViewGroup will override the method and drawChild the method.
  • 4. Draw the scroll bar of the View.

Okay, so that’s the drawing flow of the View.