Android View的工作原理(上)

栏目: IOS · Android · 发布时间: 5年前

内容简介:众所周知,android为我们提供大量的基础控件,这些控件完成基本功能是没有问题的,也比较全面,但是对于一些比较精致的产品,不仅仅是基础功能实现就OK,它们往往要很炫的效果,这就需要自定义view了,好了不多说了,直接开始主题,View的绘制分为measure、layout、draw,其中测量是最复杂的,我们单独来讲,布局和绘制将在下一篇文章去讲解。在正式讲解View的工作原理之前,我们先了解一下ViewRoot,ViewRoot的实现类是ViewRootImpl,它是连接WindowManager和Dec

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

二、理解ViewRoot和DecorView

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

Android View的工作原理(上)

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

下面来简单讲一下DecorView,如下图:

Android View的工作原理(上)

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

三、理解一下MeasureSpec

3.1 MeasureSpec的概念

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

Android View的工作原理(上)

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方法,

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方法

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。我们把上述规则总结到一张表里面,方便记忆:

Android View的工作原理(上)

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

四、View的measure过程

view的三大过程中,measure是最复杂的,因为往往要确定一个view的大小,要经历好多次测量才能ok。measure过程要分情况来看,View和ViewGroup,因为ViewGroup不仅仅要测量自己还要测量子元素,一层一层传递下去。

4.1、单个View的measure

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

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.3、关于measure可能会遇到的坑

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

马云如是说

马云如是说

朱甫 / 中国经济出版社 / 2008-1-1 / 39.80元

任何一个企业家的成功,都需要一种特立独行的精神。换尔言之,他一定是不断地否定别人的反对意见,坚持自己独特的观点,才能够真正走向大成功。在中国企业家群像里,马云就是这样一个特立独行的人。 目录 *永不放弃——马云论创业精神 *天下没有难做的生意——马云论经营理念 *B2B时代——马云论电子商务 *网络只是一个工具——马云论互联网与网络公司 *太多钱会坏事——马云论......一起来看看 《马云如是说》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试