初识ViewRoot和DecorView
低版本——2.3中是ViewRoot,高版本——4.0以上是ViewRootImpl,看名字感觉跟是View的root,实际跟View没有这种关系,View树的根是DecorView,DecorView又是在PhoneWindow中的。
WindowManagerImpl
中的addView()
中有一段这样的代码root.setView(view, wparams, panelParentView);
,root就是ViewRootImpl,所以ViewRootImpl其实是view树的管理者,他把DecorView和WindowManager联系起来,它用来管理View树
ViewRootImpl中的performTraversals
方法是View绘制的起始点(View的绘制流程是measure、layout、draw)
在performTraversals
中:
- 先调用
performMeasure
然后内部调用,mView.measure
,内部调用onMeasure
(5.0、4.0版本中略有差异)
-
performLayout
同上 -
performDraw
同上,不过通过dispatchDraw
分发draw而不是onDraw(onDraw是画自己,dispatchDraw是画孩子)
关于view的三个流程:
- measure之后,就可以通过
getMeasureWidth
获取到测量的宽高,一般情况他是view的宽高。(之所以说是一般情况是因为看下面) - layout之后,view的左上右下就固定了,可以通过
getLeft/getRight/getTop/getBottom
获取到这些信息,getWidth
就是根据这个计算的。 - draw之后,view才会可见
DecorView是根View,是FrameLayout,内部通常是Linearlayout,然后里面是titlebar、… 、content,content就是我们setContentView
所设置的View的容器。具体这个结构还和Activity主题有关
理解MeasureSpec
MeasureSpec是一个类,它可以描述成一个32位的二进制数,高二位表示MODE,是测量模式(未知、精确、取最大三种,对应的英文就不说了),低30位是SIZE(数字、包裹内容、匹配父容器,英文也不说了,就是xml中我们常用的三种)
通过各种逻辑与或非操作,可以把两个数字mode和size包装成一个MeasureSpec,还可以把MeasureSpec解包成mode和size,这些操作谷歌已经写好了,直接可以用
一个View,有MeasureSpec,通过MeasureSpec,调用measure()
,然后调用onMeasure()
,在onMeasure中,对于普通的view,直接就可以完成测量,对于ViewGroup,还要测量他的孩子(注意,是测量的宽高,上一小节说了,最终的宽高在layout之后才能确定,不过一般情况是一样的)
那么MeasureSpec是怎么来的呢?MeasureSpec是View自身的LayoutParams结合父亲的一些规则(其实就是父View的MeasureSpec,毕竟父View的MeasureSpec就可以决定父View的测量宽高了,就像这个View的MeasureSpec确定之后,测量宽高就确定了),计算来的,换句话说就是,给一个View设置Layoutparams,不能保证他在任何情况下都这么大,因为还和他的父View有关系。(注意,还有一种特殊情况,DecorView没有父View,所以他的MeasureSpec是自己的Layoutparams和窗口——Window的尺寸决定的,DecorView是在Window中的,之前说过)。具体的DecorView、普通View,他们的MeasureSpec是如何创建的呢,下面就来分析:
-
DecorView的MeasureSpec创建:
DecorView的MeasureSpec是在ViewRootImpl中创建的,在
measureHierarchy
中调用getRootMeasureSpec
创建,看代码可知,这个MeasureSpec是由DecorView的layoutparams和窗口大小决定的
//注:这两个参数分别是窗体宽高、DecorView的layoutparams的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;
}
在measureHierarchy
中创建完成宽高的MeasureSpec之后,就开始执行performMeasure
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//内部调用decorView的measure方法
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
return windowSizeMayChange;
}
-
普通View的MeasureSpec的创建:
DecorView的MeasureSpec是从ViewRootImpl中创建的,然后让DecorView调用measure方法
普通的View是在他的父View中创建MeasureSpec,然后让子View(自己)调用measure方法
和ViewRootImpl中的measureHierarchy类似,在ViewGroup中有个measureChildWithMargins方法,里面通过getChildMeasureSpec创建孩子的MeasureSpec,然后调用child.measure(),整个过程跟ViewRootImpl中的measureHierarchy很相似
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//这里可知,孩子的measureSpec的创建是由父View的measurespec、父view的padding,以及孩子自己的layoutparams(margin、width等)决定的
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
,下面这段代码需要十分熟悉:
//三个参数分别是
//1.父View的measurespec
//2.父View已经占用的尺寸,也就是孩子不能使用的(这个是父View的padding+孩子的margin,看上面那段代码可知)
//3.子view的width(MATCH_PARENT、WARP_CONTENT、具体数值)
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//父容器的可用尺寸(去掉了padding),如果是负的,那就是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:
//孩子的尺寸是具体数值(大于等于0就是具体数值)
if (childDimension >= 0) {
//如下
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//孩子是MATCH_PARENT
} 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.
//孩子是包裹内容,那么孩子的测量模式就是AT_MOST,并且此时size的含义就是孩子最大可能的尺寸,而不是孩子的具体尺寸了
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
//父亲是AT_MOST,说明父亲的尺寸不确定,但是父亲最大不能超过某个数值,这个数值是已知了
case MeasureSpec.AT_MOST:
//孩子是具体数值,那孩子就像下面那样,精确模式、具体尺寸
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//孩子是MATCH_PARENT,那么孩子不是精确的,但是孩子可以确定他最大尺寸,那就是父亲的最大尺寸,模式是AT_MOST
} 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:
//这种情况
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
网上各种表就不看了,上述代码理解了,其他表什么的都是浮云
综上:View的创建过程是在它的父View中的,DecorView是特殊的,没有父View,也不是在PhoneWindow中,而是在ViewRootImpl中,ViewRootImpl是DecorView和WindowManager的纽带,所以他引用了DecorView和WindowManager
View的工作流程
view的工作流程主要指的是:measure、layout、draw,其中measure最为复杂,一个一个来分析
Measure
View的测量分两种情况:View和ViewGroup,对于View测量完就好了。对于ViewGroup,还要测量孩子。
View的测量
由上一节可知,在View的父容器中会为View创建MeasureSpec,然后child.measure(),就进行测量了。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//....
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
measure是一个final,所以没法重写,内部调用了onMeasure()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这个方法就是给View设置测量值,所以着重看参数是如何拿到的getDefaultSize()
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) {
//不考虑UNSPECIFIED
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//所以一般情况下,尺寸就是测量的值,由此可知,如果我们继承自View的一个自定义View,在测量的时候,我们需要重写onMeasure(如果是AT_MOST,那么我们就根据情况,提供一个数值,设置为宽高),因为如果是WARP_CONTENT,那么在获取尺寸的时候,就会拿到specSize,而由于MODE是AT_MOST,所以这个尺寸是父容器中可用的最大尺寸,所以效果跟使用MATCH_PARENT一样了。
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
ViewGroup的测量
ViewGroup本身就是一个View,所以他也一样,measure中调用onMeasure,具体的测量逻辑是在onMeasure中,但是ViewGroup是一个抽象类,他的不同的子类,测量规则都没法统一,所以他没有实现onMeasure,在线性布局、相对布局等中都有实现,稍后分析。
在ViewGroup中有个measureChildren方法,这可以说是一个默认的方法,在我们自己实现onMeasure时可以调用它,也可以不调用(一般测量孩子都调用他),相当于一个模板。看看代码。
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,内部再让孩子去measure,于是就到了View的测量
Layout
layout方法用来决定View自身的位置,在layout中调用了onLayout方法,这个方法没有具体的实现,需要子类自己实现,主要是为了决定子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 = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
//调用onLayout,具体的实现都不一样
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
if (mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) 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 &= ~FORCE_LAYOUT;
}
在Linearlayout中,onLayout中主要就是遍历孩子,然后调用setChildFrame方法,这个方法内部就是调用child的layout方法,所以又回到了上面那一步。
在调用setChildFrame时,会传入宽高,最终到了child.layout,这个宽高就是通过getMeasureWidth获取的,也就是说,onLayout中的宽高就是测量宽高,所以说,一般情况下,测量宽高和最终的宽高一样,但是测量宽高先被赋值。而且测量宽高可能被多次赋值,所以有时候和最终宽高不一样。
另一种情况就是layout参数传进来的宽高没有直接使用,比如如下,也会导致测量和最终不同。
public void layout(int l, int t, int r, int b) {
super.layout(l,t,r+10,b+10)
}
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
// 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);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
// Step 2, save the canvas' layers
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
}
看注释可知,View的draw过程主要有以下几步:
- 画背景
- 画内容
- 画孩子
- 画装饰
draw是通过dispatchDraw将绘画分发给孩子的
有个方法是setWillNotDraw();可以设置当前view不绘制内容,一般继承自ViewGroup,并且确保自身不需要绘制,就设为true,可以优化。默认为false。
自定义View
注意事项
- 直接继承自View或者ViewGroup需要处理padding和wrap_parent
- 尽量不要使用handler,因为view自带post
- 利用onDetachedFromWindow和onAttachedToWindow,维护线程、动画的起止
自定义View实例
继承自View的自定义View
- 在onMeasure中处理wrap_parent
- 在onDraw中处理padding
- 自定义xml属性,文件名字不一定要交attrs。自定义属性获取完数据之后记得调用recycle。
继承自ViewGroup的自定义View
- 在onMeasure中调用measureChildren测量孩子(也可以自己写逻辑),然后分析自己的measurespec,最后调用setMeasuredDimension
- onLayout中根据测量宽高,遍历孩子,为其布局。