前言:这是在下第一次写android源码的文章,写的不好,还请多多指正。看了一些分析源码的文章,总结出看android源码的一点经验,我奉行的是拿来主义,即不知道的属性或者方法,不用去关注他内部的具体实现逻辑,只需要这是干什么的,直接拿来用,注重流程的梳理,不去计较一点一滴的得失,切记不能只见树木,不见森林。
基础知识
Android系统的整个view采用了组合模式,所有的view都直接或者间接的继承子View类,构成了整个view体系的树形结构,在ViewGroup内部维护了一个hashSet用于管理子视图。
懒得去画,这里借用一下别人的图片,还请见谅!图片来源
1、简单介绍一下几个基本概念:
- 窗口(非Window类):这是一个纯语义的说法,即所看到的屏幕上的独立的界面,比如一个带有TitleBar的Acticity、一个对话框、一个menu菜单等,这些都称为窗口。而从WmS的角度来讲,窗口是接收用户信息的最小单元,WmS内部用特定的类表示窗口,而给WmS添加一个窗口调用的是WindowManager类的add View(),也就是说从WmS的角度来讲,添加一个窗口就是添加一个view对象,不管这个对象是来自于Activity还是自定义View,WmS接收到消息后,首先判断这个消息来自于哪个窗口,即哪个View对象,然后通过IPC调用,把这个消息传递给客户端的ViewRoot.W类。
- Window类:该类在android.view包中,是一个abstract类,该类抽象了“客户端窗口”的基本操作,并且定义了一组Callback接口,Activity就是通过实现这个Callback接口以获得对消息处理的机会,因为消息最初是有WmS传递给View对象的。
- ViewRootImpl类:没有找到ViewRoot类,只找到了ViewRootImpl类,估计被取代了,客户端申请创建窗口时需要一个客户端代理,用以和WmS进行交互,ViewRoot内部类W就是完成P这个功能的。WmS所管理的每一个窗口都会对应一个ViewRootImpl类。简单来说ViewRootImpl就是DecorView和Window链接的桥梁。View的绘制都需要通过ViewRootImpl来完成,在ActivityThread中,会同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);
- W类:该类是ViewRoot类的一个内部类,继承自Binder,用于向WmS提供一个IPC接口,从而让WmS控制窗口客户端的行为。
- Activity中有一个成员为Window,其实例化对象为PhoneWindow,PhoneWindow为抽象Window类的实现类。
- PhoneWindow类:该类继承自Window,同时PhoneWindow内部包含类一个DecorView对象,DecorView继承自FrameLayout,因此,PhoneWindow内含了一个View对象,并且提供了一组通用窗口操作API。
- DecorView类:该类继承自FrameLayout,并且是PhoneWindow的内部类,是所有应用窗口的根View,Decor是Decoration的缩写,因此DecorView就是对普通的FrameLayout进行一定的修饰,如添加一个通用的TitleBar,并响应特定的按键消息等 。
依据面向对象从抽象到具体我们可以类比上面关系就像如下:
Window是屏幕,是抽象的概念,PhoneWindow是一块手机电子屏,DecorView就是显示内容的区域,Activity就是手机电子屏安装位置,淡然也可以安装在其他地方,如dialog。
2、理解MeasureSpec
MeasureSpec我理解为测量规则,在view的measure过程中非常重要。MeasureSpec是view的内部类,是一个32位的int值,高两位为SpecMode,表示测量模式;低30位为SpecSize,表示在某种测量模式下的规格大小。
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);
}
}
@MeasureSpecMode
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有三种类型:
UNSPECIFIED
父容器不对View有任何限制,要多大就给多大,这种情况一般用于系统内部,表示一种测量状态。
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize的值。它对应于LayoutParams中的match_parent和具体数值两种模式。
AT_MOST
父容器指了一个可用大小即SpecSize,View的大小不能大于这个值,具体什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
3、MeasureSpec和LayoutParams的对应关系
在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureParams,然后再根据这个MeasureSpec来确定View测量后的宽/高。
对于DecorView,它的MeasureSpec由窗口的尺寸和自身LayoutParams共同确定;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。
对于DecorView,在ViewRootImpl类中的measureHierarchy方法中展示了DecorView的MeasureSpec的创建过程。
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
其中desiredWindowWidth和desiredWindowHeight表示屏幕的宽/高。再进入到getRootMeasureSpec方法中:
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize The available width or height of the window
*
* @param rootDimension The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
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.MATCH_PARENT:精确模式,大小就是窗口的大小。
- LayoutParams.WRAP_CONTENT:最大模式,大小不确定,但是不能超过窗口的大小。
- 固定大小(具体的数值):精确模式,大小为LayoutParams中指定的大小。
对于普通的View,View的measure过程是由ViewGroup传递而来,在ViewGroup类中的measureChildWithMargins方法:
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
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);
}
此方法对子View进行测量,先调用getChildMeasureSpec方法计算子View的MeasureSpec,然后在调用measure方法进行测量,最后完成对所有子View的遍历。
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//父容器的size减去父容器的padding,得到子View的size
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);
}
通过此方法,可以很清楚的知道子View的计算流程,是通过父容器的MeasureSpec和View自身的LayoutParams来共同决定的,他们之间的对应关系就是switch里面的内容,这里就上面的关系做一个总结:
从上面的表格可以知道,子View的MeasureSpec计算过程,其中UNSPECIFIED模式主要用于系统内部对此测量的情景,一般不会去关注。
### 总结一下
这里主要介绍了View绘制过程中所涉及到一些基本概念和一些关键类的作用及其所扮演的角色,着重理解MeasureSpec的作用和与LayoutParams的关系。这篇文章就讲这么多,下篇文章将会重点分析View的整个绘制流程和关键点,理解View绘制的流程对编写自定义控件有很大帮助。