android开发中了解view的绘制流程至关重要,尤其自定义View,需要重写onMeasure,onLayout,onDraw等方法,那么view的绘制流程到底是怎么样的呢?前一篇文章(Android中view的显示原理之DecorView是如何被添加至Window中以及view绘制流程开始的地方)分析了View绘制的入口是在ViewRootImpl中的performTraversals()方法中,会依次调用performMeasure —> performLayou —>performDraw,那么今天我们继续分析view的整个绘制流程。
View绘制入口是在performTraversals(),核心代码如下:
private void performTraversals() {
***
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//测量
***
performLayout(lp, mWidth, mHeight)//布局
***
performDraw();//绘制
}
可以看到在调用测量方法前先得到了childWidthMeasureSpec 、childHeightMeasureSpec ,通过方法名大概可以看出是子view的宽度和高度的测量规范,那么通过这个方法是怎么得到子view的测量规范的呢?这里不着急往下走了,先来介绍一个特殊的类MeasureSpec。
这个类看类名意思是测量规范,它就决定了View的测量规格。View在测量的时候会涉及到两个概念:一个是模式(SpecMode),一个是尺寸(SpecSize)。这两都被封装进了MeasureSpec中,并通过一个32位的int数值表示,其中前2位表示模式,后30位表示尺寸。通过SpecMode+SpecSize就构成了View的测量规格(即在某种模式下当view的大小)。
三种测量模式
private static final int MODE_SHIFT = 30;
***
//如果父容器没有对子view有任何约束,那么它可以是任意大小
// UNSPECIFIED 模式一般系统内部使用
//对应的二进制为:00000000000000000000000000000000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//父容器检测出子view的大小,子view的大小就是其想要的大小
//对应的二进制为:01000000000000000000000000000000
public static final int EXACTLY = 1 << MODE_SHIFT;
//子view可以任意大小,但是不能超过父容器
//对应二进制为:10000000000000000000000000000000
public static final int AT_MOST = 2 << MODE_SHIFT;
UNSPECIFIED
UNSPECIFIED模式表示父容器没有对子view做任何限制,子view大小可以是任何尺寸,该模式一般是系统内部使用,实际开发中很少用到。
EXACTLY
EXACTLY 模式表示子view的大小为精确值,子view的宽高布局属性值为LayoutPamras match_parent或者固定数值,子view的大小就是SpecSize。
AT_MOST
AT_MOST模式表示当前子view的大小不能超过父容器的指定的大小,子view的宽高布局属性是LayoutPamras wrap_content,子view的大小可以是不超过父容器指定的大小的任意值。
那么这些模式又是怎么确定的呢?相信童鞋肯定有这样的疑问。那么我们来分析分析源码,让你彻底理解。
在调用 performMeasure方法前会先调用getRootMeasureSpec方法生成对应的宽高测量规格。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
//这里传入的第一个参数是windowSize也就是当前窗口宽或者高的大小,第二个参数传入的是一个int类型的数据,是当前子view 的宽或者高布局属性。
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
//如果当前view的宽或者高的布局属性是MATCH_PARENT,也就是填充父容器,
那么它的模式就是EXACTLY,它的大小就是父容器的大小。
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
//如果当前view的宽或者高的布局属性是WRAP_CONTENT,也就是包裹内容,
那么它的模式就是AT_MOST,它的大小最大就是父容器的大小。
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
//这里就表示当前view的宽或者高的布局属性是一个确定的值,它的模式就是EXACTLY,大小就是确定值。
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
getRootMeasureSpec方法分别得到了view的宽和高的测量规格,那么这个view是哪个View呢?通过源码可知mWidth、mHeight分别对应窗口的宽和高,lp.width、lp.height分别对应DecorView的宽和高的布局属性。那么通过MeasureSpec.makeMeasureSpec就得到了DecorView宽和高的测量规格,其中makeMeasureSpec方法就是将SpecMode与SpecSize打包成一个32位的int数值,前2位表示测量模式,后30为表示测量尺寸。
接下来我们就进入期待已久的 performMeasure。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;`在这里插入代码片`
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
这里调用了mView的measure方法,有木有很激动,这就是大家经常说的measure方法。那么这个mView是什么呢?我们来看看。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
***
}
}
}
它在setView中被赋值,在之前分析view绘制流程入口的时候,这个方法传进来的DecorView的实例,这里就将其看作DecorView。我们知道DecorView的数据类型是FrameLayout,先接着看 mView.measure做了什么。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
***
onMeasure(widthMeasureSpec, heightMeasureSpec);
***
}
这里进入了View的measure方法,调用了onMeasure。突然一下似乎明白了点什么,自定义View时经常需要重写的onMeasure方法出现了!DecorView是FrameLayout的子类,那么去看下FrameLayout中的onMeasure方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
***
//遍历子view
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {//测量子view的条件
//开始测量子view 传入的是父控件宽高的测量规格
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
***
}
}
//这个方法很关键,讲完子view的测量,就会豁然明朗了
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
在FrameLayout的onMeasure中遍历了当前容器的子view,并逐个测量。在其他容器中(如:RelativeLayout,LinearLayout等)其测量流程都大致一样,先测量子view,再测量自身大小,这里就拿FrameLayout来说明测量大致流程。
//调用了ViewGroup的measureChildWithMargins方法
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子view的布局属性对象
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//获取子view的宽度测量规格
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//获取子view的高度测量规格
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//开始子view的测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上述代码通过传入的父容器的测量规格和子view的布局属性获取子view的测量规格,并开始子view的测量,子view的测量规格是怎么得到的呢?接着看。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//通过MeasureSpec获取到父容器的SpecMode和SpecSize
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) {
// Parent has imposed an exact size on us
//如果父容器的测量模式是EXACTLY
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {//如果子view的布局属性值大于0,证明其大小为确定的某个值,其测量模式为EXACTLY。
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {//如果子view的布局属性值为MATCH_PARENT,父容器测量模式为EXACTLY(代表着父容器的大小是确定的),那么填充父容器其大小也是确定的。
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {//如果子view的布局属性值为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
//如果父容器的测量模式是AT_MOST
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {如果子view的布局属性值大于0,证明其大小为确定的某个值,其测量模式为EXACTLY。
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {如果子view的布局属性值为MATCH_PARENT,父容器测量模式为MATCH_PARENT(代表着父容器的大小是不确定的),要求子view的大小不能超过父容器。
// 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) {//如果子view的布局属性值为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
//如果父容器的测量模式是UNSPECIFIED
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {//如果子view的布局属性值大于0,证明其大小为确定的某个值,其测量模式为EXACTLY。
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {//子view的布局属性是MATCH_PARENT,但是父容器的大小是不可知的,所以子view的大小也是不可知的。
// 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) {//子view的布局属性是包裹内容,但是其大小是不可知的,有图父容器的大小是不可知的,所以其测量模式只能是UNSPECIFIED。
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
//最后调用了MeasureSpec.makeMeasureSpec,将子view的SpecSize和SpecMode打包,生成对应的测量规格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述代码诠释了子view的测量规格是由父容器的测量规格和子view的布局属性共同决定的,View的测量规格有别于顶层布局视图DecorView的测量规格,这里我们总结一下View的测量规格。
回到measureChildWithMargins方法,在生成了子view的测量规格后,调用了子view的measure(childWidthMeasureSpec, childHeightMeasureSpec)。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
***
//子view的测量又会进入到View的onMeasure方法中
onMeasure(widthMeasureSpec, heightMeasureSpec);
***
}
调用了View的onMeasure方法,接着走下去。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这里调用了setMeasuredDimension方法
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
走到这里真是万分激动啊!自定义View时重写onMeasure方法是不是会调用一个setMeasuredDimension方法!对!就是它啊!它到底做了些什么操作呢?
//在调用setMeasuredDimension前先调用了getDefaultSize,并将当前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:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
***
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
这里调用了setMeasuredDimensionRaw(measuredWidth, measuredHeight)。
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
对测量后的结果进行了保存,并对对应的标志位进行了设置,然后就完了。测量就这么结束了?前面在FrameLayout的onMeasure中提到过setMeasuredDimension方法,这里就可以解释了。在测量完子view后计算总的宽度和高度(也就是测量后的宽高),再调用setMeasuredDimension对值进行保存。
测量完结后会对当前view进行布局,接着看看performLayout(lp, mWidth, mHeight)。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
***
final View host = mView;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
***
}
这里将mView(上面分析了是DecorView)赋值给host,并调用了host的layout,传入测量后的宽高。
//来到View的 layout方法
*@param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
* 注释解释了各个参数额度含义,分对应左,上,右,下相对父容器的位置
public void layout(int l, int t, int r, int b) {
***
setFrame(l, t, r, b);//确定自身相对父容器的位置
onLayout(changed, l, t, r, b);//这个方法不是在自定义View时(继承ViewGroup时,必须重写的方法吗!),
***
}
看看setFrame做了什么。
protected boolean setFrame(int left, int top, int right, int bottom) {
***
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
***
}
对当前view相对于父容器的位置进行了保存并设置。
在View的layout方法中还调用了onLayout(changed, l, t, r, b)方法,进去一探究竟。
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
//注释的大意是:带有子类的派生类应该重写该方法,并调用子类的layout方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
这里onLayout没有实现是因为在View中只需要确定自身View当前在父容器中的位置,而如果是在ViewGroup中,则需要重写该方法。在FrameLayout中看看onLayout方法。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
//调用了子view的layout方法
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
在ViewGroup中会遍历子view,确定每个子view在父容器中的位置。
前面的测量和布局只是确定了View的大小和其在父容器中的位置,绘制就是将当前视图渲染到可见屏幕上,具体是怎么绘制的呢?下面为你揭晓。
private void performDraw() {
***
draw(fullRedrawNeeded)
***
}
private boolean draw(boolean fullRedrawNeeded) {
***
drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)
***
}
performDraw调用了draw(fullRedrawNeeded),draw(fullRedrawNeeded)调用了drawSoftware(surface, mAttachInfo, xOffset, yOffset,scalingRequired, dirty, surfaceInsets)方法。
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
***
mView.draw(canvas);//这里mView分析的是DecorView,这里只是分析View的绘制(mView不一定是DecorView,这里只是拿DecorView来分析)
***
}
上述 drawSoftware中调用了View的draw(canvas)方法。
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
上述注释大意是:在指定的画布上渲染当前View及其子View,在执行渲染前当前View
必须已经完成了完整的布局。当实现一个View时,要重写onDraw方法而不是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 //绘制View的内容
* 4. Draw children //绘制子View
* 5. If necessary, draw the fading edges and restore layers //如果有必要,绘制渐暗边缘以及图层的恢复
* 6. Draw decorations (scrollbars for instance)//绘制装饰(例如滚动条)
*/
// Step 1, draw the background, if needed
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
***
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
}
通过查看源码得知View的绘制分为七个步骤:
其中onDraw方法,在不同的View中绘制需求各有不同,绘制出来的内容当然也是多种多样,在自定义View(继承View)时必须实现该方法。
dispatchDraw是对子View进行绘制,这里我们看看ViewGroup中dispatchDraw的实现是怎样的呢?
@Override
protected void dispatchDraw(Canvas canvas) {
***
for (int i = 0; i < childrenCount; i++) {
***
drawChild(canvas, transientChild, drawingTime);
}
***
}
在ViewGroup的dispatchDraw方法中调用了drawChild,顾名思义,对子View进行绘制。
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChild又调用了View的draw方法,这里再给出一张draw流程的图。
至此,整个View的绘制流程就结束了。各位童鞋你明白了吗?
由于个人水平有限,文章难免出现错误,如有错误,欢迎留言,一定及时修正。