preface

The previous several analyzed the process of Measure, Layout and Draw, which will be called when the View is displayed for the first time. What if you changed the properties of the View after that? For example, change the color, change the text content, change the picture, etc., will still go through these three processes? Follow this path to analyze the Invalidate/RequestLayout process. Through this article, you will learn:

1, Invalidate process 2, RequestLayout process 3, Invalidate/RequestLayout process 4, child thread is not able to draw UI 5, postInvalidate process

Invalidate the process

A small Demo

public class MyView extends View { private Paint paint; private @ColorInt int color = Color.RED; public MyView(Context context) { super(context); init(); } 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) {// Canvas. DrawColor (color); } public void setColor(@ColorInt int color) { this.color = color; invalidate(); }}Copy the code

MyView displays a red rectangle by default, which is exposed to the outside world using setColor to change the color of the drawing. After the color change, you need to re-execute onDraw(xx) to see the effect of the change, and trigger the onDraw(xx) call with the invalidate() method. Let’s look at how the invalidate() method triggers the onDraw(xx) method to execute.

Invalidate () call stack

Invalidate does what the name suggests: to invalidate something. In this case, invalidates the current drawing and needs to be redrawn. Of course, it’s often simply called refresh. Invalidate () is the method in view.java.

#View.java public void invalidate() { invalidate(true); } public void invalidate(Boolean invalidateCache) {//invalidateCache invalidateInternal(0, 0, mright-mleft, mBottom - mTop, invalidateCache, true); } void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) { ... If (skipInvalidate()) {return; } //PFLAG_DRAWN indicates that the View has been drawn previously. PFLAG_HAS_BOUNDS indicates that the View has been laid out, Determine the coordinates of the if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) = = (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) | | (invalidateCache &&  (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) || (mPrivateFlags & PFLAG_INVALIDATED) ! = PFLAG_INVALIDATED || (fullInvalidate && isOpaque() ! = mLastIsOpaque)) {if (fullInvalidate) {// Default true mLastIsOpaque = isOpaque(); // Clear the draw flag mPrivateFlags &= ~PFLAG_DRAWN; } / / need to draw mPrivateFlags | = PFLAG_DIRTY; If (invalidateCache) {/ / 1, plus the failure tag/draw / 2, remove mapped cache tag/effective/branch used both markers in hardware accelerated rendering mPrivateFlags | = PFLAG_INVALIDATED; mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; } final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; if (p ! = null && ai ! = null && l < r && t < b) { final Rect damage = ai.mTmpInvalRect; Damge (l, t, r, b); damge (l, t, r, b); // Call invalidateChild p.invalidatechild (this, damage); // Call invalidateChild p.invalidatechild (this, damage); }... }}Copy the code

The parent layout’s invalidateChild(xx) method is called when the View to be refreshed determines the refresh area. This method is final in the ViewGroup.

#ViewGroup.java public final void invalidateChild(View child, final Rect dirty) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo ! = null && attachInfo. MHardwareAccelerated) {/ / 1, if it is support hardware acceleration, walked the branch onDescendantInvalidated (child, child); return; } //2, ViewParent parent = this; if (attachInfo ! = null) {// Animation related, ignore... do { View view = null; if (parent instanceof View) { view = (View) parent; }... parent = parent.invalidateChildInParent(location, dirty); } while (parent! = null); }}Copy the code

As can be seen from the above, hardware-accelerated rendering and software-accelerated rendering are distinguished in this method. Let’s take a look at the differences between the two:

If the Window supports hardware acceleration, go through the following process:

#ViewGroup.java public void onDescendantInvalidated(@NonNull View child, @NonNull View target) { mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION); if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) ! = 0) {/ / here will go mPrivateFlags = (mPrivateFlags & ~ PFLAG_DIRTY_MASK) | PFLAG_DIRTY; // Clear the draw cache valid flag mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; } the if (mLayerType = = LAYER_TYPE_SOFTWARE) {/ / if it is opened the drawing software, is combined with paint failure tag mPrivateFlags | = PFLAG_INVALIDATED | PFLAG_DIRTY; // Change target to target = this; } if (mParent ! = null) {/ / call the parent layout onDescendantInvalidated mParent. OnDescendantInvalidated (this, target); }}Copy the code

The purpose of the onDescendantInvalidated method is to continually look up the parent layout and empty the parent PFLAG_DRAWING_CACHE_VALID flag, i.e. draw cache empties. Descendantin appropriated () method: “SRC: descendantinappropriated ()”; “SRC: descendantinappropriated ()”;

#ViewRootImpl.java @Override public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) { // TODO: Re-enable after camera is fixed or consider targetSdk checking this // checkThread(); if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) ! = 0) { mIsAnimating = true; } invalidate(); } @unsupportedappusage void invalidate() {// msupportedappusage void invalidate() { MHeight = Window size mDirty. Set (0, 0, mWidth, mHeight); if (! MWillDrawSoon) {// Enable the View traversals (); }}Copy the code

Make a summary:

Invalidate () for hardware acceleration support, the purpose is to find views that need to be redrawn. The current View definitely needs to be redrawn, so continue recursively looking for its parent layout until you get to the root View. 2. If the View needs to be redrawn, add the PFLAG_INVALIDATED mark. 3. Set the redraw area.

Graphing the hardware-accelerated invaldiate process:

If the Window does not support hardware acceleration, use software to draw branches: Parent. InvalidateChildInParent (location, dirty, returns to mParent, as long as mParent not null then call invalidateChildInParent (xx), This is actually traversing the ViewTree process, so let’s look at the key invalidateChildInParent(xx):

# viewgroup. Java public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) { Also is the area need to redraw the if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID))! = 0) {/ / the View map or drawing cache effectively the if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))! // start (CHILD_LEFT_INDEX); // start (CHILD_LEFT_INDEX); location[CHILD_TOP_INDEX] - mScrollY); If ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {// If (mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {// If (mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {// If (mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {// If (mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {// If (mGroupFlags & FLAG_CLIP_CHILDREN) == 0) { mBottom - mTop); } final int left = mLeft; final int top = mTop; If ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {if (! dirty.intersect(0, 0, mRight - left, mBottom - top)) { dirty.setEmpty(); }} location[CHILD_LEFT_INDEX] = left;}} location[CHILD_LEFT_INDEX] = left; location[CHILD_TOP_INDEX] = top; } else { ... } // mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; if (mLayerType ! = LAYER_TYPE_NONE) {/ / if set the cache type, marking the View needs to redraw mPrivateFlags | = PFLAG_INVALIDATED; } // return mParent; } return null; }Copy the code

In line with hardware-accelerated drawing, finally call ViewRootImpl invalidateChildInParent(xx) to see the implementation:

#ViewRootImpl.java public ViewParent invalidateChildInParent(int[] location, Rect dirty) { checkThread(); if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty); If (dirty == null) {// If the dirty area is empty, invalidate() will be refreshed by default; return null; } else if (dirty.isEmpty() && ! mIsAnimating) { return null; }... invalidateRectOnScreen(dirty); return null; } private void invalidateRectOnScreen(Rect dirty) { final Rect localDirty = mDirty; Localdir. union(dirty. Left, dirty. Top, dirty. Right, dirty. Bottom); . if (! MWillDrawSoon && (intersected | | mIsAnimating)) {/ / open the View of three drawing process scheduleTraversals (); }}Copy the code

Make a summary:

1. Invalidate () For software drawing, the purpose is to find areas that need to be redrawn. 2. Determine the location of the redrawn area in the Window. This area needs to be redrawn.

The invalidate process is plotted by the software:

The difference of invalidate between hardware-accelerated drawing and software drawing is analyzed above. The ultimate purpose of both is to redraw the Draw process. The replay Draw process is triggered by calling scheduleTraversals() to see how this is done.

To learn more about hardware accelerated drawing, please go to: Android custom View Draw process (middle)

Triggering the Draw process scheduleTraversals is analyzed in detail in this article: Android Activity creation to View display process

The three main traversals are actually enabled in viewrotimpl ->performTraversals(), in which Measure, Layout, and Draw are performed according to certain conditions. This article focuses on how to trigger the Draw process.

#ViewRootImpl.java private void performDraw() { ... // call draw method Boolean canUseAsync = draw(fullRedrawNeeded); . } finally { mIsDrawing = false; }... } private Boolean draw(Boolean fullRedrawNeeded) {//mSurface create Surface when ViewRootImpl = mSurface; if (! surface.isValid()) { return false; }... if (! Dirty. IsEmpty () | | mIsAnimating | | accessibilityFocusDirty) {/ / invalidate, dirty was the assignment / / to meet one of the conditions, Focus on the first condition if (mAttachInfo mThreadedRenderer! = null && mAttachInfo.mThreadedRenderer.isEnabled()) { ... / / hardware accelerated rendering mAttachInfo. MThreadedRenderer. The draw (mView, mAttachInfo, this); } else { ... // Draw if (! drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) { return false; }}}... return useAsyncReport; } private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty, Rect surfaceInsets) { final Canvas canvas; Canvas = msurface.lockCanvas (dirty); canvas = msurface.lockCanvas (dirty); } catch (Surface.OutOfResourcesException e) { return false; } catch (IllegalArgumentException e) { return false; } finally {} try {//ViewTree start drawing mview.draw (canvas); } finally { .. } return true; }Copy the code

As you can see, invalidate finally triggers the Draw process.

1, whether hardware accelerated drawing or software drawing, will set the redrawn rectangle area. For hardware-accelerated drawing, the redrawn area is the size of the entire Window. For software drawing it is to set the intersecting rectangular regions. 2, as long as the redraw area is not empty, then when the three processes open, Draw process must be called. 3. For hardware-accelerated drawing, the View that needs to be redrawn is controlled by drawing markers, so when we call view.invalidate(), the View is set to redraw markers, and the View Draw (xx) is called during the Draw process. Of course, if the parent layout has a software cache, then the parent layout needs to be redrawn, and the children of the parent layout need to be redrawn. 4. For software drawing, the entire ViewTree Draw process will be called, but Canvas only draws the rectangle specified by the redraw area.

As you can see, enabling hardware-accelerated drawing avoids unnecessary drawing. For details on the difference between hardware-accelerated drawing and software drawing, please read the series of articles: Android custom View Draw process (part 1)

Finally, the invalidate process is shown in a diagram:

RequestLayout process

As the name implies, rerequest the layout. Take a look at the view.requestLayout () method:

# view.java public void requestLayout() {if (mMeasureCache! = null) mMeasureCache.clear(); . / / add mandatory layout tag, the tag triggers layout mPrivateFlags | = PFLAG_FORCE_LAYOUT; / / added to paint marker mPrivateFlags | = PFLAG_INVALIDATED; if (mParent ! = null && ! MParent. IsLayoutRequested ()) {/ / if the last layout request has been completed/layout/father continue to call requestLayout mParent. RequestLayout (). }... }Copy the code

As you can see, this recursive call follows the same pattern as invalidate, looking up its parent layout all the way to ViewRootImpl, and setting the PFLAG_FORCE_LAYOUT and PFLAG_INVALIDATED flags for each layout. Check the ViewRootImpl requestLayout ()

# viewrootimpl.java public void requestLayout() {if (! MHandlingLayoutInLayoutRequest) {/ / check whether the thread is consistent checkThread (); // Flag a layout request mLayoutRequested = true; // Enable View traversals (); }}Copy the code

RequestLayout clearly has a simple purpose:

1. Look up the parent layout and set the mandatory layout tag. 2

In the same formula as invalidate, doTraversal()->performTraversals() is called when the refresh signal comes, and performTraversals() actually performs the three traversals.

# viewrootimpl.java private void performTraversals() {//mLayoutRequested when in requestLayout specifies true Boolean layoutRequested = mLayoutRequested && (! mStopped || mReportNextDraw); If (layoutRequested) {/ / measure process windowSizeMayChange | = measureHierarchy (host, lp, res, desiredWindowWidth, desiredWindowHeight); }... final boolean didLayout = layoutRequested && (! mStopped || mReportNextDraw); If (didLayout) {// Layout procedure performLayout(lp, mWidth, mHeight); }... }Copy the code

Thus it can be seen that:

RequestLayout will eventually trigger the Measure/Layout process. 2. Draw process will not trigger because no redraw area is set.

What is the use of the PFLAG_FORCE_LAYOUT flag set earlier? Recall the measure process:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... / / requestLayout, Final Boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; . if (forceLayout || needsLayout) { ... int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); If (cacheIndex < 0 | | sIgnoreMeasureCache) {/ / measuring onMeasure (widthMeasureSpec, heightMeasureSpec); } else { ... }... }}}Copy the code

The PFLAG_FORCE_LAYOUT flag triggers onMeasure() to measure itself and its child layouts.

If the size of the View is changed and the View size is larger, then the requestLayout is reconfigured because the Measure and Layout processes are used, but the result will not be achieved without calling Draw. In fact, the View layout is already taken into account. In the View. The layout (xx) – > setFrame (xx)

#View.java 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; . int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth ! = oldWidth) || (newHeight ! = oldHeight); // Size changes call invalidate and pass true, otherwise pass false invalidate(sizeChanged); . }... return changed; }Copy the code

In other words:

1. Invalidate may be triggered after requestLayout is called. 2. If invalidate() is triggered, the process will be redrawn regardless of whether true or false is passed in.

For more in-depth analysis of measure and layout, please go to: Measure Process of Android Custom View Procedure Of Android Custom View Procedure

Diagram the requestLayout process:

Invalidate/RequestLayout usage scenarios

Combined with requestLayout and Invalidate and View three processes, as shown below:

1. The invalidate call will only trigger the Draw process. RequestLayout triggers the Measure, Layout process and calls invalidate if the size changes. 3. Use requestLayout when it comes to View size and position changes. 4. Call invalidate when only a redraw is required. 5, If you are not sure whether requestLayout triggers invalidate, you can continue to call invalidate after requestLayout.

If the parent layout calls Invalidate, will the child layout go through the redraw process? Let’s list these relationships.

Child layout/parent layout Invalidate/RequestLayout relationship

Invalidate child layout If software drawing or parent layout has software cache drawing enabled, the parent layout will go through the redraw process (provided the WILL_NOT_DRAW flag is not set).

The child RequestLayout parent resets the Measure and Layout processes.

If the parent layout Invalidate is drawn by software, the child layout will go through the redrawing process.

Parent RequestLayout If the parent Layout size changes, the child Layout Measure process, Layout process is triggered.

Is it true that child threads can’t draw UI

Create a child thread in the Activity onCreate and display the dialog:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_group);

        new Thread(new Runnable() {
            @Override
            public void run() {
                TextView textView = new TextView(MainActivity.this);
                textView.setText("hello thread");
                Looper.prepare();
                Dialog dialog = new Dialog(MainActivity.this);
                dialog.setContentView(textView);
                dialog.show();
                Looper.loop();
            }
        }).start();
    }
Copy the code

The answer is yes, and here’s why.

RequestLayout () = checkThread(); requestLayout () = checkThread();

Void checkThread() {// If (mThread! = thread.currentThread ()) { Only created ViewTree thread to operate inside the View throw new CalledFromWrongThreadException (" Only the the original thread that created the View a hierarchy can touch its views."); }}Copy the code

The question is what is an mThread? From where?

#ViewRootImpl.java public ViewRootImpl(Context context, Display display) { ... MThread = thread.currentThread (); mThread = thread.currentThread (); . }Copy the code

The ViewRootImpl object is created during a call to WindowManager.addView(xx). For WindowManager/ Windows, go: Windows /WindowManager is a must-know

Now it’s clear to look back at Dialog creation:

Dialog.show () calls WindowManager.addView(xx), so the ViewRootImpl object is called in the child thread, and mThread points to the child thread. When the ViewRootImpl object is successfully built, call its setView(xx) method, which calls requestLayout, which is still a child thread. CheckThread () does not throw an exception.

In fact, “child threads cannot update the UI” is a more reasonable statement: The View can only be manipulated by the thread that built the ViewTree. But generally, the thread in which an Activity builds the ViewTree is called the UI (main) thread, hence the term.

PostInvalidate process

Invalidate () can only be called from the main thread (not checkThread() in hardware-accelerated conditions), so what if you want to call from a child thread? If you want to switch to the main thread via Handler and then execute invalidate(), it’s a bit redundant.

#View.java public void postInvalidate() { postInvalidateDelayed(0); } public void postInvalidateDelayed(long delayMilliseconds) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo ! = null) {/ / or an ViewRootImpl attachInfo. MViewRootImpl. DispatchInvalidateDelayed (this, delayMilliseconds); }}Copy the code

Cut to ViewRootImpl. Java

#ViewRootImpl.java public void dispatchInvalidateDelayed(View view, Message.obj = view Message MSG = mhandler. obtainMessage(MSG_INVALIDATE, view); mHandler.sendMessageDelayed(msg, delayMilliseconds); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_INVALIDATE: //obj is the View ((View) msg.obj).invalidate(); break; . }}Copy the code

Discovered the truth:

PostInvalidate is switched to the UI thread by a handler in ViewRootImpl, and finally invalidate() is executed. The hanlder bound thread in ViewRootImpl is the UI thread.

This article is based on Android 10.0

If you like, please like/follow, your encouragement is my motivation to move forward.