1.初识ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的,在ActivityThread中,当ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
View的绘制过程就是从ViewRoot的performTraversals方法开始的,它经过measure、layout、draw三个过程才能最终将一个View绘制出来:
1.measure用来测量View的宽和高。
2.layout用来确定View在父容器中放置的位置。
3.draw用来将view绘制在屏幕上。
大致的流程如下图所示:
如图所示,performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout、draw这个三个流程。其中:
1.perfromMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父元素的measure过程,如此反复就完成整个View树的遍历。
2.performLayout的传递流程和performMeasure是一样的。
3.performDraw的传递过程是在draw方法中通过dispathDraw来实现的,本质上并没有区别。
Measure过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽高,在几乎所有的情况下它都等于View的最终宽高,这仅仅是在代码规范的前提之下。
layout最终决定了View的四个顶点的坐标和实际View的宽/高,完成以后,可以通过getTop、getBottom、getLeft、getRight来拿到View的四个顶点坐标位置,并可以通过getWidth和getHeight来得到View的最终宽高
draw过程决定了View的显示,只有draw方法完成以后View的内容才会最终显示在屏幕上。
下面我们来认识一下DecorView,这个是Activity的顶级View。
大家应该还记得我们在Activity中加载布局使用的方法吧!setContentView,那个contentview就是我们这个DecroView中的子布局,从之前的博文View的事件分发中,我们也可以了解到View的点击事件都要先经过DecorView,然后再传递给我们的View。
1.2 理解MeasureSpec
MeasureSpec的中文意思是测量规格的意思,MeasureSpec代表一个32的int值,高2位代表SpecMode测量模式,低30位代表SpecSize测量规格大小。
经常使用的三个函数:
1.public static int makeMeasureSpec(int size,int mode)
构造一个MeasureSpec
2.public static int getMode(int measureSpec)
获取MeasureSpec的测量模式
3.public static int getSize(int measureSpec)
获取MeasureSpec的测量大小
SpecMode分为三类,每一类都没有他们对应的约束。
1.UNSPECIFIED
父容器不对View有任何限制,要多大就给多大,这种情况,一般我们自定义View用不到。
2.Exactly
这个表示准确值,这个对应着LayoutParams中的matchparent和准确值,这个时候View的最终大小就是SpecSize所指定的数。
3.AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于该值,具体是什么要看View中自己的处理,所有自定义View时,我们需要自己在measure里面处理设置布局的大小,它对应layoutparams中的wrap_content
2.1 MeasureSpec和LayoutParams的关系
在上面系统中,使用MeasureSpec来进行View的测量,但是正常情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置layoutparams,在View测量的时候,系统会将LayoutParams在父容器的约束下自动转化成对应的MeasureSpec,然后再根据MeasureSpec来确定View最终的宽高。
MeasureSpec不是由LayoutParams唯一决定的,子View的宽高由自身的layoutparams和父容器的MeasureSpec。
对于DecorView,其MeasureSpec由窗口的尺寸和自身的Layoutparams决定;
对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同决定;
遵循的规则:
1.LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小
2.LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小
3.固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小
**4.当view采用固定的宽高时,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams的大小
5.当View的宽高时matchparent时,如果父容器是精确模式,那么View也是精确模式,并且View也是精确模式并且大小是父容器的剩余空间。如果父容器是最大模式,那么View也是最大模式但是大小不能超过剩余的空间。
6.当View是wrap_content,那么不管父容器的模式,View一定是最大模式,但是不能超过父容器的剩余空间。**
3. View的工作流程
View的工作流程主要是指measure、layout、draw这三个流程,即测量、布局、绘制,其中measure确定View的测量宽高,layout确定View的最终宽高和上下左右的位置,draw将View绘制到屏幕上。
3.1 measure操作
measure过程要分情况来看:
1.针对View的measure过程
View的measure过程由其meaure方法完成,measure方法是一个final类型的方法,子类不可以重写此方法,因为View的measure内部会调用onMeasure方法,我们来看一下View的onMeasure的实现即可。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
里面就是将getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)获取到的宽和高赋给View。
我们再来看一看getDefalutSize的实现。
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;
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
我们来分析一下getDefaultSize,如果当前的模式为测量模式为UnSpecified,那么View的宽高就是传递过来Size的大小,如果测量模式为AT_MOST或者EXACTLY,那么View的宽高就是SpecSize的大小。也就是说View的内部将matchparent和wrapcontent是一样处理的,没有区别看来,这就是我们自定义View时,要自己处理wrapcontent的原因。
结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身的大小,否则在布局中使用wrap_content就相当于使用match_parent。matchparent使用的是父View剩余的控件。
解决方法:重写View的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 你自己计算出来的宽;
int measureHeight = 你自己计算出来的高;
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightSpaceMode == MeasureSpec.AT_MOST && widthSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(measuredWidth, measureHeight);
} else if (heightSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpaceSize, measureHeight);
} else if (widthSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
}
还是so easy的,只需要判断measureSpec的mode是不是AT_MOST,如果是就我们自己计算好的值赋给View。
2.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方法进行测量。
这样差不多View中measure过程就差不多了,总结一下:
1.View的onmeasure在measure中,onmeasure中处理wrap_content和padding。
2.ViewGroup的onmeasure在measure中,viewgroup除了要测量自身的宽高,还要通过measureChild测量子View的宽高,只有里面子View的宽高确定了,才好计算Viewgroup的宽高。
3.注意MeasureSpec和layoutParams的对应关系。
3.2 layout操作
layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用Layout方法,在layout方法中onLayout方法又会被调用。这样一层一层的完成所有子View的布局过程。
layout里面通过setFrame来确定View的四个顶点的位置,然后再layout中调用onLayout,这个方法用来确定子View的位置,起到一层层传递的作用。
3.3 draw操作
draw的过程也比较简单,它的作用是将View绘制到屏幕上面。View的绘制过程遵循下面:
1.绘制背景 (drawBackground)
2.绘制自己 (onDraw(canvas))
3.绘制children ( dispatchDraw(canvas))
4.绘制装饰 onDrawForeground(canvas);
@CallSuper
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;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// 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
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
// 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);
}
View的绘制过程的传递是通过dispatchDraw实现的,dispatchdraw会遍历调用所有子元素的draw方法。如此draw事件就一层一层的传递下去。
好,大结局。学习好上面的所有知识,我们就要打开自定义View的大门了啊,Follow Me!!!