为什么需要学习View的绘制流程?Android提供了很多的基本View组件,如TextView、EditText、Button等等,这些组件在日常开发中经常要用到,也满足了大部分的需求。然而,有时候用户的要求很特殊,需要的效果基本组件根本实现不了,这时我们就要自定义View,于是View的绘制流程就派上用场了,不学习View的绘制流程,根本写不出符合需求的View,相反,学好了绘制流程,无论什么样的View你都可以实现,只有需求想不出,没有你写不出的自定义View。
简介
View的绘制流程可分为三大阶段,Measure(测量)、Layout(定位)、Draw(绘制),这三个阶段依次进行。Measure的本质就是确定组件的宽和高,Layout的本质是确定组件4个顶点的位置,Draw的本质就是把测量好的、定好位的组件画出来。为什么是这样的顺序?简单举个例子,画画的时候,下笔之前要先想好要画多大、画在哪里吧,画多大就是测量、画在哪就是定位,画这个动作就是绘制。
那么在是在哪调用这3个流程的呢?这里只需要知道入口是ViewRootImpl的performTraversals方法,再上面就是ActivityThread里的代码了,这里不需深究。performTraversals方法会依次调用performMeasure()、performLayout()、performDraw()来完成整个View树的绘制流程。看看其工作流程图。
注意这里并不是说performXXX()方法是ViewGroup里面的,它突出的重点是流程,比如View的measure阶段对应3个方法,preformMeasure->measure->onMeasure,在ViewGroup的onMeasure方法中又遍历子View调用它的measure流程,这样measure就从父View传到子View,子View重复父View的动作,直到反复遍历整个View树,measure阶段结束了就到layout阶段,以此类推。View经历完3个流程才会显示到界面上。
可以看看ViewRootImpl的部分源码,验证上面的内容。
private void performTraversals() {
// 省略代码,通常是一些条件判断
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 省略代码,通常是一些条件判断
performLayout(lp, mWidth, mHeight);
// 省略代码,通常是一些条件判断
performDraw();
}
performTraversals()方法的代码很长,全部贴出来不实际,可以自己在AndroidStudio看ViewRootImpl的源码进行验证。前提是你的AndroidStudio下载了源码文件。
下面分别详细讲解三大流程。
measure
要理解measure流程,就要先理解MeasureSpec,因为measure流程其实就是不断的计算MeasureSpec,并传递子View,子View再根据传来的MeasureSpec给某些变量赋值。
MeasureSpec
MeasureSpec是一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。可以把MeasureSpec简单理解为一个对象,里面有两个属性SpecMode和SpecSize,但为了节省空间,把它包装成一个32位的int值。
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
//可以理解为创建对象(传入两个属性size和mode)
public static int makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
// 下面对应两个get方法
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
需要注意的是,MeasureSpec确实是一个类,但是measure流程使用的MeasureSpec是通过makeMeasureSpec返回的一个int值,并不是MeasureSpec类。
SpecMode
- UNSPECIFIFD
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部(平时用的基础View和自定义View几乎没有这种情况)。 - EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值。如下面所示。
android:layout_width="match_parent"
android:layout_width="100dp"
- AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应与LayoutParams中的wrap_content。
android:layout_width="wrap_content"
注意:
这里并非说一个View在xml文件定义的属性是match_parent,它的SpecMode就是EXACTLY,还有视父View的SpecMode而决定的。所以,一个View的MeasureSpec是由父View的MeasureSpec和自身的LayoutParams属性所决定的。既然如此,那最顶层View的MeasureSpec是怎样得到的呢?下面进行分析。
上面提到measure流程的入口是performTraversals()方法,在里面依次调用了performXXX()函数开始三大流程,可能细心的人已经发现,performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
已经传入了MeasureSpec供后面的View使用。再看ViewRootImpl源码,查看childWidthMeasureSpec是怎么来的(childHeightMeasureSpec与其类似,一个View通常有两个MeasureSpec,分别代表宽和高)。
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// 省略代码
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
可以看到这两个MeasureSpec是通过getRootMeasureSpec()方法得到的,看方法名就知道,这是获取根部MeasureSpec的方法,即顶层View的MeasureSpec。进入该方法查看。
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