Sand Grains III Building My Android Skill Tower: View Workflow

View Workflow

1. Customize View process

Start->Constructor-> onMeasure() ->onSIzeChanged() ->onLaout() ->onDraw() ->View State Change->Finish

  • Constructor: View Initialization
  • onMeasure: Measure View size
  • onSizeChanged: Determine View Size
  • onLayout: Determine sub View layout
  • onDraw: Actual drawing content

2.MeasureSpec

MeasureSpec stands for a 32-bit Int value, two high bits for SpecMode, and thirty low bits for SpecSize. SpecMode refers to the measurement mode, and two SpecSize refers to the size of a specification in a measurement mode.

MeasureSpec avoids excessive object memory allocation by packaging SpecModel and SpecSize into an int value for ease of operation. It provides a packaging and unpacking method. SpecModel and SpecSize are also an int value. A set of SpecModes and SpecSize can be packaged into a MeasureSpec, and a MeasureSpec can get SpecModel and SpecSize by unpacking.The MesureSpec mentioned here refers to the int value represented, not the MeasureSpec itself.

SpecMode s come in three categories:

  • UNSPECIFIED

    Parent containers do not have any restrictions on VIew s, how large or how large they are, which are generally used inside the system to represent a measured state.

  • EXACTLY

    The parent container has detected the exact size required for the View, at which point the final size of the View is the value specified by SpecSize. It corresponds to the match_parent and the exact value in LayoutParams

  • AT_MOST

    The parent container specifies an available size, SpecSize, which VIew cannot be larger than, depending on the implementation of the different Views. It corresponds to wrap_content in LayoutParams.

3. The relationship between MeasureSpec and LayoutParams

MeasureSpec is used to enter the measurements of View internally, but normally we use View to specify MeasureSpec, although we can give ViewSet up LayoutParams. When View measures, LayoutParams will be converted to the corresponding MeasureSpec under the constraints of the parent container, and then the width and height of the View measured will be determined based on this MeasureSpec. It is important to note that MeasureSpec is not the only one determined by LayoutParams, and LayoutParams needs to work with the parent container to determine the View's EasureSpec to furtherDetermines the width and height of the View. Also, for the top-level View (DecorView)For DecorView, the MeasureSpec conversion process is slightly different. For DecorView, its MeasureSpec is determined by the size of the window and its own LayoutParams; for ordinary View, its MeasureSpec is determined by the MasterSpec of its parent container and its own LayoutParams. Once the MeasureSpec is determined, the measured width and height of the VIew can be determined in the onMeasureSpec.

4.View's Maze process

Return to calling View's onMeasure method in View's measure method.

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

The setMeasuredDimension method sets the width/height measurements of the View, so we only need to look at the getDefaultSize method.

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

The result returned by getDefaultSize is the SpecSize in MeasureSpec, which is the size of the View after measurement. The measured size mentioned here many times is because the final size of the View is determined by the layout phase, so it must be distinguished, but in almost all cases the measured and final size of the View are equal.

As for UNSPECIFIED, this is generally used for internal measurement, in which case the size of the View is the first parameter size of getDefaultSize, which is the return value of getSuggestedMinimumWidth and getSuggestedMinimumHeight, respectively.

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

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

    }

As you can see from the above, if the View has no background set, the width of the view is mMinWidth, and mMinWidth corresponds to the value of android:minWidth, so if no background is set, the width of the View is the value specified by android:minWidth. If this property is not specified, then MMinWidthThe default is 0; if the View specifies a background, the width of the View is max(mMinWidth, mBackground.getMinimumWidth());

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}


/**
     * Returns the drawable's intrinsic width.  Returns the inherent width of a drawing object
     * <p>
     * Intrinsic width is the width at which the drawable would like to be laid
     * out, including any inherent padding. If the drawable has no intrinsic
     * width, such as a solid color, this method returns -1.
     *
     * @return the intrinsic width, or -1 if no intrinsic width
     */
    public int getIntrinsicWidth() {
        return -1;
    }

As you can see from the source code, getMinimumWidth returns the original width of the Drawable, provided that it has the original width, otherwise it returns 0; under what circumstances does Drawable have the original width? ShapeDrawable has no original width and BitmapDrawable has the original width and height. (ps: I still don't know o (o)

getMinimumWidth: If the View does not have a background set, it returns the value specified by android:minWidth, which can be 0; if the View has a background set, it returns the maximum of android:minWidth and the minimum width of the background, and the final return value is the width and height of the View measured in the case of UNSPECIFIED.

EXACTLYAT_MOSTUNSPECIFIED
dp/pxchildSizechildSizechildSize
match_parentparentSizeparentSize0
warp_contentparentSizeparentSize0

From the implementation of the getDefaultSize method, the width and height of the View are determined by the SpecSize, so we can conclude that custom controls that directly inherit the View need to override the onMeasure method and size themselves when wrap_content is set, otherwise using wrap_content in the layout is equivalent to using match_parent.From the code above, we know that if VIew uses wrap-content in its layout, it has specMode Is AT_MOST mode, in which the width and height are equal to SpecSize; in this case View's SpecSize is parentSize, as shown in the table above, while parentSize is the size currently available in the parent container, that is, the size of the remaining space in the parent container.The width and height of the view is equal to the size of the parent container's current remaining space, which is exactly the same as using match_parent in the layout.

So, how to solve this problem?

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightSpecSize);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,mHeight);
        }
    }
}

In the code above, we only need to specify a default internal width and height (mWidth mHeight) for VIew and set this width and height when wrap_content.
For non-wrap_content scenarios, we can follow the system's measurements, but there is no fixed basis for how to specify the default internal width and height size. If you look at the source code such as TextView and ImageView, you can see that their onMeasure methods are specially handled for wrap_content scenarios.

5.The measure process of ViewGroup

For ViewGroup, in addition to completing its own measurements, it iterates through and invokes measurezooming in on all child elements, which are recursively executed. Unlike ViewGroup, ViewGroup is an abstract class, so it does not override the onMeasure method of View, which in turn provides a method called measureChildren.

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

ViewGroup measure s each child element when measuring.

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

The idea of measureChild is to take out the LayoutParams of the child element, then create a MeasureSpec of the child element by getChildMeasureSpec, and then pass the MeasureSpec directly to the View's Methodology for measurement. The working process of getChildMeasureSpec is explained in detail above.

ViewGroup does not define the specific process of its measurement because ViewGroup is an abstract class whose onMeasure method of measurement process requires subclasses to implement it, such as LinearLayout, RelativeLayout, etc. Why does ViewGroup not implement its onMeasure uniformly like View? That is because different ViewGroup subclasses have different layout characteristics, which results inTheir test details are different, for example, the layout characteristics of LinearLayout and RelativeLayout are obviously different, so VIewGroup cannot be implemented uniformly.

Recommend a video for everyone

[]: https://www.bilibili.com/video/BV1xK411A7J2?from=search&seid=7260027986203914408&spm_id_from=333.337.0.0

For example, LinearLayout, the system traverses child elements and executes a measureChildBeforeLayout method on each child element, which calls the measuremethod on the child element inside so that each child element begins to enter the measurementprocess in turn, and the system passes through mTotalLengthThis variable stores the initial height of the LinearLayout in the vertical direction. For each child element measured, mTotalLength increases, mainly including the height of the child element and the margin of the child element in the vertical direction. When the child element is measured, LinearLayout measures its size.

For a vertical inearLayout, its horizontal measurement follows the View's measurement process, which is different from View's in the vertical direction. Specifically, if the height in its layout is used inMatch_parent, or a specific value, is measured in the same way as View, i.e. at SpecSize; if wrap_content is the height of Cao Yong in its layout, it is the sum of all the heights occupied by all the child elements, but it still cannot exceed the remaining space of its parent container, although its final height also needs to consider the padding in its slaughter vertical direction.The process can refer to the source code:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

When View's measurements are complete, use getMeasuredWidth/Htight The method can correctly obtain the measured width and height of the View. It is important to note that in some extreme cases the system may need to measure multiple times to get the final measured width and height, in which case the measured width and height obtained in the onMeasure method are likely to be indeterminate. A good habit is to get the measured width or final width of the View in the onLayout method.High.

The measurements process of the View has been analyzed in detail above. Now consider a situation where, for example, we want to do a task when the Activity is started, but this task requires getting the width and height of a View. Some people may choose to get the width and height of this View from onCreate or onResume. In fact, it is not possible in onCreate, onStart, or onResume.The width and height information of a VIew can be obtained correctly because the View's Mensure process and Activity's life cycle method are not synchronized, so there is no guarantee that when the Activity executes onCreate, onStart, onResume, a View has been measured. If the View has not been measured, then the width and height obtained is 0.

Is there any way to solve this problem? The answer is yes. There are four ways to solve this problem in Exploration of Android Development Art:

  • onWindowFocusChanged

    The onWindowFocusChanged method means that the View has been initialized and the width and height are ready, at which point the width and height are fine. It is important to note that onWindowFocusChangedIt is called multiple times, and once when the Activity window gets focus and loses focus. Specifically, when the Activity continues and pauses execution, onWindowFocusChanged is called. If onResume and onPause are used frequently, onWindowFocusChanged is also called frequently.

    Typical code:

    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus){
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }
    
  • view.post(runnable)

    Post allows you to post a runnable to the end of the message queue, and then wait for Looper to call the next runnable while View is initialized.

    The typical code is as follows:

    protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }
    
  • ViewTreeObserver

    This can be accomplished using many callbacks from ViewTreeObserver, such as OnGlobalLayoutListener This interface calls the OnGlobalLayout method back when the state of the View tree changes or the visibility of the View inside the VIew tree changes, so it is a good time to get the width and height of the VIew. It is important to note that OnGlobalLayout is called several times along with the state change of the View tree, etc.

    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }
    
  • view.measure(int widthMeasureSpec, int heightMeasureSpec)

Get the width and height of the View by manually measuring it. This method is complex and needs to be handled case by case, according to LayoutParams:

  1. match_parent

    Give up directly and you can't measure a specific piece of public security. The simple reason is that, according to the measurements process of View, as shown in the table above, constructing such a MeasureSpec needs to know the parentSIze, the remaining space of the stepparent container, and at this time, we can't know the size of the parentSize, so it's theoretically impossible to measure the size of the View.

  2. Specific values (dp/px)

    For example, the width and height are 100px, as follows:

    int widthMeasureSpc = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
    int heightMeasureSpc = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
    view.measure(widthMeasureSpec, heightMeasureSpec);
    
  3. wrap_content

    int widthMeasureSpc = MeasureSpec.makeMeasureSpec((1 << 30)-1,MeasureSpec.AT_MOST);
    int heightMeasureSpc = MeasureSpec.makeMeasureSpec((1 << 30)-1,MeasureSpec.AT_MOST);
    view.measure(widthMeasureSpec, heightMeasureSpe);
    

    (1 << 30) -1, by analyzing the implementation of MeasureSpec, we can know that the size of View is represented by 30-bit binary, that is, the maximum is 30 1, that is, (1 << 30) -1. In the maximization mode, it is reasonable to use View to theoretically support the maximum to construct MeasureSpec.

There are two incorrect uses of View's measure on the network, why is it incorrect? First, it violates the implementation specifications within the system (legal SPEC mode cannot be exited through the incorrect MeasureSpec, which causes the measurement process to fail). Second, there is no guarantee that the measurements will produce the correct results.

6.Layout process

Layout Your role is that ViewGroup determines the location of the child elements. When the location of the ViewGroup is determined, it traverses all the child elements in onLayout and calls its layout method. In the layout method, the onLayout method is called again. The Layout procedure is much simpler than the measure procedure, the layout method determines the location of the View itself, and the onLayout method determines all the elements.Position of 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);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        final boolean wasLayoutValid = isLayoutValid();

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if (!wasLayoutValid && isFocused()) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            if (canTakeFocus()) {
                // We have a robust focus, so parents should no longer be wanting focus.
                clearParentsWantFocus();
            } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
                // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
                // layout. In this case, there's no guarantee that parent layouts will be evaluated
                // and thus the safest action is to clear focus here.
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                clearParentsWantFocus();
            } else if (!hasParentWantsFocus()) {
                // original requestFocus was likely on this view directly, so just clear focus
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
            }
            // otherwise, we let parents handle re-assigning focus during their layout passes.
        } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            View focused = findFocus();
            if (focused != null) {
                // Try to restore focus as close as possible to our starting focus.
                if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
                    // Give up and clear focus once we've reached the top-most parent which wants
                    // focus.
                    focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                }
            }
        }

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

The layout method is roughly as follows: First, the setFrame method is used to set the position of the four vertices of the View, that is, to initialize mLeft, mRight, mTop, mBottom These four values, once the four vertices of the View are determined, the position of the View in the parent container is also determined; then the onLayout method is called, which is used by the parent container to determine the position of the child elements and is fatal to the onMeasure method. The specific implementation of onLayout is also related to the specific layout, so neither View nor ViewGroup actually implements the onLayout method.

Or the implementation logic of onLayout in LinearLayout is similar to that of onMeasure. (ps: The source code is too long, please check it for yourself).
In the layoutVertical method, this method traverses all the child elements and calls the setChildFrame method to specify a corresponding position for the child elements, where the child Top increases gradually, which means that the following child elements will be placed in the lower position, which just fits the characteristics of LinearLayout in the vertical direction. As for the setChildFrameMethod, he just calls the layout method of the child element, so that after the parent element finishes its own positioning in the layout method, it calls the layout method of the child element through the onLayout method, and the child element determines its own location through its own layout method, so that the entire view tree layout process is completed by passing this layer by layer.

private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

We noticed that width and heights in setChildFrame are actually measurements of the width and height of child elements.

In the layout method, the four fixed-point positions of the child elements are set by the setFrame, and the values are assigned in the setFrame so that the position of the child elements is determined.

public void layout(int l, int t, int r, int b) {
    ...

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

At the beginning, we asked the question: What is the difference between the measured width and the final width and height of a View? This question can be specifically: What is the difference between the two methods of getMeasuredWidth and getWidth of a View.

First, let's look at the implementation of getWidth and getHeight:

public final int getWidth() {
    return mRight - mLeft;
}

public final int getHeight() {
        return mBottom - mTop;
    }

Combining mLeft, mTop, mBottom, mRight The return value of the getWidth method is exactly the measured width of the VIew, so in the default implementation of View, the measured width and final width of the VIew are equal, except that the measured width and height are formed in the measurements process of the View, and the final width and height are formed in the layout process of the View, that is, they are assigned at different times and the measured width and height are assigned slightly earlier.

7.Draw process

The View drawing process follows these steps:

  • Draw background.draw(canvas).
  • Draw yourself (onDraw)
  • Draw children(dispatchDraw)
  • Draw Decorations (onDrawScrollBars)

The View drawing process is passed through dispatchDraw, which iterates through the draw methods that call all your child elements so that the draw time is passed down one level at a time.

View has a special method, setWillNotDraw.

public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

If a View does not need to draw anything, it will be optimized after setting this tag bit TRUE. By default, VIew does not enable this optimized tag bit, but ViewGroup will enable this optimized tag bit by default.

When their custom controls inherit from ViewGroup and do not have drawing capabilities, they can be turned on for subsequent optimization. Of course, when we know exactly what a VIewGroup needs to swipe through onDraw, we need to explicitly turn this off.

This is the end of the article. This is a note made today by learning "Exploring the Art of Android Development". I hope you can add and correct the inadequacies!

Keywords: Android

Added by jponte on Sun, 19 Sep 2021 12:55:44 +0300