View 是开发中最常用的类之一,我们来看看View的真面目吧。
首先,先了解几个概念
window是个抽象类,用于承载View和Viewgroup的类,我们在activity中添加View的过程,其实承载这些View的都是Window,只要有View的地方就有Window,比如说Toast,Dialog等。
在activity中View的从层次结构如下图。
在最外层黑的是activity,红的是window,绿的是顶级View(DecorView),接着是标题栏,ContentView
DecorView是个FrameLayout一般内部有个LinearLayout,LinearLayout是标题栏,ContentView也是FrameLayout,它具有id=content,可以通过R.android.id.content找到ContentView,找到之后我们可以得到我们为activity设置的布局View,contentView.getchildAt(0)。
从上图可知,View的绘制是从DecorView开始绘制的。而它的绘制是有ViewRoot类的performTraversals()方法开始的。如下图
从上图看出,绘制过程是从ViewGroup开始,每个View都要经历三个过程,依次执行ViewGroup的Measure遍历子View执行Measure,然后ViewGroup的layout遍历子View执行Layout,draw同上步骤。
MeasureSpec,理解为测量规格。
MeasureSpec 值是一个32位int数,高2位代表了SpecMode指测量模式,低30位代表了SpecSize指在改成测量模式下的大小。
子View的该值由父容器在Measure过程中 计算得到,然后调用子View的Measure将MeasureSpec传入,进行子View的Measure过程。
那是怎么通过Measure,获得模式和大小呢?其实,还有一个Mask值为0x3<< 30用来求解Mode和Size,通过MeasureSpec的值与Mask按位与,即可 得到相应的值。
SpecMode有三类:
UNSPECIFIED:父容器不对子View有任何限制,要多大给多大,一般用于系统内部,常常我们给View指定一个默认大小。
EXACTLY:父容器已经得到子View的精确大小,对应于子View的LayoutParams是matchParent和具体数值的情况。
AT_MOST:父容器指定一个可用大小即SpecSize,子View不能大于这个值,View对应的Layoutparams是wrap_content
MeasureSpec和LayoutParams 的关系:
DecorView的MeasureSpec由窗口的尺寸和自身的LayoutParams来共同确定。对于布局中的普通View,MeasureSpec由父容器的MeasureSpec和自身的Layoutparams(包括margin和padding等参数)共同决定。MeasureSpec一旦确定后就可以对子View进行Measure确定View的测量宽高。看一段源码:
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
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计算出child的MeasureSpec然后,调用其Measure()进行测量。看看getChildMeasureSepc()的源码。
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这段代码的注释写的还是比较清楚的,根据父容器的MeasureSpec中的mode和siez值结合子View自身的Layoutparams得到MeasureSpec,调用makeMeasureSpec生成子View的MeasureSpec的值。
view的MeasureSpec创建规则,childlayoutparams和parentSpecmode决定如下表所示
看出wrap_content时需要进行处理否则和match-parent大小一样,
可以指定一个默认的大小,或者根据View的内容确定大小。
Measure用于测量子view的宽高,layout用于确定view的顶点的位置和最终的宽高,draw用于在频幕上绘制view
这里要区分View和Viewgroup的Measure
viewgroup除了测量自身外还要遍历其child,对child进行测量。
View的Measure是final方法,会调用onMeasure(),我们经常需要重写onMeasure()实现自定义View。看看onMeasure()源码。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
**
* This method must be called by {@link #onMeasure(int, int)} to store the
* measured width and measured height. Failing to do so will trigger an
* exception at measurement time.
*
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);
}
onMeasure调用setMeasuredDimension()设置View的宽高测量值,从注释可以看出该方法必须在onmeasure中调用以保存测量的宽高,getdefaultSize()处理了mode是UNSPECIFED情况,给定一个默认大小,其他情况使用MeasureSpec的值。
ps:这里需要注意在自定义View的时候需要在onMeasure中处理wrap_content的情况,否则wrap_content即mode是At_most的大小和match_parent大小一样。
ViewGroup是个抽象类,除了Measure自身还需要measureChildren(),但没有实现onMeasure方法,像LinearLayout和RelativeLayout的onMeasure方法实现是有差别的,所以由子类具体实现onMeasure。
看一下LinearLayout的onMeasure()实现:
// 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;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
代码还是有点多的,就不贴了,主要是计算了垂直方向遍历子View的高度得到总的高度然后调用setMeasuredDimension()设置自己的大小。
Measure过程完了之后,就可以在onMeasure中获得view的宽高了,但这可能是不准确的,有时候Measure需要多次执行,最好在onlayout中获取最终的宽高。
如果想在activity初始化时候如oncreate,onresume中获得view的宽高是不行的,因为view的Measure和activity的生命周期不是同步执行的。得到的宽高可能是0.
可以在以下几处地方获取,view的宽高。
1.onwindowFoucusChanged
该方法表示view已经初始化完毕了,注意该方法会被多次调用,,activity的窗口失去焦点和获得焦点都会调用此方法。比如onpause和onresume
2.view.post(Runnable)
在activity的onstart中通过post将一个Runnable投递到消队列的尾部,在run中获取测量宽高,
3.ViewTreeObserver
该类的多个方法可以实现获取宽高功能,比如OnGlobalLayoutListener,,View树的状态或者View的可见性发生改变时都会调用onGloaballayout()回调,可以在此获得宽高。
4.手动调用Measure()方法
根据view的Layoutparams判断是否能获取成功,
match_parent不行此时需要知道父view的Measurespec,
具体数值,
MeasureSpec.makeMeasureSpec(100,MesureSpec);
View.measure()
wrap_content
此时指定mode为at_most,
layout在Viewgroup中的作用是Viewgroup确定自身的位置和子元素的位置,Viewgroup位置确定后会调用onlayout确定子View的位置,调用子View的layout继续确定子子view的位置,不断遍历。
layout调用setFrame确定view的四个顶点位置,
onlayout有子类具体实现,LinearLayout中会调用setChildFrame()确定子view的顶点,
最后说明一下measuredwidth和width的区别,默认情况下,是相同的,width是在layout之后通过右顶点减左顶点得到的,特殊情况在onlayout()中传入的顶点不是测量得到的宽高是,就会出现不同。
draw过程比较简单,分为六部,主要介绍一下四部
1.绘制背景,background.draw
2.绘制自己ondraw
3.绘制chilren,dispatchdraw
4.绘制装饰,onDrawScrollBars;
Viewgroup有一个方法setWillNotDraw(),当继承自viewgroup时,如果不需要绘制自己可以设置改值为false,系统会进行相应优化,viewStub就是使用这个方法,实现不绘制。
view的状态改变会导致重绘,也可以动态设置view的显隐导致重绘.
调用performTranversal,走绘制流程,但是另外需要注意的是,invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。
这个方法中的流程比invalidate()方法要简单一些,但中心思想是差不多的,这里也就不再详细进行分析了。