View绘制流程:
起始点为ViewRootImp的performTraversals方法。在该方法中调动这3个方法来触发以下3个流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
performDraw();
一、 measure()过程
主要作用:为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth)
每个View的控件的实际宽高都是由父视图和本身视图决定的。
ViewRoot根对象地属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调View/ViewGroup对象的onMeasure()方法.
如果一个View是ViewGroup,它还有子View,那么它会调用measureChildWithMargins()去测量子View。在该方法里会调用
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
在View的onMeasure()方法里面进行实际测量并设定View的宽高,通过调用 setMeasuredDimension(h , l)方法设定
简单来说,在最顶层的ViewRoot调用measure方法,然后会触发回调onMeasure方法,在onMeasure方法中又会触发调用子View的measure方法来一层层的往下测量。
measure—>onMeasure->measure......
二、layout布局过程
该过程和measure过程一样,也是从ViewRoot发起,逐层下发,通过layout触发onLayout方法,在onLayout方法里面触发子控件的layout方法。
1 、layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法
(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ;
2、如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。
只有ViewGroup需要重写Onlayout方法,去实现它里面的子控件的摆放
layout->onLayout->layout......
三、draw绘制流程
该过程和measure过程也类似。从ViewRoot的draw方法开始,逐层递归往下,通过draw方法触发onDraw方法,但是对于ViewGroup有点区别,draw方法主要流程如下:
1、绘制该View的背景
2、为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)
3、调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)
4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)
值得说明的是,ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,应用程序一般不需要重写该方法,但可以重载父类
函数实现具体的功能。
5、dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个
地方“需要重绘”的视图才会调用draw()方法)。值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能
实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。
6、绘制滚动条,前台等
draw->drawBackground->onDraw->dispatchDraw->onDrawForeground
在ViewGroup中,系统已经为我们实现好了dispatchDraw方法,它会判断哪些子控件需要重新绘制,在该方法中调用drawChild然后在调用子控件的draw方法,进入到以这个子控件为root的绘制流程
我们需要重写onDraw方法来完成我们控件的绘制。
四、触发重新绘制的三个函数:invalidate(),requsetLaytout()以及requestFocus(),这三个函数最终会调用到ViewRoot中的schedulTraversale()方法,该函数然后发起一个异步消息,消息处理中调用
performTraverser()方法对整个View进行遍历。
performTraverser方法中分别调用performMeasure,performLayout,performDraw
invalidate:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”
视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。
一般引起invalidate()操作的函数如下:
1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。
2、setSelection()方法 :请求重新draw(),但只会绘制调用者本身。
3、setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,继而绘制该View。
4、setEnabled()方法 :请求重新draw(),但不会重新绘制任何视图包括该调用者本身。
requestLayout:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括该调用者本身。
requestFocus:请求View树的draw()过程,但只绘制“需要重绘”的视图。
View的测量过程:
1、MeasureSpec :测量规格类,其中高2位代表测量模式,低30位代表大小,其中,测量模式有以下三种
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT; //父容器不对View有任何限制,要多大给多大,一般用于系统内部。
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;//精准模式,父容器检查出子View的精确大小,一般对应于指定dp或者match_parent(当父容器的测量模式为EXACTLY的时候)
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;//最大空间模式,父容器指定了一个大小SpecSize,子View不能够超过这个大小,具体是多少看子View的实现,一般对应于wrap_content
2、MeasureSpec和LayoutParams的关系
首先是根View DecorView的MeasureSpec的创建过程,参看ViewRootImp的measureHierarchy方法
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight);
这里面host是DecorView,lp是window的layoutParams,desiredWindowWidth和desiredWindowHeight是屏幕的宽高。
接着我们看getRootMeasureSpec这个方法,它在measureHierarchy方法里面执行:
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);其中这个lp.width是布局的宽高,他有以下三个值
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
从代码可以看出,DecorView的测量规格,根据width的取值,按照以下规则确定:
ViewGroup.LayoutParams.MATCH_PARENT:精准模式,就是屏幕的宽高
ViewGroup.LayoutParams.WRAP_CONTENT:最大模式,只要不超过窗口大小就行
固定大小,写死dp值的时候,也是精准模式,大小为LayoutParams里面指定的大小
在绘制入口performTraversals()方法初始阶段复制为WindowManager.LayoutParams lp = mWindowAttributes;
final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
WindowManager.LayoutParams 继承自ViewGroup.LayoutParams
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
所以DecorView默认宽高是填满窗口,是精准模式。当根View的测量规格确认之后,我们就会进行它的子View的测量规格
3、子View的MeasureSpec确定
子View的MeasureSpec是根据父view的MeasureSpec确认的,在我们确认了顶级View的MeasureSpec之后,我们开始确认子View的MeasureSpec
一般来讲,布局都是一个容器,需要测量子View,DecorView其实是一个FrameLayout,打开源码看它的onMeasure方法,会调用 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
进入到FrameLayout的onMeasure方法里面,进行子View的测量。这里强调一下,帧布局和线性布局都会调用ViewGroup的measureChildWithMargins方法
去测量子View,和相对布局复杂一些,因为它的各个子View会相互依赖。而在ViewGroup里面也有measureChildren的方法去循环调用测量子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);
}
这里我们就以帧布局来分析
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);
}
从代码可以看出,会调用getChildMeasureSpec来确认子控件的MeasureSpec。入参中,第一个是父控件的测量规格,第二个是父控件已经使用的距离,
包括父View的大小,内边距和外边距,第三个是父控件的宽度。接下来根据源码来分析子View的测量规格的确认。以宽度为例
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//首先获取到父控件的测量模式和测量大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//判断子View还是否有会只的距离,没有的话就取0
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) {//如果子控件的宽度大于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) {//如果子控件的宽度大于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 //如果父控件是未确认大小模式
/**
* Always return a size of 0 for MeasureSpec values with a mode of UNSPECIFIED
*/
//View.sUseZeroUnspecifiedMeasureSpec 说明下这个变量,从注释中可以看出,如果是UNSPECIFIED,那么它会永远返回0,但是在安卓M(6.0)之后
//这个值会返回true,官方注释是说,在安卓6.0之后,可以有一个hints的size对于UNSPECIFIED的MeasureSpecs,在滚动的容器里面,左边的
//子控件会知道父控件的期望size,例如,list items可以是他们父控件的三分之一的大小
static boolean sUseZeroUnspecifiedMeasureSpec = false;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it //如果子控件的宽度大于0,那么子控件的大小就是设定的大小,模式也为精准模式
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;
}
//如果子控件不是确认的大小,那么6.0之前,子View的size会是0,模式是UNSPECIFIED,6.0之后子View的size是父控件的确认size,模式是UNSPECIFIED
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
4、子控件的测量
当View的测量规格确认之后,就会进入到View的measure方法然后进入onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
通过setMeasuredDimension会设定View的最终测量宽高
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://如果是不确认模式,大小为size,getSuggestedMinimumWidth方法的返回值
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;//如果是另外两种模式,就将之前的测量规格的值设定给最终的值。实际情况下,每一个控件都会自己重写自己的onMeasure方法,根据内容重新设定宽高的值
break;
}
return result;
}
这里在说下 getSuggestedMinimumWidth
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果没有设定背景,那么宽度就是mMinWidth,对应android:mMinWidth属性的值,没有设定就是0;如果有设定背景,那么获取背景的最小宽度和mMinWidth比较取最大值
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
该方法返回背景Drawable的原始宽度,这里并不是所有Drawable会有原始宽度BitmapDrawable有值,ShapeDrawable是0
我们可以看出,如果子控件是warp_content模式,那么它的测量大小是parenSize和AT_MOST,这样和mathc_content是一样的,
实际上,在具体的控件的onMeasure方法,最后的大小取值不一定就是parenSize。这里我们以TextView为例,来看看它的onMeasure方法
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
首先取出测量模式和宽高,在定义实际的宽高变量
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.如果是精准模式,实际宽就是测量的宽
width = widthSize;
}
如果不是精准模式而是最大填充模式,根据text的内容,计算出实际的大小然后和测量的大小比较取最小的那个
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
高度也是一样的代码,然后就设定最终测量结果setMeasuredDimension(width, height);
所以,在我们自定义控件的时候,如果要使得warp_content生效,我们必须对onMeasure方法进行重写,参考TextView,定义好实际宽度高度
对于AT_MOST模式,我们要判断是使用之前测量规格里面的大小还是使用实际宽度大小。
5、获取控件的宽高
通过源码我们知道,在onResume方法执行之后,视图才开始进行测量,所以在onResume方法获取不到View的宽高值,所以需要通过以下途径获取
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
}
view.post(runnable)
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(
//在这里面要记得移除掉这个监听器
view.getViewTreeObserver().removeGlobalOnLayoutListener(this)
);
View的Layout过程
layout过程比测量过程简单的多,主要首先会通过setFrame方法来确认View的四个顶点的位置,即mLeft,mRight,mTop,mBottom,确认好四个值之后会开始
执行layout方法。
public final int getWidth() {
return mRight - mLeft;
}
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
正常情况下,在layout过程,会调用setFrame方法来确认四个顶点的位子,而传入的值,一般都是left,left+mMeasuredWidth。所以在最终的情况下
getWidth会和getMeasuredWidth一样。
View的draw过程
1、绘制背景 background.draw(canvas)
2、绘制自己 onDraw
3、绘制children(dispatchDraw)
4、绘制装饰 onDrawScrollBars
5、通知WMS绘制完成mWindowSession.finishDrawing(mWindow);