Android View的工做原理(上)

1、前言

众所周知,android为咱们提供大量的基础控件,这些控件完成基本功能是没有问题的,也比较全面,可是对于一些比较精致的产品,不只仅是基础功能实现就OK,它们每每要很炫的效果,这就须要自定义view了,好了很少说了,直接开始主题,View的绘制分为measure、layout、draw,其中测量是最复杂的,咱们单独来说,布局和绘制将在下一篇文章去讲解。android

2、理解ViewRoot和DecorView

在正式讲解View的工做原理以前,咱们先了解一下ViewRoot,ViewRoot的实现类是ViewRootImpl,它是链接WindowManager和DecorView的纽带,View的三大流程都是经过ViewRoot来完成的,它是在ActivityThread中被初始化的。View的绘制流程是从ViewRoot的performTraversals开始的,经历三个步骤后最终呈如今界面的view,大体以下: 面试

performTraversals会依次调用perfornMeasure、performLayout、performDraw,这三个分别完成顶级View的measure、layout、draw,performMeasure再去调用measure,最后去调用onMeasure完成子view的测量,子view会再去调用measure,依次递归下去,直到因此的子view measure完毕。

下面来简单讲一下DecorView,以下图:bash

DecorView是全部View的顶级View,它里面有个LinearLayout,分为title bar和content,咱们常常在onCreat里面用到的setContentView方法就是为这个content设置布局的,也就是说,咱们写的布局都塞进了这个content,哦。。。。,明白了,这就是为啥要叫setContentView而不叫setView了吧。

3、理解一下MeasureSpec

3.1 MeasureSpec的概念

能够说,这个概念是贯穿了整个View绘制的全部流程,是的,表面上看它就是一个尺寸规格,也就是决定View的大小,它绝大部分均可以决定View的大小,固然也不是它一我的说了算,毕竟有些ViewGroup的LayoutParams也对子view的大小有影响。ide

MeasureSpec表明一个32位的int值,其中高两位是mode,低30位是size,其中mode的三种值,分别是:

  • UNSPECIFIED:父容器不对View作任何限制,要多大就给多大,这种主要用于系统内部,应用层开发通常用不到。
  • AT_MOST:就是子View的值根据本身定义的大小来给定,可是不能够超过父类的大小,至关于LayoutParams的wrap_content。
  • EXACTLY:父类已经检测到了子View的精确大小了,这时候View的大小就是SpecSize,它对应LayoutParams的match_parent和具体值这两种状况。

3.2 MeasureSpec和LayoutParams的对应关系

上面提到过了,View的大小是由MeasureSpec来决定的,咱们通常会给view设置LayoutParams参数,这个params参数会在父容器的MeasureSpec约束的状况下转换为对应的MeasureSpec,这个Spec会最终肯定view的测量大小,也就是说view的大小是由父容器的MeasureSpec和view的LayoutParams共同决定的,MeasureSpec一旦肯定后,onMeasure就能够肯定view的测量宽高了。布局

View的measure过程是由ViewGroup的measure传递来的,这里看一下ViewGroup的measureChildWithMargins方法,post

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

能够看到,在测量子view的时候会去获取子view的MeasureSpec,这里详细看一下getChildMeasureSpec方法ui

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

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

这个方法有点长,可是很简单,它就是根据父容器的MeasureSpec和子view的LayoutParams来肯定子view的MeasureSpec。咱们把上述规则总结到一张表里面,方便记忆: spa

这里不是我本身创造的一张表,而仅仅是对上述过程的一种解释而已。可是有一种特殊的状况咱们要注意一下, 就是当子view是warp_content的时候,无论父类是啥,结果都是同样的,这就会有问题,怎么办呢,这就交给子view的onMeasure去处理吧,因此在自定义view的时候若是view 的params设置为wrap_content的时候,咱们就要去实现onMeasure方法。具体的后面会讲。

4、View的measure过程

view的三大过程当中,measure是最复杂的,由于每每要肯定一个view的大小,要经历好屡次测量才能ok。measure过程要分状况来看,View和ViewGroup,由于ViewGroup不只仅要测量本身还要测量子元素,一层一层传递下去。code

4.一、单个View的measure

View里面的measure是final方法,这就意味着该类不容许被继承,measure里面调用了onMeasure方法,也就是说measure的工做就是在onMeasure里面完成的,看看o n M e asure方法:orm

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

代码很简单,setMeasuredDimension设置测量的值,主要的是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;
    }
复制代码

getDefaultsize就是返回测量后的大小,这里注意是测量后的大小,由于view的大小最终肯定是在layout后,有时候layout也会对view的大小形成影响,不过绝大部分getDefaultsize就是最终view的大小。

注意的点: 从getDefaultsize方法能够看到,view的大小由specSize来决定,因此,直接继承View的自定义控件须要重写onMeasure方法而且设置wrap_content时的自身大小,不然在布局中的wrap_content和match_parent就没有什么区别了,从上面的表格也能够清晰的看到,这种状况是咱们不但愿见解的,怎么解决呢?很简单,咱们设置一个默认值就能够了

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //若是view在布局中使用wrap_content ,这时候就是AT_MOST,咱们须要在onmeasure里面作特殊处理,不然和match_parent就没有区别了
        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(500, 300);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize, 300);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500, heightSpecSize);
        }
    }
复制代码

具体的默认值是多少,咱们根据本身的状况来定。

4.2 ViewGroup的measure过程

对于ViewGroup的measure过程,它会更加复杂一点,由于它不只要measure本身,还要measure子view,ViewGroup没有重写onMeasure方法,它提供了另外一种方法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);
            }
        }
    }
复制代码

上面的方法很明了,就是对每个子元素进行measure,咱们看看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);
    }
复制代码

这也太简单了,就是获取子元素的measure spec值,而后调用view的measure操做,这个和单独的view就没啥区别了,就这样一直迭代下去,直到单个的view测量结束。这是测量子元素的过程,那么ViewGroup怎么测量本身的呢。

其实ViewGroup并无定义其测量的具体过程,由于它是一个抽象类,其测量过程onMeasure交给了其子类去实现了,好比LinearLayout类就有本身专门的onMeasure方法,这也是符合逻辑的,由于没个Layout都有本身的特性,咱们不可能在ViewGroup统一去处理。 咱们以LinearLayout为例,看以下代码:

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

看垂直方向的,水平方向的相似:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
        ...

        final int count = getVirtualChildCount();

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        boolean matchWidth = false;
        boolean skippedMeasure = false;

        final int baselineChildIndex = mBaselineAlignedChildIndex;
        final boolean useLargestChild = mUseLargestChild;

        int largestChildHeight = Integer.MIN_VALUE;
        int consumedExcessSpace = 0;

        int nonSkippedChildCount = 0;

        // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            nonSkippedChildCount++;
            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only // laid out using excess space. These views will get measured // later if we have space to distribute. final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); skippedMeasure = true; } else { if (useExcessSpace) { final int usedHeight = totalWeight == 0 ? mTotalLength : 0; measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight); final int childHeight = child.getMeasuredHeight(); if (useExcessSpace) { lp.height = 0; consumedExcessSpace += childHeight; } final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); if (useLargestChild) { largestChildHeight = Math.max(childHeight, largestChildHeight); } } final int margin = lp.leftMargin + lp.rightMargin; final int measuredWidth = child.getMeasuredWidth() + margin; maxWidth = Math.max(maxWidth, measuredWidth); childState = combineMeasuredStates(childState, child.getMeasuredState()); allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT; if (lp.weight > 0) { /* * Widths of weighted Views are bogus if we end up * remeasuring, so keep them separate. */ weightedMaxWidth = Math.max(weightedMaxWidth, matchWidthLocally ? margin : measuredWidth); } else { alternativeMaxWidth = Math.max(alternativeMaxWidth, matchWidthLocally ? margin : measuredWidth); } i += getChildrenSkipCount(child, i); } if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) { mTotalLength += mDividerHeight; } if (!allFillParent && widthMode != MeasureSpec.EXACTLY) { maxWidth = alternativeMaxWidth; } ... maxWidth += mPaddingLeft + mPaddingRight; // Check against our minimum width maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState); if (matchWidth) { forceUniformWidth(count, heightMeasureSpec); } } 复制代码

这个方法很是长,系统会遍历每一个子元素,而且调用子元素的measureChildBeforeLayout方法:

void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }
复制代码

这个方法内部又在执行measure子元素的操做,当子元素所有测量完毕后,Linearlayout才会去测量本身的大小。

注意的点 : 测量的过程是从父类开始分发,递归的测量子元素,最后再测量父类。layout的过程是刚好相反的,咱们后面再讲。

4.三、关于measure可能会遇到的坑

View的measure通常是很复杂的,某些状况下得屡次测量,因此为了保险起见,咱们应该在layout结束后再去获取View的宽和高。在实际需求中,好比,在activity中,你怎么获取某个view的width和height呢?有的人确定会说,很简答啊,直接在oncreat中去调用getWidth和getHeight,这确定是不行的,大家能够去试试,这里获取到的极有多是空值,这是由于View的绘制和Activity生命周期不存在同步的关系,没法保证在哪个周期View的测量工做已经完成了,因此不靠谱。这里简单提一下几种常见的解决方案,可是不展开讲解了:

  • Activity的onWindowsFocusChanged,在这个里面去获取宽和高。
  • view.post(runnable),等到消息队列开始执行的时候,view确定是ready状态了。
  • ViewTreeObserve 重写addOnGlobalLayoutListener方法。

五、总结

1.对于measure过程,咱们只要了解view和ViewGropup的大体流程就能够了,尤为注意 在自定义view 的时候要重写onMeasure方法,而且给wrap_content布局赋默认值。

2.获取某个view宽和高的时机,这个很重要,在面试中常常被考察到。

相关文章
相关标签/搜索