Interviewer: why do you customize the wrap of view_ Will content fail?

preface

Interviewer: why wrap in custom View_ Will content fail?

To answer this question, we need to understand the past and present lives drawn by view

When is the view drawn?

In which life cycle of the Activity is the view drawn?

After onResume

Get to know viewrootimpl

We know that the onResume method is actually just a callback method. The previous call is

handleResumeActivity -> performResumeActivity -> onResume

After onResume, it will return to handleResumeActivity, and then execute addView to add the visible view to the window, where the window acts as a display,

  public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
    		//Call onResume
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);

      ViewManager wm = a.getWindowManager();
      if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l); // Add decor to wm
                }
      }
    ...
    //  wm is a WindowManagerGlobal that holds viewrootimpl
    public void addView(View view, ViewGroup.LayoutParams params,
                          Display display, Window parentWindow, int userId) {

			root.setView(view, wparams, panelParentView, userId);// ViewRoootImpl
    }

The root here is ViewRootImpl. After entering setView, it is the work of ViewRootImpl. ViewRootImpl is the tool for adding views in window

The role of ViewRootImpl in drawing views

setView has a lot of code, mainly calling requestLayout()

requestLayout does two things

  1. Check whether the current thread is the thread creating the View. If not, throw an exception

        void checkThread() {
            if (mThread != Thread.currentThread()) {
                throw new CalledFromWrongThreadException(
                        "Only the original thread that created a view hierarchy can touch its views.");
            }
        }
    
  2. Execute scheduleTraversals

    mTraversalBarrier means inserting a synchronization barrier into the handler, which means that the next task to be sent to the handler is the highest priority and needs to be processed immediately (screen refresh often needs to be the highest priority)

    M choreographer is a thread that will execute the drawing task. The drawing task is in the incoming mTraversalRunnable

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
          	// Note the mTraversalRunnable passed in
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();  
        }
    }
    

Perform specific drawing

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

    void doTraversal() {
        if (mTraversalScheduled) {
						...
            performTraversals();
						...
        }
    }
    private void performTraversals() {}

It will be executed to performTraversals. There are many calls to this method, which can be directly to ViewGroup and View, as shown below (the picture comes from the network)

performTraversals will call performMeasure, performLayout and performDraw respectively

These three methods, I think you can guess, will start the onMesure, onLayout and onDraw methods

Summary

Back to our question

When is the view drawn? viewRootImpl does it all after onResume, and the end point is the three drawing methods of the view

Therefore, the Activity before onResume cannot obtain the width and height of the view, because the width and height of the view are finally determined in onLayout

But in onCreate, you can get it through the View.post() method. Why?

In fact, it's also very simple. It's nothing more than blocking it and putting it in the queue. When it's drawn and notified, we can get the width and height. Let's take a look at the source code

    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
      	//If it is not null here, it means that the view has been added to the window and the drawing has already been completed
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }

        //If it is null, it means that the view is not ready and put it in the queue
        getRunQueue().post(action);
        return true;
    }

Do you think he can predict the future? In fact, he is either bragging or waiting until the future has happened

Next, we discuss the specific rendering process of view

Drawing process of View

If you don't know about View, you don't really get started with android

Both TextView gizmos and large containers such as LineLayout are evolved from View, and TextView also inherits from View

public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {}

Layout controls such as LineLayout are special in that they come from ViewGroup, which inherits from View

public class RelativeLayout extends ViewGroup {}
public abstract class ViewGroup extends View implements ViewParent, ViewManager {}

View can be compared to water. A lot of water together is a view group, but it is still water in essence

In addition to display, View must have perfect sliding and clicking strategies, which is the most frequent operation on the mobile phone. Next, we will learn more about the display, sliding, event and drawing principles of View.

Display method

If you want to display and know which control is placed, you need to accurately locate it. Here we use the coordinate system. There are two kinds

  • android coordinate absolute positioning

    The simplest is to take the upper left corner as the coordinate origin, the right side is the positive direction of the x axis, and the lower side is the positive direction of the y axis

    Use getRawX() and getRawY() methods to obtain X and Y coordinates, which is an absolute positioning method

  • view coordinate relative positioning

    Because the space in android is nested layer by layer, a child control can see its position through its relative position to the parent control. The specific method is as follows (figure from the network)

Commonly used, such as obtaining the width and height of the view,

width = getRight() - getLeft();
height = getBottom() - getTop ();

Of course, the system already has getWidth and getHeight methods, and their internal logic is the same

/**
 * Return the width of your view.
 *
 * @return The width of your view, in pixels.
 */
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
    return mRight - mLeft;
}

Sliding event

In terms of sliding, like the UI written in other languages, android records the Down coordinate when clicking, and then records the UP coordinate after the finger slides, calculates the offset, and modifies the View coordinate through the offset. When the finger slides on the mobile phone, onTouchEvent event will be triggered. If you want to customize the operation, you can override this method

public boolean onTouchEvent(MotionEvent event) {
switch (action) {
         case MotionEvent.ACTION_UP:
         case MotionEvent.ACTION_DOWN:
         case MotionEvent.ACTION_CANCEL://?
         case MotionEvent.ACTION_MOVE:
}

The other three are easy to understand. What is the situation of ACTION_CANCEL?

For example, if you slide a View in a LineLayout, but it slides to an area outside the LineLayout, the View cannot go out. At this time, ACTION_CANCEL can be triggered. You can set it back to its original position or leave the View at the edge

There is also an onInterceptTouchEvent method in the View Group, coupled with various nested views in android, which is also confusing to many people. Here is the issue of event consumption.

event processing

Why is there this problem?

Just imagine, there must be a palm sized place on the mobile phone everywhere

When I click the TextView in the blue area, I actually click RelativeLayout and LinearLayout

So, how does android know which control to click?

The method is event interception. Clicking is an event. Which layer intercepts this event and executes the corresponding logic is an event consumption. If it is intercepted and does not execute the logic, it will be released and intercepted by other controls and recurs in turn

The above bold interception and execution correspond to the two methods onintercerttouchevent and onTouchEvent in the view

Obviously, the first intercepted view is the outermost view, LinearLayout

  • onIntercerptTouchEvent

    If you override the onintercerttouchevent method on the outer LinearLayout, the return value is false, which means that it releases the event and enters the internal RelativeLayout. Similarly, the onintercerttouchevent method of which control returns true, which control wants to intercept the event.

    Note that in order to be efficient, android intercepts only down incoming events (refer to different case s of onTouchEvent above). After confirming that the return value of onintercerttouchevent is true, the move, up, etc. of the intercepted event will be executed directly with down to the onTouchEvent of the current control

    If the return value is false, it means that the current control releases this event, then move and down will stay in onintercerttouchevent of the current control and pass in the next intercepting control together.

    Note that onintercerttouchevent is only available in View Group. The reason is very simple, because only it can nest views, and the default return value is false. Generally, ViewGoup does not easily handle events, but gives them to child views, which is also in line with our intuitive feeling of its "container".

  • onTouchEvent

    Suppose it is finally transmitted to TextView, and he has no way to transmit it. Does he have to consume this event?

    No, his onTouchEvent also has a return value. return false indicates that he is unwilling to consume this event. In this way, the packaged events (down, up, move, etc.) will be returned to RelativeLaout

    As shown in the figure

In addition, there is also a dispatchTouchEvent() method, which is responsible for distributing events. Here alone, it is convenient for everyone to disassemble and understand. It is simpler. Although the above logic is closed-loop, one is still missing. How can the control be intercepted when the user clicks the control?

The dispatchTouchEvent contains an ontuchlistener, which is placed in the activity or framework. First, get the event, give it to the dispatchTouchEvent for unified management, and then distribute it to the onintercerttouchevent of the corresponding ViewGroup. You can follow the above logic. Since this article is more about understanding the principle, it does not do specific implementation.

draw

The above explanations serve this process and are scattered knowledge points. Next, we will string them together

The workflow of View is to measure, layout and draw through three methods respectively. If you want to customize the View, you need to rewrite it

measure

The measure in the View must measure the View itself, but if the View is a ViewGroup, it will traverse the views inside and call their own measure to measure themselves. This is a recursive process

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   		// Note the widthMeasureSpec and getDefaultSize here
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
 }

There is only one line of source code here, that is, get the default width and height and measure it

I believe you must have used wrap_content is to make the control size just wrap the content. If you set the width and height as a fixed value in xml, you don't need measure, just because we set wrap_content or match_parent, the onMeasure() method of view will be called

  • match_parent

    For match_parent, you only need to know the parent control of the current View and assign its Size to the current View, so we need to do two things: 1. Find the original ViewGroup control measurement, 2. Pass the measurement data down to the smallest View

  • wrap_content

    wrap_content is the minimum value that just wraps the internal content, so on the contrary, it is to calculate the size of the child control

What is the role of widthMeasureSpec?

How does the View Group pass information to child views?

MeasureSpec class, which holds two data

  1. The specific size of the parent control of the child View
  2. The restriction type of the parent control on the child View

The first one is easy to understand. After all, match_ The parent is passed by this, and the child view cannot exceed this size

There are three types of restrictions for the second

private static final int MODE_SHIFT = 30;
public static final int UNSPECIFIED = 0 << MODE_SHIFT; //Unlimited size
public static final int EXACTLY     = 1 << MODE_SHIFT; //For match_parent
public static final int AT_MOST     = 2 << MODE_SHIFT; //Represents wrap_content

So the whole measurement method is:

The parent layout first measures itself, then calls child.measure in its own onMeasure, and then the child View will comprehensively measure its own size according to the restriction information of the parent layout and its own content size, and then save the data through the setMeasuredDimension method,

wrap_content invalidation

There is also a getDefaultSize method in onMesure, which is the real method to obtain the size of the view

Let's see, interviewer, this is the answer you want!

    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; // The specsize here represents match_parent
            break;
        }
        return result;
    }

As you can see, there are three default mode s, but at here_ Most and actual are treated as the same case, so why wrap_ What about content invalidation?

It's written in the note. The key is how specSize comes from?

Since the parent view and child view must be involved here, obviously, the answer is in ViewGroup. We found a getChildMeasureSpec method. Here I retain the English explanation, which is easier to understand. Let's see why others do this

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
			switch (specMode) {
           //When the parent view is in the active mode, the match of the child view_ Parent and wrap_ The correspondence of content is ok
        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 view at_ Match of sub view in most mode_ Parent and WRAP_CONTENT has become AT_MOST mode
        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;

Therefore, the core is that the child view cannot bigger than the parent view, so it is the at of the parent view by default_ Most, that is, the maximum space left

So why wrap_content invalid? Because it needs to fill the remaining maximum space of the parent view, it just matches the match of the child view_ Parent attribute effect

How to solve this problem?

In fact, it's very simple. No matter how complex the above things are, they are Default. We just need to customize our width and height in onMeasure in the custom View, and then write back through setMeasuredDimension

layout

layout is used to confirm the position of ViewGroup child elements,

 public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        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);
   			...
 }

As you can see, first initialize the left, top, bottom and right coordinates, and then set setFrame. When the four vertices are determined, the position of the view in its parent container is determined. Even if it is strange, it is locked in the rectangle composed of these four coordinates. Next, call the onLayout() method

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

onLayout() in View is empty, which means that we need to rewrite it ourselves. We can see the rewriting in RelativeLayout

protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //  The layout has actually already been performed and the positions
        //  cached.  Apply the cached values to the children.
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                RelativeLayout.LayoutParams st =
                        (RelativeLayout.LayoutParams) child.getLayoutParams();
                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
            }
        }
    }

Here we get all the child controls and call the layout method of the child controls. Because RelativeLayout may nest other layouts, getLayoutParams here is used to obtain specific location parameters. Obviously, if you want to modify the location of the view, you can directly call setLayoutParams

To sum up, the Layout method determines its own coordinates, then calls onLayout and executes the child control Layout() method to get the coordinates of the child controls.

draw

Measure is to measure the size of the View, and layout is to determine the position of the View. Everything is ready. Only the View is drawn. There are six steps in the draw source code

/*
 * Draw traversal performs several drawing steps which must be executed
 * in the appropriate order:
 *
 *      1\. Draw the background 	Draw background
 *      2\. If necessary, save the canvas' layers to prepare for fading	
 *      3\. Draw view's content	Draw content
 *      4\. Draw children				Draw child controls
 *      5\. If necessary, draw the fading edges and restore layers
 *      6\. Draw decorations (scrollbars for instance)		Draw decoration
 */

Steps 2 and 5 are layer related operations, but we generally don't use them in normal development, so we can skip them. In the draw() source code, the above steps correspond to the following source code

// Step 1, draw the background
drawBackground(canvas);
// Step 3, drawing content
onDraw(canvas);
// Step 4, drawing child controls
dispatchDraw(canvas);
// Step 6, drawing decoration
onDrawForeground(canvas);

Of course, we don't need to rewrite all of our custom view s. Generally, we can draw the contents with onDraw() method, and others can use draw() by default. The simplest onDraw is to draw a circle and draw with Canvas

 protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);
   canvas.drawCircle(width, height, radius, mPaint)
 }

The parameters passed in above only need to be self-contained, so that a View can be drawn. For more complex and interesting views, we will implement them in the user-defined View. Here we only need to understand the principle.

Summary

For View, we need to master its display, sliding, event handling mechanism and rendering mechanism, among which

The presentation needs to understand the representation of relative position and absolute position

Sliding and event handling need to understand the process of down, up and move events from user click to final consumption.

Drawing needs to understand how the view knows its width, height, position and image

last

Interviewer: I'll just ask wrap_content, why don't you start with Pangu?

Me: ah, I accidentally talked too much. In fact, there is another sliding conflict about the sliding event,

Interviewer: the meeting room I booked has exceeded the time limit. People outside will rush in and cut me. Go back and wait for the notice

In order to help you learn better, I have sorted out a learning roadmap and learning documents for Android system learning. You can look down. If you need to refer to the full version of the learning roadmap or materials, you can click the small card below to access them.


Keywords: Android Design Pattern view

Added by ecco on Tue, 02 Nov 2021 11:36:55 +0200