Android View的工作原理(上)

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

内容简介:众所周知,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方法。

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

查看所有标签

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

Domain-Driven Design

Domain-Driven Design

Eric Evans / Addison-Wesley Professional / 2003-8-30 / USD 74.99

"Eric Evans has written a fantastic book on how you can make the design of your software match your mental model of the problem domain you are addressing. "His book is very compatible with XP. It is n......一起来看看 《Domain-Driven Design》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器