View的工作原理

来源:互联网 发布:平度市淘宝客服招聘网 编辑:程序博客网 时间:2024/04/27 18:47

  • View的工作原理
  • MeasureSpec
  • measure过程
    • View的measure
    • ViewGroup的measure
    • 获取View的测量值
  • layout过程
  • draw过程

View的工作原理

View的主要工作流程包括measure,layout,draw。一个View如果要显示在界面上,首先需要通过measure方法调用onMeasure方法对View进行测量,在onMeasure方法用又会对所有子元素进行measure过程,这样就将measure流程传递到子元素中,子元素再一级一级往下传递,最终完成所有元素的测量。然后会通过layout方法调用onLayout方法对View的位置进行布局,过程同onMeasure过程相似,最后通过dispatchDraw进行绘制的分发,通过onDraw方法完成View的绘制。但是measure方法和layout方法都是final类型的方法,子类不能进行重写。

measure过程决定了View的宽高,我们可以通过getMeasureWidth和getMeasureHeight方法获取测量的View的宽高值。layout过程决定了View各个定点的坐标和实际的View的宽高,我们可以通过getTop,getLeft,getRight,getBottom方法获取View的位置,通过getWidth和getHeight方法获取View的实际宽高。draw过程决定了View的显示,只有完成draw过程View才能显示在界面上。顺序:measure>>layout>>draw。

DecorView是顶层的View,一般情况下他的内部会包含一个垂直的LinearLayout,该LinearLayout包含了TitleBar和ContentView两部分内容,我们在Activity中通过setContentView方法设置的View就包含在该contentView中。contentView的id为android.R.id.content,那么我们就可以通过该id找到我们加载到界面的View,如下所示:

    //获取contentview    ViewGroup content = (ViewGroup) findViewById(android.R.id.content);    //contentview的子元素就是我们设置的View    View view = content.getChildAt(0);

MeasureSpec

View的测量过程受到父容器LayoutParams施加的规则(wrap_content,match_parent)的影响。在测量的过程中,系统会将View的layoutParams根据父容器的规则转化成对应的MeasureSpec,然后再根据这个MeasureSpec测量出View的宽高。也就是说,父容器和子容器的LayoutParams共同影响了View的测量结果。

MeasureSpec是View的一个内部类,主要由SpecMode和SpecSize两部分组成,MeasureSpec通过将两个值进行打包成一个int值控制View的测量。SpecMode有三类,UNSPECIFIED,EXACTLY,AT_MOST。
UNSPECIFIED:指父容器不对View有任何限制,要多大给多大。
EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的大小就是SpecSize所指定的值,一般对应于match_parent和具体的数值这两种模式。
AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体数值需要根据View模式再具体确定。

首先我们看一下ViewGroup的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,但是在measure之前,会将父元素的measureSpec与值和padding,margin进行分析通过getChildMeasureSpec来生成子元素的MeasureSpec。

 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;        }        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);    }

可以看到首先获取父元素的SpecMode和SpecSize,然后判断子元素的LayoutParams设置的值,最终生成子元素的MeasureSpec。

总结下来就是:当View采用固定宽高的时候,无论父容器是什么模式,View的MeasureSpec都是EXACTLY,大小为固定的值;当View的宽高都是match_parent的时候,如果父容器是EXACTLY,那么View也是EXACTLY,大小为父容器的剩余空间;如果父容器是AT_MOST,那么View也是AT_MOST,大小不会超过父容器的剩余空间。当View的宽高是wrap_content,不管父容器的模式是什么,View的模式总是AT_MOST,且大小不能超过父容器的剩余空间。

measure过程

View的measure

View的measure过程通过measure方法进行,在measure会调用onMeasure进行测量,因为measure方法是final类型的,子类中不能进行重写,只需看onMeasure方法是如何实现的即可。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    }    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;    }    protected int getSuggestedMinimumWidth() {        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());    }    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {        boolean optical = isLayoutModeOptical(this);        if (optical != isLayoutModeOptical(mParent)) {            Insets insets = getOpticalInsets();            int opticalWidth  = insets.left + insets.right;            int opticalHeight = insets.top  + insets.bottom;            measuredWidth  += optical ? opticalWidth  : -opticalWidth;            measuredHeight += optical ? opticalHeight : -opticalHeight;        }        setMeasuredDimensionRaw(measuredWidth, measuredHeight);    }

首先通过getSuggestedMinimumWidth,看View是否有背景,如果没有背景,则返回mMinWidth,该值对应的就是布局文件中android:minWidth设置的值,如果没有设置该值,则为0,如果设置了背景图片,则通过mBackground.getMinimumWidth()获取图片的真实宽度,然后与mMinWidth进行比较取其中的较大值作为getDefaultSize中的size输入。

getDefaultSize会根据measureSpec中的specMode进行计算返回View的测量后的大小。

通过getDefaultSize方法的实现来看,View的宽高由specSize决定,如果我们自定义控件继承自View,在布局中无论使用wrap_content或者是match_parent都相当于使用match_parent。因为View在布局中如果使用wrap_content,那么他的SpecMode为AT_MOST,根据之前MeasureSpec章节的分析,此时他的specSize都为父容器的size,那么如果我们想要给子容器设置一个wrap_content时有一个默认的宽高,就需要在onMeasure方法中对宽高的specMode分别进行判断,当specMode为AT_MOST的时候,设置一个固定的宽高即可,如果宽高中只有一个值为wrap_content,另一个值在setMeasuredDimension传递默认的specSize即可。代码比较简单,就不给出。

ViewGroup的measure

ViewGroup与View不同的是,它不仅需要对自身进行测量,同时需要对所有的子元素进行测量。ViewGroup并没有重写onMeasure方法,因为不同的ViewGroup子类有不同的布局特性,导致他们的测量细节各不相同,例如LinearLayout和RelativeLayout,两者的布局特性显然不同,因此ViewGroup无法做统一的实现。

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

我们可以看到,在对子元素进行测量的时候,会获取所有的子元素,然后对每一个执行measureChild方法,在measureChild方法中取出每一个child的LayoutParams,然后再通过getChildMeasureSpec方法创建子元素的MeasureSpec,然后将MeasureSpec传递给子元素进行测量。

然后我们来看一下LinearLayout的具体实现:

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

这是LinearLayout的测量过程,分为垂直布局和水平布局两种的测量方法,我们就看一下垂直布局的,水平布局同理。我们只看其中比较关键的代码:

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {        .......        for (int i = 0; i < count; ++i) {            final View child = getVirtualChildAt(i);            ..........             measureChildBeforeLayout(                       child, i, widthMeasureSpec, 0, heightMeasureSpec,                       totalWeight == 0 ? mTotalLength : 0);            if (oldHeight != Integer.MIN_VALUE) {               lp.height = oldHeight;            final int childHeight = child.getMeasuredHeight();            final int totalLength = mTotalLength;            mTotalLength = Math.max(totalLength, totalLength + childHeight +topMargin +                   lp.bottomMargin + getNextLocationOffset(child));        }    }

我们可以看到,系统会遍历所有子元素,调用measureChildBeforeLayout方法,对子元素进行测量,并且会获取子元素的高度,同时使用mTotalLength来记录当前总的高度,没测量一个,将其高度加到mTotalLength上,这样就能获取所有子元素的高度只和。然后LinearLayout会对自身进行测量。

系统会对heightMode,及垂直方向的模式进行判断,如果为EXACTLY,即match_parent或者是固定高度,那么他的测量过程和View的测量过程相同,如果为AT_MOST,即wrap_content,那么他的高度为所有子元素的高度之和,但是仍然不能超过父容器的剩余空间。同时高度还需要考虑padding和margin的值。

获取View的测量值

很多人会遇到这样一个问题,如果我们需要在Activity中获取某个View的宽和高,但是发现在Activity的onCreate或者onResume里通过view.getWidth()或者getMeasuredWidth()方法获取到的值都为0;这是因为在onCreate的时候,view的measure方法并没有执行完毕,因此此时获取到的测量数据为0。那么应该如何获取view的宽高值呢。

方法1:
重写onWindowFocusChanged,该方法会在activity获取或者失去焦点的时候被调用,也就是activity在对用户可见或者不可见的时候被调用。当activity可见的时候,预示着各个view已经初始化完毕,已经绘制在activity上了,所以此时可以正常获取view的宽高值。

    @Override    public void onWindowFocusChanged(boolean hasFocus) {        super.onWindowFocusChanged(hasFocus);        if(hasFocus){            tv.setText("width:" + tv.getWidth() + "\nmeasureWidth:" + tv.getMeasuredWidth());        }    }

方法2:
通过post可以将一个runnable投递到消息队列的尾部,等待Looper调用次runnable的时候,view也已经初始化好了。

    tv.post(new Runnable() {        @Override        public void run() {            tv.setText("width:" + tv.getWidth() + "\nmeasureWidth:" + tv.getMeasuredWidth());        }    });

方法3:
使用ViewTreeObserver的众多回调可以完成这个功能,例如使用OnGlobalLayoutListener,当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法会被调用。

    ViewTreeObserver observer = tv.getViewTreeObserver();    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {        @Override        public void onGlobalLayout() {            tv.getViewTreeObserver().removeOnGlobalLayoutListener();            tv.setText("width:" + tv.getWidth() + "\nmeasureWidth:" +getMeasuredWidth());        }    });

layout过程

layout的作用是ViewGroup确定子元素的位置,当ViewGroup的位置确定后,他在onLayout中会比遍历所有的子元素并调用layout方法,在layout中又会调用onLayout方法,同measure的过程一样。不同的是layout方法用来确定View本身的位置,而onLayout方法用来确定子元素的位置。view中可以重写layout方法,viewGroup不能重写layout方法。

首先我们看一下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);        }    }

同样的,包括垂直方向和水平方向的layout过程,我们看一下垂直方向的:

    void layoutVertical(int left, int top, int right, int bottom) {        .........        final int count = getVirtualChildCount();        .........        for (int i = 0; i < count; i++) {            final View child = getVirtualChildAt(i);            if (child == null) {                childTop += measureNullChild(i);            } else if (child.getVisibility() != GONE) {                final int childWidth = child.getMeasuredWidth();                final int childHeight = child.getMeasuredHeight();                final LinearLayout.LayoutParams lp =                        (LinearLayout.LayoutParams) child.getLayoutParams();                ........                if (hasDividerBeforeChildAt(i)) {                    childTop += mDividerHeight;                }                childTop += lp.topMargin;                setChildFrame(child, childLeft, childTop + getLocationOffset(child),                        childWidth, childHeight);                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);                i += getChildrenSkipCount(child, i);            }        }    }    private void setChildFrame(View child, int left, int top, int width, int height) {                child.layout(left, top, left + width, top + height);    }

首先会获取子元素的个数,然后对所有子元素进行遍历,获取子元素的宽高值,并同时用childTop保存之前所有child的总高度,那么当前child所处的高度就应该是childTop的值,然后通过setChildFrame方法调用子元素的layout方法来让每一个子元素确定(知道)自己的位置。在这个过程中需要考虑padding,margin以及对齐方式,以此确定child四个顶点确定的位置,通过setChildFrame传递过去。其中width为right-left的值,height为bottom-top的值。

然后我们看一下child的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;        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);            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);                }            }        }        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;    }

可以看到在layout方法中,获取传递过来的四个位置的值,通过setFrame方法设置四个顶点的位置。

通过上边的步骤,子元素每一个的位置都已经确定,只需通过draw过程就可以将其显示在界面上了。

draw过程

draw的过程比较简单,它只是将我们测量好的和定好位置的view绘制出来,通常会按照如下几步进行绘制;

1.绘制背景(background.draw)
2.绘制自己(onDraw)
3.绘制children(dispatchDraw)
4.绘制装饰(onDrawScrollBars)

当绘制子元素的时候,是通过dispatchDraw进行传递(分发)的,dispatchDraw会遍历所有的子元素的draw方法,然后一层一层传递下去。

view有一个特殊的方法,setWillNotDraw(),当view不需要绘制任何内容的时候,那么设置该标记为true的时候系统会进行相应的优化。默认情况下,view没有启用这个标记为,但是ViewGroup会默认启用这个优化标记位,因此在我们自定义控件继承自ViewGroup且自身不具备绘制功能的时候,可以开启这个标记位。如果重写了onDraw方法,那么就需要关闭这个标记位。

0 0