知识准备:
1.每个activity都会有一个Window(唯一实现类PhoneWindow)对象,Window对象包含ViewRoot(ViewRootImpl.java),ViewRoot包含DecorView。
DecorView继承自FrameLayout(即ViewGroup),ViewRootImpl不是具体View,它相当于一个View的工具类或者叫管理类,比如view.requestLayout()个人理解:这里仅仅是代表DecorView的高度=content高度+状态栏高度+导航栏高度,状态栏、导航栏并不是由DecorView负责绘制的,因为他们并不在同一个绘制窗口的同一层中。
进入正题,下面通过伪代码的方式分析view绘制流程
一、ViewRootImpl
整个画面的绘制流程是从ViewRootImpl的performTraversals()方法开始的
public class ViewRootImpl {
private void performTraversals() {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
performDraw();
//这里会调用DecorView的measure、layout、draw方法,DecorView继承自FrameLayout即ViewGroup,这样整个画面的View绘制流程就串联起来了。
//根据这三行代码可知,Measure、Layout、Draw这三大过程是针对整个view树结构进行的,并不是一个view测量完之后就进行这个view的layout及draw过程。
//而是整个view树结构的measure完成后,再进行整个View树结构的layout,最后进行整个view树的draw。所以,不要经常无意义的重绘画面,资源耗费很大。
// 虽然进行重绘的过程会根据第一次绘制的参数有所优化,但是也是有很复杂的计算过程。
//上面三个方法中的宽高的参数,是系统根据屏幕尺寸,当前界面theme计算出来的。
}
}
二、ViewGroup
public class ViewGroup {
// 一、measure过程:
// 默认没有提供:
// 1.public final void measure(int widthMeasureSpec, int heightMeasureSpec)方法,因为是final的,所以是在view中实现的,请到View中寻找相关默认代码。
// 2.onMeasure(int widthMeasureSpec, int heightMeasureSpec),这个需要各个不同ViewGroup控件自己实现,请到View中寻找相关默认代码
//比如LinearLayout的onMeasure()方法,就会调用到measureChildWithMargins(),然后getChildMeasureSpec()方法,返回的宽高值,调用child.measure(width,weight)
//这样,getChildMeasureSpec()方法里的规则就影响到了子View的宽高,以及wrapContent问题。
// ViewGroup默认提供了如下几个方法,作为工具方法在自定义ViewGroup时候使用:
// 遍历每个孩子,调用measureChild方法对其进行测量
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];
//测量子元素时,对可见性为GONE的View要做特殊处理,一般来说就是跳过对它们的测量,来优化布局效率。
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);
}
//在自己自定义控件时,上面的这两个方法几乎不会用到。因为measureChildren太过简单粗暴,
//只考虑了父控件的padding,但是没考虑到child view的margin,这就会导致child view在使用match_parent属性的时候,margin属性会有问题
//而且一般都要考虑孩子们之间的逻辑关系(排列顺序、间隔等),再计算他们的测量规格。
// 不过这个方法也给我们一点启示,就是:
//测量子元素时,对可见性为GONE的View要做特殊处理,一般来说就是跳过对它们的测量,来优化布局。
//一般都用下面的 几个方法来测量子view,比如LinearLayout就用到了下面的方法,来测量子view相关参数
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);
}
//measureChildWithMargins方法,比measureChild方法多考虑了个margin。所以,这个方法使用的更多一些。
/**
* getChildMeasureSpec()方法的作用是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个最可能符合条件的child view的测量规格。
*而且请注意,这各方法里的运算规则,最终影响了自定义view时候,view要在其onMeasure中单独处理view的wrap_conent属性,不处理的话就和match_parent
*呈现出一样的效果了
*
* @param spec 父控件的测量规格
* @param padding 父控件里已经占用的大小
* @param childDimension child view布局LayoutParams里的尺寸
* @return child view 的测量规格
*/
//注意,这个方法是static的
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) {
// 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了
case MeasureSpec.EXACTLY:
//如果child的布局参数有固定值,比如"layout_width" = "100dp"
//那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局参数是"match_parent",也就是想要占满父控件
//而此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了,就是父控件的大小,测量模式也是EXACTLY
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
//比如TextView根据设置的字符串大小来决定自己的大小
//那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
//所以测量模式就是AT_MOST,测量大小就是父控件的size,注意这里wrap_content返回的测量模式是AT_MOST,
//并且允许使用的最大尺寸是父控件的大小parentSize,这样在View的onMeasure中,默认的getDefaultSize()方法中,没有对这个AT_MOST情况作出处理,
//而是直接返回了父控件的大小,就相当于match_parent效果了,这里先注意一下,等分析到View的onMeasure过程时,会再次分析到。
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
case MeasureSpec.AT_MOST:
//同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
//但同样的,child的尺寸上限也是父控件的尺寸上限size
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
//child想要根据自己逻辑决定大小,那就自己决定呗
//还是要注意这里child的AT_MOST模式,及wrap_content
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
//这个case一般用不到,是系统重绘的时候用到的
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);
}
//二、layout过程
//通过super.layout(l, t, r, b);能调用到View类里的setFrame()和onLayout()方法
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
//ViewGroup方法里onLayout声明成了抽象方法,所以继承ViewGroup的类都得自己去实现自己定位子元素的逻辑。
//比如LinearLayout中onLayout最终会调用child.layout(left, top, left + width, top + height);方法,去布局子元素
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
//三、draw过程
//没有提供实现:draw(Canvas canvas)、drawBackground()、onDraw(canvas)、onDrawScrollBars,都复用View的默认实现
//提供实现了dispatchDraw(canvas),此方法里遍历调用child.draw(canvas, this, drawingTime);绘制孩子
@Override
protected void dispatchDraw(Canvas canvas) {
for (int i = 0; i < childrenCount; i++) {
child.draw(canvas, this, drawingTime);
}
}
}
三、View
public class View {
//一、measure过程:
//注意这个方法是final的,不能被重写
// 父布局ViewGroup调用过ViewGroup.getChildMeasureSpec(),得到的宽高值被传到View.measure()方法里,ViewGroup.getChildMeasureSpec()根据规则,
// 产生MeasureSpec.AT_MOST、MeasureSpec.EXACTLY等模式,传给了子View,从而传给了onMeasure()方法。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
//这个方法中的setMeasuredDimension(width,height)最终决定并告诉系统测量值的大小
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//前方高能预警:
// View的getDefaultSize方法,在当测量模式为UNSPECIFIED时,返回的就是上面getSuggestedMinimumWidth/Height()方法里的大小,对我们正常逻辑没什么影响。
// 重点是在AT_MOST和EXACTLY两种情况下。getDefaultSize十分简单粗暴,直接返回了specSize,
// 也就是View父布局提供的测量规格里的测量尺寸,然后这就被setMeasuredDimension(width,height)。
// 1.如果当前view是精确模式,MeasureSpec.EXACTLY,也就是xml中指定了尺寸,则直接返回了specSize,
// 这没问题,父布局在检测到该view是精确尺寸的时候,就直接告诉view,你的尺寸就按照被指定的精确尺寸大小设置,specSize就是这个精确尺寸值。
// 2.如果当前view是MatchParent模式,那么父布局给出的尺寸就是当前view可用的最大尺寸,并且MeasureSpec.AT_MOST,当前view直接使用这个尺寸就行。
// 3.如果当前view是WrapContent模式,那么父布局给出的尺寸就是当前view可用的最大尺寸,并且MeasureSpec.AT_MOST,当前如果直接用这个尺寸,
// 就会造成wrap_content时与match_parent的效果一样。所以要在onMesure方法中额外处理WrapContent情况,计算出具体期望大小,
// 然后再调用setMeasuredDimension(width,height)
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = android.view.View.MeasureSpec.getMode(measureSpec);
int specSize = android.view.View.MeasureSpec.getSize(measureSpec);
switch (specMode) {
case android.view.View.MeasureSpec.UNSPECIFIED:
result = size;
break;
case android.view.View.MeasureSpec.AT_MOST://这里应该分match_parent和wrap_content两种情况决定返回值,
//但是系统简单粗暴的没处理,直接返回父布局给出建议的最大尺寸
case android.view.View.MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
//view的背景尺寸和测量指定的最小尺寸,之一
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//二、layout过程
//layout方法首先会调用setFrame方法来给View的四个顶点属性赋值,即mLeft,mRight,mTop,mBottom四个值,
// 此时这个View的位置就确定了。同时我们也就能通过调用getWidth()和getHeight()方法来获取View的实际宽高了。
// 然后,onLayout方法才会被调用,
// 在View类里的onLayout方法是个空方法,而在ViewGroup方法里声明成了抽象方法,所以继承ViewGroup的类都得自己去实现自己定位子元素的逻辑。
// 最后,在layout方法的最后有一个OnLayoutChangeListener的集合,这是View位置发生改变时的回调接口。所以可以通过addOnLayoutChangeListener
// 方法可以监听一个View的位置变化,并做出想要的响应。
public void layout(int l, int t, int r, int b) {
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);
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
}
//默认是空方法,因为view不包含子view,不需要处理
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
//三、draw过程
public void draw(Canvas canvas) {
/*
* 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
if (!dirtyOpaque) {
drawBackground(canvas);//绘制背景
}
// Step 2, save the canvas' layers
//省略一些代码,Step 2不是必须的
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);//绘制子view
// Step 5, draw the fade effect and restore layers
//省略一些代码,Step 5不是必须的
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
}
//View的drawBackground()有默认实现
private void drawBackground(Canvas canvas) {
//省略一堆代码,绘制背景
}
//View的onDraw方法是空方法,绘制自己。一般继承View的,都需要实现onDraw()方法
protected void onDraw(Canvas canvas) {
}
//View的dispatchDraw方法是空方法,绘制孩子。一般继承ViewGroup的,都需要实现dispatchDraw()方法
protected void dispatchDraw(Canvas canvas) {
}
//绘制滚动条等装饰
protected final void onDrawScrollBars(Canvas canvas) {
//省略一堆代码,绘制滚动条等装饰
}
//如果一个view不需要绘制任何内容,那么就请设置这个标志位为true,这样系统会进行相应的优化。View默认未启用,ViewGroup默认启用了。
//当明确知道ViewGroup要来绘制内容时,要关闭WILL_NOT_DRAW这个标志位。
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0,DRAW_MASK);
}
}
以上就是view绘制流程的分析,关于view的重绘有两个方法:invalidate()和requestLayout()。invalidate()没有走measure、layout过程,只走了draw过程;而requestLayout()方法这是整个绘制流程重新走了一遍。关于view重绘流程,具体请参考http://blog.csdn.net/guolin_blog/article/details/17045157。