美文网首页Android开发经验谈Android开发Android技术知识
10分钟读完全文 对View的工作原理 全面理解!

10分钟读完全文 对View的工作原理 全面理解!

作者: 木木玩Android | 来源:发表于2020-07-23 11:35 被阅读0次

    1、ViewRoot 和 DecorView

    1. ViewRoot对应ViewRootImpl类,是连接WindowManager和DecorView的纽带。View的三大流程是通过ViewRoot完成的。 在ActivityThread中,当Activity对象被创建完毕时,会将DecorView添加到Window中,同时会创建ViewRootImpl,且ViewRootImpl和DecorView会建立关联。如下代码,WindowManagerGlobal的addView()方法:
    public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
                ...
                root = new ViewRootImpl(view.getContext(), display);
                root.setView(view, wparams, panelParentView);
                ...
                }
    复制代码
    
    1. View绘制流程从 performTraversals开始,经过Measure、layout、draw。流程图如下

    <figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

    1. DecorView是顶级View,是一个FrameLayout,上面是标题栏、下面是内容栏。内容栏就是setContengView的内容view,id是content。事件 经过DecorView 然后传给我们自己的View。

    2、 MeasureSpec

    MeasureSpec封装了从父级传递到子级的布局要求。系统把view的LayoutParams 根据 父容器施加的规则(父容器的SpecMode) 转换成 view的MeasureSpec,然后使用这个MeasureSpec确定view的测量宽高(不一定是最终宽高)。

    2.1MeasureSpec

    1.MeasureSpec---view的测量规格:高2位的SpecMode,低30位的SpecSize。 2.SpecMode的分类: UNPECIFIED,父容器对view不限制,要多大给多大,一般系统内部使用。 EXACTLY,父容器检测出view所需大小,view最终大小就是SpecSize的值。对应 LayoutParams中的matchParent、具体数值 两种模式。 AT_MOST,父容器制定了可用大小即SpecSize,view的大小不能大于这个值,具体要看view的具体实现。对应LayoutParams中的wrap_content。

    2.2MeasureSpec和LayoutParams的对应关系

    前面说了View的MeasureSpec是由LayoutParams和父容器的MeasureSpec共同决定。顶级view,即DecorView,是由窗口尺寸和自身LayoutParams决定

    1、DecorView,ViewRootImpl中measureHierarchy()方法(performTraversals中执行),代码如下,desiredWindowWidth、desiredWindowHeight是屏幕的尺寸。

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
                ...
                    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
                    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ...
                }
    复制代码
    

    performMeasure()内部是调用mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),mView就是DecorVIew。继续看getRootMeasureSpec()方法如下:

    /**
         * Figures out the measure spec for the root view in a window based on it's
         * layout params.
         *
         * @param windowSize
         *            The available width or height of the window
         *
         * @param rootDimension
         *            The layout params for one dimension (width or height) of the
         *            window.
         *
         * @return The measure spec to use to measure the root view.
         */
        private static int getRootMeasureSpec(int windowSize, int rootDimension) {
            int measureSpec;
            switch (rootDimension) {
    
            case ViewGroup.LayoutParams.MATCH_PARENT:
                // Window can't resize. Force root view to be windowSize.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                // Window can resize. Set max size for root view.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                break;
            default:
                // Window wants to be an exact size. Force root view to be that size.
                measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                break;
            }
            return measureSpec;
        }
    复制代码
    

    DecorView的MeasureSpec就明确了,根据其LayoutParams:

    • MATCH_PARENT:精确模式,就是窗口大小;
    • WRAP_CONTENT:最大值模式,最大值不能超过窗口大小;
    • 固定值(如100dp):精确模式,就是LayoutParams的指定值。

    2、普通View,测量过程从ViewGroup传递下来,看ViewGroup的measureChildWithMargins()方法:

    /**
         * Ask one of the children of this view to measure itself, taking into
         * account both the MeasureSpec requirements for this view and its padding
         * and margins. The child must have MarginLayoutParams The heavy lifting is
         * done in getChildMeasureSpec.
         *
         * @param child The child to measure
         * @param parentWidthMeasureSpec The width requirements for this view
         * @param widthUsed Extra space that has been used up by the parent
         *        horizontally (possibly by other children of the parent)
         * @param parentHeightMeasureSpec The height requirements for this view
         * @param heightUsed Extra space that has been used up by the parent
         *        vertically (possibly by other children of the parent)
         */
        protected void measureChildWithMargins(View child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                            + widthUsed, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                            + heightUsed, lp.height);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    复制代码
    

    即先获取child的MeasureSpec,再调child.measure()。可以看到,child的MeasureSpec是由父容器的MeasureSpec、父容器的padding、child的LayoutParams、child的marging 共同决定。继续看getChildMeasureSpec()方法:

    /**
         * Does the hard part of measureChildren: figuring out the MeasureSpec to
         * pass to a particular child. This method figures out the right MeasureSpec
         * for one dimension (height or width) of one child view.
         *
         * The goal is to combine information from our MeasureSpec with the
         * LayoutParams of the child to get the best possible results. For example,
         * if the this view knows its size (because its MeasureSpec has a mode of
         * EXACTLY), and the child has indicated in its LayoutParams that it wants
         * to be the same size as the parent, the parent should ask the child to
         * layout given an exact size.
         *
         * @param spec The requirements for this view
         * @param padding The padding of this view for the current dimension and
         *        margins, if applicable
         * @param childDimension How big the child wants to be in the current
         *        dimension
         * @return a MeasureSpec integer for the child
         */
        public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
    
            //padding,就是已被占用的空间,就是 父容器的padding+child的marging
            //size,是ViewGroup本身size减去已使用的空间,是ViewGroup能提供给child的最大值。
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
    
            switch (specMode) {
            // Parent has imposed an exact size on us
            case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent has imposed a maximum size on us
            case MeasureSpec.AT_MOST:
                if (childDimension >= 0) {
                    // Child wants a specific size... so be it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size, but our size is not fixed.
                    // Constrain child to not be bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent asked to see how big we want to be
            case MeasureSpec.UNSPECIFIED:
                if (childDimension >= 0) {
                    // Child wants a specific size... let him have it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should
                    // be
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how
                    // big it should be
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
            }
            //noinspection ResourceType
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    复制代码
    

    可见,view的MeasureSpec由 viewParent的MeasureSpec和自身layoutParams确定。另外,child的可利用的尺寸是parent尺寸减去padding,上面代码已有注释,这很好理解。 梳理如下:

    | parentSpecMode

    /childLayoutParams EXACTLY AT_MOST UNSPECIFIED
    dp/px EXACTLY
    childSize EXACTLY
    childsize EXACTLY
    childsize
    match_parent EXACTLY
    parentSize AT_MOST
    parentSize UNSPECIFIED
    0
    wrap_content AT_MOST
    parentSize AT_MOST
    parentSize UNSPECIFIED
    0

    注意,parentSize是父容器可使用的大小。

    更新,看到鸿洋公众号的文章关于UNSPECIFIED说明:

    MeasureSpec.UNSPECIFIED是不是真的不常见?

    在日常定制View时,确实很少会专门针对这个模式去做特殊处理,大多数情况下,都会把它当成MeasureSpec.AT_MOST一样看待,就比如最最常用的TextView,它在测量时也是不会区分UNSPECIFIED和AT_MOST的。

    不过,虽说这个模式比较少直接接触到,但很多场景下,我们已经在不知不觉中用上了,比如RecyclerView的Item,如果Item的宽/高是wrap_content且列表可滚动的话,那么Item的宽/高的测量模式就会是UNSPECIFIED。 还有就是NestedScrollViewScrollView,因为它们都是扩展自FrameLayout,所以它们的子View会测量两次,第一次测量时,子View的heightMeasureSpec的模式是写死为UNSPECIFIED的。 我们在自定义ViewGroup过程中,如果允许子View的尺寸比ViewGroup大的话,在测量子View时就可以把Mode指定为UNSPECIFIED。

    看到ScrollView重写了measureChild方法,指定高度的mode是UNSPECIFIED

    <figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

    3、View的工作流程

    View的三大流程,measure、layout、draw。measure确定view的测量宽高,layout确定view的最终宽高和四个顶点位置,draw绘制到屏幕。

    3.1 Measure过程

    view的测量过程,由measure()方法完成。viewGroup测量自身后,还需调用child.measure()遍历测量子view。

    3.1.1 view的测量过程

    /**
         * <p>
         * This is called to find out how big a view should be. The parent
         * supplies constraint information in the width and height parameters.
         * </p>
         *
         * <p>
         * The actual measurement work of a view is performed in
         * {@link #onMeasure(int, int)}, called by this method. Therefore, only
         * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
         * </p>
         *
         *
         * @param widthMeasureSpec Horizontal space requirements as imposed by the
         *        parent
         * @param heightMeasureSpec Vertical space requirements as imposed by the
         *        parent
         *
         * @see #onMeasure(int, int)
         */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        // measure ourselves, this should set the measured dimension flag back
         onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
    }
    复制代码
    

    可见view的measure()方法是final,不可被子类重写。里面调用onMeasure(),实际真正的测量过程在onMeasure()中。所以只有onMeasure()可以且必须被子类重写。另外,参数widthMeasureSpec、heightMeasureSpec就是上一节最后的表格中的值。继续看onMeasure():

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

    从名字就可以看出,setMeasuredDimension()就是设置测量的尺寸,且在onMeasure()中必须被调用,否则在测量时会发送异常。getDefaultSize()获取默认的宽/高。所以View类中的onMeasure() 是设置默认的宽高。 继续看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;
        }
    复制代码
    

    UNSPECIFIED,一般是系统使用,不需要关心。这里view大小直接取size,就是getSuggestedMinimumWidth()/getSuggestedMinimumHeight(),意思是 建议的 最小宽高。看下实现:

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

    没有背景,就取mMinWidth,就是xml中设置的minWidth属性值;有背景,取 mMinWidth 、背景的MinimumWidth 的较大值。drawable的getMinimumWidth()如下,有固有宽度就取固有宽度(如BitmapDrawable),没有就是0(如ShadeDrawable)。

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

    AT_MOST、EXACTLY,直接取specSize,就是上一节最后的表格中的值,作为测量宽高。那这样取specSize是否合适呢? 再来看一遍specSize的来源。

    | parentSpecMode

    /childLayoutParams EXACTLY AT_MOST UNSPECIFIED
    dp/px 1EXACTLY
    childSize 2EXACTLY
    childsize EXACTLY
    childsize
    match_parent 3EXACTLY
    parentSize 4**AT_MOST
    parentSize** UNSPECIFIED
    0
    wrap_content 5**AT_MOST
    parentSize** 6**AT_MOST
    parentSize** UNSPECIFIED
    0

    1、2的情况,具体dp值,取SpecSize没问题,因为是EXACTLY,就是给定的的尺寸。 3的情况,match_parent,取SpecSize,即parentSize,也没问题,因为是EXACTLY,也是确定的尺寸。 4的情况,match_parent,但父容器又是wrap_content,系统就给了AT_MOST+parentSize,限制最大尺寸为parentSize。而这里直接取specSize即parentSize,似乎也没问题。这个看一个例子一,如下,view是match_parent,可见view取得确实是parentSize。

    5、6的情况,wrapContent即AT_MOST+parentSize,取specSize也就是parentSize,所以和3、4一样都是parentSize,即 View类 中 默认wrapContent等同于match_parent。

    再看一个情况例子二,如下,View换成TextView(继承View),尺寸就不是parentSize了,而是内容尺寸,说明TextView在onMeasure中做了处理。

    image.png

    继续看,例子三如下,同时有TextView、View,此时textView又是取parentSize(可用空间):

    所以得出结论: 通常直接继承View的自定义View,在onMeasure()需要处理 : a、wrap_content的情况,否则wrap_content就等同于match_parent; b、match_parent+父容器wrap_content的情况,否则就像例子一,父容器wrap_content是无效的,处理方式就是例子二中的textView。 总结就是,直接继承View的自定义View,需要处理AT_MOST时的宽高

    处理方式如下:

    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(mWidth, mHeight);
            } else if (widthMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(mWidth, heightSize);
            } else if (heightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(widthSize, mHeight);
            }
        }
    复制代码
    

    实际就是在 AT_MOST时 设置一个指定的尺寸mWidth、mHeight,其他情况沿用系统。至于mWidth、mHeight是多少,则要具体看你的view的逻辑了。例如TextView,可以参考其源码的实现。

    3.1.2 ViewGroup的测量过程

    ViewGroup需要完成自身的测量,还要遍历子view调用measure()方法进行测量。

    ViewGroup是抽象类,没有重写onMeasure,因为无法做到统一,是让具体继承ViewGroup的子类重写自己的逻辑。但是提供一些方便的方法给子类调用。如measureChildren()、measureChild()、measureChildWithMargins(),上面第二节分析过measureChildWithMargins(),这里我们看下measureChildren():

    /**
         * Ask all of the children of this view to measure themselves, taking into
         * account both the MeasureSpec requirements for this view and its padding.
         * We skip children that are in the GONE state The heavy lifting is done in
         * getChildMeasureSpec.
         *
         * @param widthMeasureSpec The width requirements for this view
         * @param heightMeasureSpec The height requirements for this view
         */
        protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
            final int size = mChildrenCount;
            final View[] children = mChildren;
            for (int i = 0; i < size; ++i) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                    measureChild(child, widthMeasureSpec, heightMeasureSpec);
                }
            }
        }
    复制代码
    

    就是遍历子view,调用measureChild(),继续看:

    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);
        }
    复制代码
    

    通过getChildMeasureSpec()获取child的MeasureSpec,然后调用child.measure(),测量就传到child内部了,很好理解。measureChild()相比measureChildWithMargins() 没有考虑child的margin值。

    上面说了,ViewGroup没有重写onMeasure,因为无法做到统一,让具体继承ViewGroup的子类重写自己的逻辑。具体看下LinearLayout的测量过程

    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            if (mOrientation == VERTICAL) {
                measureVertical(widthMeasureSpec, heightMeasureSpec);
            } else {
                measureHorizontal(widthMeasureSpec, heightMeasureSpec);
            }
        }
    复制代码
    

    继续看measureVertical():

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        //下面这句官方注释:看每个人多高,也记住最大宽度。想想这不就是计算竖向LinearLayout宽高的思路嘛!
        // See how tall everyone is. Also remember max width.
            for (int i = 0; i < count; ++i) {
                ...
                final View child = getVirtualChildAt(i);
                ...
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                ...
                    // Determine how big this child would like to be. If this or
                    // previous children have given a weight, then we allow it to
                    // use all available space (and we will shrink things later
                    // if needed).
                    final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                    //这里测量child(里面就是measureChildWithMargins())
                    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                            heightMeasureSpec, usedHeight);
    
                    final int childHeight = child.getMeasuredHeight();
                    ...
                    final int totalLength = mTotalLength;
                    //这里mTotalLength加上child的高度、margin,就是child高度累积。
                    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                           lp.bottomMargin + getNextLocationOffset(child));
                    ...
                    //这里记录最大宽度(包含margin)
                    final int margin = lp.leftMargin + lp.rightMargin;
                    final int measuredWidth = child.getMeasuredWidth() + margin;
                    maxWidth = Math.max(maxWidth, measuredWidth);
                ...
            }
            //遍历完了:高度加上自身的上下padding
            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            int heightSize = mTotalLength;
            // Check against our minimum height
            heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    
            //这里很重要:调用resolveSizeAndState--决定 计算的高度(高度累加)和 LinearLayout的父容器约束的高度,取哪一个。
            // Reconcile our calculated size with the heightMeasureSpec
            int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
            heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        ...
            //最大宽度加上左右margin
            maxWidth += mPaddingLeft + mPaddingRight;
    
            // Check against our minimum width
            maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    
            //设置最终的测量尺寸(宽也也同样调用resolveSizeAndState决定取哪个)
            setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                    heightSizeAndState);
    }
    复制代码
    

    所以,简单概括就是: 1.先测量所有child; 2.根据child的情况获取自身宽高(累加高度、最大宽度)。

    那么,是否就取 累加高度、最大宽度?再看下resolveSizeAndState():

    /**
         * Utility to reconcile a desired size and state, with constraints imposed
         * by a MeasureSpec. Will take the desired size, unless a different size
         * is imposed by the constraints. The returned value is a compound integer,
         * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
         * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
         * resulting size is smaller than the size the view wants to be.
         *
         * @param size How big the view wants to be. --想要的尺寸
         * @param measureSpec Constraints imposed by the parent. --父布局给的measureSpec
         * @param childMeasuredState Size information bit mask for the view's
         *                           children.
         * @return Size information bit mask as defined by
         *         {@link #MEASURED_SIZE_MASK} and
         *         {@link #MEASURED_STATE_TOO_SMALL}.
         */
        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:
                    //AT_MOST时,想要的尺寸大于约束的尺寸,就只能取 约束的尺寸。
                    if (specSize < size) {
                        result = specSize | MEASURED_STATE_TOO_SMALL;
                    } else {
                        result = size;
                    }
                    break;
                case MeasureSpec.EXACTLY:
                    //dp值、match_parent且父EXACTLY,就是SpecSize
                    result = specSize;
                    break;
                case MeasureSpec.UNSPECIFIED:
                default:
                    result = size;
            }
            return result | (childMeasuredState & MEASURED_STATE_MASK);
        }
    复制代码
    

    这个过程就是 限制 AT_MOST时,即wrap_content(或match_parent且父wrap_content)时高度不能大于parent的剩余空间。

    3.1.3 获取View宽高的时机

    Measure过程完成,就可通过getMeasuredWidth()、getMeasuredHeight()获取测量宽高。但某些极端情况 需要多次Measure才能确定最终宽高。所以在onLayout方法中获取测量宽高是真正ok的。 我们知道,activity的onCreate中无法获取到view的宽高。实际onCreate、onStart、onResume都不能保证view已完成测量,所以可能获取的都是0。因为view的measure和activity生命周期不是同步的。

    以下是保证可以获取view测量宽高的方法

    1、Activity/View # onWindowFocusChanged

    onWindowFocusChanged:View已初始化完毕,宽高已准备ok。 但会多次调用,获取焦点、失去焦点都回调用。(这个回调是ViewRootIml中分发到DecorView,接着到Activity、到各级View。)

    @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            if (hasFocus) {
                int measuredWidth = scoreView.getMeasuredWidth();
                int measuredHeight = scoreView.getMeasuredHeight();
            }
        }
    复制代码
    

    2、view.post(runnable)

    view.post可以把runnable放入消息队列,等待looper到此runnable是view已经初始化完成。v详细原理参考【Android源码解析】View.post()到底干了啥

    @Override
        protected void onStart() {
            super.onStart();
            scoreView.post(new Runnable() {
                @Override
                public void run() {
                    int measuredWidth = scoreView.getMeasuredWidth();
                    int measuredHeight = scoreView.getMeasuredHeight();
                }
            });
        }
    复制代码
    

    3、ViewTreeObserver

    ViewTreeObserver有很多回调,其中有个OnGlobalLayoutListener,当View树的状态发生改变或者View树内部view的可见性发生改变时 方法 onGlobalLayout()都会被调用。所以是会回调多次。 此时也可以获取view的宽高:

    ViewTreeObserver observer = view.getViewTreeObserver();
            observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    if (mIsGroupListAnimating) {
                        mIsGroupListAnimationPending = true;
                    } else {
                        updateLayoutHeightInternal(animate);
                    }
                }
            });
    复制代码
    

    3.2Layout过程

    layout()的作用是View用来确定view本身位置,内部调用onLayout()来确定子view的位置。 layout过程比measure过程简单很多。看View的layout方法:

        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;
            //使用setFrame方法设置4个顶点,就确定位置了~
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                //这里调用onLayout,是个空实现。ViewGroup中重写了,还是空实现,但加了abstract,即ViewGroup的子类必须重写onLayout确定子View的位置。
                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);
                    }
                }
            }
    
            ...
        }
    复制代码
    

    先是用setFrame方法设置4个顶点,就确定位置了,即mLeft、mTop、mBottom、mRight确定了。 然后调用onLayout,是个空实现。ViewGroup中重写了onLayout,还是空实现,但加了abstract,即ViewGroup的子类必须重写onLayout确定子View的位置。 那就看看LinearLayout的onLayout

    @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            if (mOrientation == VERTICAL) {
                layoutVertical(l, t, r, b);
            } else {
                layoutHorizontal(l, t, r, b);
            }
        }
    复制代码
    

    继续看layoutVertical():

    void layoutVertical(int left, int top, int right, int bottom) {
            final int paddingLeft = mPaddingLeft;
    
            int childTop;
            int childLeft;
    
            // Where right end of child should go
            final int width = right - left;
            int childRight = width - mPaddingRight;
    
            // Space available for child
            int childSpace = width - paddingLeft - mPaddingRight;
    
            final int count = getVirtualChildCount();
    
            final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
            final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    
            switch (majorGravity) {
               case Gravity.BOTTOM:
                   // mTotalLength contains the padding already
                   childTop = mPaddingTop + bottom - top - mTotalLength;
                   break;
    
                   // mTotalLength contains the padding already
               case Gravity.CENTER_VERTICAL:
                   childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
                   break;
    
               case Gravity.TOP:
               default:
                   childTop = mPaddingTop;
                   break;
            }
            //遍历子view
            for (int i = 0; i < count; i++) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    childTop += measureNullChild(i);
                } else if (child.getVisibility() != GONE) {
                    //获取child的测量宽高
                    final int childWidth = child.getMeasuredWidth();
                    final int childHeight = child.getMeasuredHeight();
    
                    final LinearLayout.LayoutParams lp =
                            (LinearLayout.LayoutParams) child.getLayoutParams();
    
                    int gravity = lp.gravity;
                    if (gravity < 0) {
                        gravity = minorGravity;
                    }
                    final int layoutDirection = getLayoutDirection();
                    final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                    switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                        case Gravity.CENTER_HORIZONTAL:
                            childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                    + lp.leftMargin - lp.rightMargin;
                            break;
    
                        case Gravity.RIGHT:
                            childLeft = childRight - childWidth - lp.rightMargin;
                            break;
    
                        case Gravity.LEFT:
                        default:
                            childLeft = paddingLeft + lp.leftMargin;
                            break;
                    }
    
                    if (hasDividerBeforeChildAt(i)) {
                        childTop += mDividerHeight;
                    }
    
                    childTop += lp.topMargin;
                    //以上就是获取子view的左、上的位置,即宽高,然后调用setChildFrame
                    setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                            childWidth, childHeight);
                     //top位置加上高度和margin,就是下一个view的top
                    childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    
                    i += getChildrenSkipCount(child, i);
                }
            }
        }
    复制代码
    

    就是遍历子view,确认childLeft、childTop,调用setChildFrame确认子view的位置:

    private void setChildFrame(View child, int left, int top, int width, int height) {
            //这里width、height就是 上面获取的 测量宽高
            child.layout(left, top, left + width, top + height);
        }
    复制代码
    

    也就是调用child的layout方法,这样就走child的layout过程了。

    一个问题:getMeasuredWidth() 与 getWidth()有何区别? 答曰:一般情况,getMeasuredWidth() 与 getWidth()两者无区别。 先看,getWidth():

    public final int getWidth() {
            return mRight - mLeft;
        }
    复制代码
    

    在上面分析LinearLayout时,child.layout的参数中 mRight就是mLeft + measuredWidth,所以getWidth()就是measuredWidth。只不过是measuredWidth在测量过程产生,getWidth()在layout过程产生。 只要不重写view的layout()方法(也不需要重写)改变顶点位置就不会出现不同的情况,例如下面这个最终宽高比测量宽高大100。

        public void layout(int l, int t, int r, int b) {
            super.layout(l,t,r+100,b+100);
        }
    复制代码
    

    3.3Draw过程

    draw过程: 1、画背景 2、画自己-- onDraw,自己实现 3、画子view-- dispatchDraw 4、画装饰

    public void draw(Canvas canvas) {
            final int privateFlags = mPrivateFlags;
            final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                    (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
            mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
            /*
             * 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;
            }
    复制代码
    

    ViewGroup一般不用onDraw画自己,只需要画子View就可以了。但明确需要画自己的话,需要调用setViewNotDraw(false);

    以上View的三大流程就分析完了。

    4、自定义View

    自定义view涉及view层次结构、事件分发、工作原理,有一定复杂度,但也是有章可循的。

    4.1自定义view的分类

    1. 继承View:重写onDraw,要处理wrap_content、padding。
    2. 继承ViewGroup:重写onMeasure测量自己、子View,重写onLayout布局子View。
    3. 继承特定View(如TextView):扩展自己的功能。
    4. 继承特定ViewGroup(如LinearLayout):扩展自己的功能。

    4.2 自定义view 注意点

    1. 支持wrap_content:直接继承View或ViewGroup的,要在onMeasure中处理wrap_content的情况。
    2. 支持padding:直接继承View在onDraw中处理;直接继承ViewGroup,在onMeasure、onLayout中处理padding和子view的margin。
    3. 不要在View中使用handler,因为本身提供了post方法。
    4. 在View#onDetachedFromWindow中停止动画或线程。
    5. 处理好嵌套滑动。

    4.3 例子

    自定义ViewGroup实例:横向滑动HorizontalView

    4.4 自定义view的思想

    先掌握基本功,弹性滑动、滑动冲突、绘制原理等,然后选择自定义的类别,按照注意事项多做就可以了。

    .题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

    我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料免费分享出来。

    知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。包含知识脉络 + 诸多细节,由于篇幅有限,下面只是以图片的形式给大家展示一部分。

    Android学习PDF+学习视频+面试文档+知识点笔记

    【Android高级架构视频学习资源】

    Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

    【Android进阶学习视频】、【全套Android面试秘籍】可以简信我【学习】查看免费领取方式!

    你的 点赞、评论、收藏、转发,是对我的巨大鼓励!

    相关文章

      网友评论

        本文标题:10分钟读完全文 对View的工作原理 全面理解!

        本文链接:https://www.haomeiwen.com/subject/zkiclktx.html