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方法,那么就需要关闭这个标记位。
- View的工作原理
- View的工作原理
- View的工作原理
- View的工作原理
- View的工作原理
- View 的工作原理
- View的工作原理
- view的工作原理
- View的工作原理
- View的工作原理
- View的工作原理
- View的工作原理
- View 的工作原理
- View的工作原理
- View的工作原理
- View的工作原理
- View的工作原理
- View的工作原理
- 【poj 1182】食物链 并查集应用
- 网络编程数据处理_学习笔记_第七周
- 利用Socket原理Java实现双方通信
- 使用spm构建seajs项目
- 第六周项目二带武器角色类
- View的工作原理
- Java第五次实验要求
- 【转】kinect和openNI学习资料汇总
- 监听CollaspingToolbarLayout折叠完成事件
- 先立好flag
- 安卓开发调试过程中出现的问题
- vs2015新建mvc的空模版项目
- 【codevs 1369】题解
- Spring中@ImportResource和@Value加载资源文件