View 工作原理基础
本章主要介绍View的工作原理,了解了工作原理后才能作出比较完善的自定义View,View需要掌握三大流程:measure、layout和draw,除了三大流程以外,还需要掌握常见的回调方法,比如构造方法、onAttach、onVisibilityChanged、onDetach等。
初识ViewRoot和DecorView
了解ViewRoot和DecorView的概念有助于更好理解三大流程。
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程都是通过ViewRoot来完成的。在ActivityThread中,当Activity被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,过程可参看如下源码:
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);
View的绘制流程从ViewRoot的performTraversals方法开始,然后经过measure,layout和draw三个过程才能将View绘制出来,其中measure测量View的宽高,layout确定view在父容器中的位置,draw负责将View绘制在屏幕上,performTraversals的大致流程图如下:
performTraversals会依次调用performMeasure、performLayout、performDraw,这三个方法分别完成顶级View的measure、layout和draw三大流程。其中在performMeasure中会调用measure方法,在measure方法中又调用onMeasure,onMeasure方法中会对所有子元素进行measure,这个时候measure流程就从父容器传递到子元素了,这样就完成了一次measure,接着子元素会重复父容器的measure过程,如此反复的完成了整个View树的遍历。其他两个同理,唯一不同的是performDraw的传递过程是在draw方法中通过dispatchDraw来实现的,不过这并没有什么本质区别。
measure过程决定View的宽高,Measure完成后可以通过getMeasureWidth和getMeasureHeight来获取View测量后的宽/高,处特殊情况外等同与View最终的宽高。layout过程决定了view的四个顶点的坐标和实际View的宽/高,layout完成后可以通过getTop、getLeft、getRight和getBottom获得四个顶点的位置,并可以通过getWidth和getHeight拿到View最终宽/高。Draw决定了View的显示,只有draw方法完成以后,view才会显示在屏幕上。
如下图所示,DecorView作为顶级View,一般情况下包含一个竖直方向的LinearLayout,分为上下两部分,上面是标题栏,下面是内容栏。在Activity中,通过setContentView设置的布局文件就是放在内容栏中,而内容栏的id则是content,因此Activity指定布局的方法是setContentView。可以通过ViewGroup content = findviewbyid(android.R.id.content)
获取content,通过content.getChildAt(0)
获取设置的View。同时通过源码可知,DecorView是一个FrameLayout,View层事件都先经过DecorView,然后才传递给View。
理解MeasureSpec
为了更好的理解View的测量过程,需要理解MeasureSpec,MeasureSpec参与了View的测量过程,在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高。
MeasureSpec
MeasureSpec代表一个32位int值,高2位代表SpecMode测量模式,低30位代表SpecSize规格大小,下面先看一下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;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,一组SpecMode和specSize可以打包成一个MeasureSpec值,一个MeasureSpec值也可以解包出原始的SpecMode和SpecSize。
SpecMode有三类:
- UNSPECIFIED
父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态 - EXACTLY
父容器已经检测出View所需要的精度大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于LayoutParams中的match_parent和具体的数值 - AT_MOST
父容器指定了一个可用大小即SpecSize,view的大小不能大于这个值,具体是什么值要看不同view的具体实现,它对应于LayoutParams中的wrap_content
MeasureSpec 和 LayoutParams 的对应关系
在view测量的时候,系统会将layoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定view测量后的宽高。顶级view(DecorView)和普通view的MeasureSpec转换过程略有不同,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来决定,对于普通View,其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams来决定。
对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中展示了DecorView的MeasureSpec的创建过程,其中desiredWindowWidth和desiredWindowHeight是屏幕的尺寸:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
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;
}
通过上述代码,DecorView的MeasureSpec的产生过程就很明确了,具体来说其遵守了如下格式,根据LayoutParams的宽高的参数来划分
- LayouParams.MATCH_PARENT:精确模式,大小就是窗口的大小
- LayouParams.WRAP_CONTENT:最大模式,大小不定,但是不能超出屏幕的大小
- 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小
对于普通View来说,这里是指我们布局中的View,View的measure过程由ViewGroup传递而来,先看下ViewGroup的measureChildWithMargins方法:
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);
}
上述的方法会对子元素进行measure,在调用子元素的measure方法之前会通过getChildMeasureSpec方法得到子元素的MesureSpec。子元素的MesureSpec的创建和父容器的MeasureSpec和子元素的LayoutParams有关,此外还和view的margin及padding有关,具体可以看下ViewGroup的getChildMeasureSpec方法
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) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} 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.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} 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:
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 = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 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
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述方法主要作用是根据父容器的MeasureSpec同时结合view本身的LayoutParams来确定子元素的MesureSpec,参数中的padding是指父容器中已占用的空间大小,因此子元素可以用的大小为父容器的尺寸减去pading,具体代码如下:
int specSize = MesureSpec.getSize(spec);
int size = Math.max(0,specSize - pading);
getChildMeasureSpec清楚展示了普通View的MeasureSpec的创建规则,更加清晰的理解getChildMeasureSpec的逻辑,这里提供一个表,表中对getChildMeasureSpec的工作原理进行了梳理:
父容器MeasureSpec 子元素LayoutParams | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
match_parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |