View 的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制。其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上。
一、View的measure过程
理解MeasureSpec
MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法。需要注意的是这里提到的MeasureSpec是指MeasureSpec所代表的int值,而并非MeasureSpec本身。
SpecMode有三类,每一类都表示特殊的含义,如下所示:
UNSPECIFIED
父容器不对View有任何限制,要多大给多大。这种情况一般用于系统内部,表示一种测量的状态。
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体数值这两种模式。
AT_MOST
父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。他对应于LayoutParams中的wrap_content.
理解完MeasureSpec之后,我们再来看measure,View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会去调用View的onMeasure方法如下所示。
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
}
上述代码很简洁,但是简洁并不代表简单,setMeasuredDimension方法会设置View宽/高的测量值,因此我们只需要看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这个方法的逻辑很简单,对于我们来说,我们只需要看AT_MOST,和.EXACTLY
这两种情况。简单的理解,其实getDefaultSize返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,所以这里必须要加以区分,但是几乎所有情况下View的测量大小和最终大小是相等的。
二、ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的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);
}
}
}
从上述代码看,ViewGroup在measure时,会对每个元素进行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);
}
很显然,measureChild的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。
ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现。,比如:LinearLayout、RelativeLayout等。
以上已经对View的measure过程进行了一下分析,现在假设一种情况,我们想在Activity的onCreate/或者onResume方法里获得View宽高,经常得不到确定的宽高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,怎么解决呢?这里给出四种方法:
1)Activity/View#onWindowFoucusChanged
onWindowFoucusChanged这个方法的含义是:View已经初始化完毕了,宽、高已经准备好了,这个时候去获取宽高是没问题的。需要注意的是,onWindowFoucusChanged会被调用多次,当Activity的窗口得到或者失去焦点时均会被调用。典型代码如下:
public void onWindowFocusChanged(boolean hasFocus){
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasureHeight();
}
}
2)view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。典型代码如下:
protected void onStart(){
super.onStart();
view.post(new Runnable(){
@Override
public void run(){
int width = view.getMeasuredWidth();
int heigth = view.getMeasuredHeight();
}
});
}
```
3)ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当Veiw树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽高一个很好的时机。需要注意的是,伴随着View树状态改变等,onGlobalLayout会被调用多次。典型代码如下:
```
protected void onStart(){
super.onStart();
ViewTreeObserver observer =view.getViewTreeObserver();observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() { view.getViewTreeObserver().removeGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int heigth = view.getMeasuredHeight();
}});
}
```
4)veiw.measure(int widthMeasureSpec,int heightMeasureSpec);
这种方法比较复杂,这里要分情况处理,根据View的LayoutParams来分:
(1)match_parent
直接放弃,无法measure出宽高。
(2)wrap_content
如下measure
```
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 <<30)-1,View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30)-1, View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
```
注意到(1<<30)-1,通过分析MeasureSpec的实现可以知道,View的尺寸使用30位二进制表示,也就是说最大是30个1(即2^30 - 1),也就是(1<<30)-1,在最大化模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的。
(3)具体数值(dp/px)
比如宽、高都是100px,如下measure:
```
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100,View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100,View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
```
####二、layout过程
layout 的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置确定后,它在onlayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比就简单多了,layout方法确定View本身的位置,而onlayout方法则会确定所有子元素的位置。
这里有个问题:通过layout确定的宽高跟measure确定的宽高有什么区别?也就是View的getMeasureWidth和getWidth这两个方法有什么区别?答:在View的默认实现中,View的测量宽高跟最终宽高是相等的,只不过测量宽高形成于View 的measure过程,而最终宽高形成于View的layout过程,但是的确会存在某些特殊情况会导致两者不一致,下面举例说明。
如重写View的layout方法,代码如下:
```
public void layout (){
super.layout(l,t,r+100,b+100);
}
```
上述代码会导致在任何情况下View的最终宽高总是比测量宽高大100px,虽然这样会导致在任何情况下View的显示不正常并且也没有实际意义,但是这证明了测量宽高的确可以不等于最终宽高。另外一种情况是在某些情况下,View需要多次measure之后才能确定自己的测量宽高,在前几次测量中可能跟最终的不一致。
###三、draw过程
View的绘制过程遵循如下几步:
(1)绘制背景background.draw(canvas).
(2)绘制自己(onDraw)
(3)绘制children(dispathDraw)
(4)绘制装饰(onDrawScrollBars)
这一点通过draw方法的源码可以明显的看出来,如下所示:
```
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/* * Draw traversal performs several drawing steps which must be executed * in the appropriate order:
* *
1. Draw the background *
2. If necessary, save the canvas' layers to prepare for fading *
3. Draw view's content *
4. Draw children *
5. If necessary, draw the fading edges and restore layers *
6. Draw decorations (scrollbars for instance) */
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
```
View绘制过程的传递是通过dispathDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层的传递下去。
最后,附带View的绘制流程图:
![performTraversals工作流程.png](http://upload-images.jianshu.io/upload_images/2507383-fe737a6dc5d69b74.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
------- 本文摘自《Android艺术开发探索》