我们都知道,自定义View的三大流程:Measure、Layout、Draw。而学习这三个流程是我们进阶高级UI的毕竟之路,但是在学习在三个流程之前,一些基础知识是必不可少的,否则只会导致我们越学越难。所以在学习Measure流程之前我们需要先弄清楚这个过程中几个最重要的知识点
一、LayoutParams到底是什么
1、LayoutParams的定义
LayoutParams顾名思义,就是布局参数。而且大多数人对此都是司空见惯,我们 XML 文件里面的每一个 View 都会接触到 layout_xxx 这样的属性,这实际上就是对布局参数的描述。大概大家也就清楚了,layout_ 这样开头的东西都不属于 View,而是控制具体显示在哪里。
2、LayoutParams的组成
LayoutParams的组成是哪些呢?我们先来看看LayoutParams的源码:
//负责给 XML 处理
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
//直接让用户指定宽高
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
//供addView这样的方法添加子控件使用
public LayoutParams(LayoutParams source) {
this.width = source.width;
this.height = source.height;
}
//供MarginLayoutParams处理
LayoutParams() { }
实际上,ViewGroup 的子类的 LayoutParams 类拥有更多的构造方法,在这里我想更加强调一下我上面提到的 MarginLayoutParams。MarginLayoutParams 继承于 ViewGroup.LayoutParams
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
@ViewDebug.ExportedProperty(category = "layout")
public int leftMargin;
@ViewDebug.ExportedProperty(category = "layout")
public int topMargin;
@ViewDebug.ExportedProperty(category = "layout")
public int rightMargin;
@ViewDebug.ExportedProperty(category = "layout")
public int bottomMargin;
@ViewDebug.ExportedProperty(category = "layout")
private int startMargin = DEFAULT_MARGIN_RELATIVE;
@ViewDebug.ExportedProperty(category = "layout")
private int endMargin = DEFAULT_MARGIN_RELATIVE;
public MarginLayoutParams(Context c, AttributeSet attrs) {
super();
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
setBaseAttributes(a,
R.styleable.ViewGroup_MarginLayout_layout_width,
R.styleable.ViewGroup_MarginLayout_layout_height);
int margin = a.getDimensionPixelSize(
com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
if (margin >= 0) {
/*看到这里大家就应该明白了,为什么我们在XML里面设置layout_margin
*属性的值会覆盖 layout_marginLeft 与 layout_marginRight 等属性的值。
*/
leftMargin = margin;
topMargin = margin;
rightMargin= margin;
bottomMargin = margin;
} else {
int horizontalMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
int verticalMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);
if (horizontalMargin >= 0) {
leftMargin = horizontalMargin;
rightMargin = horizontalMargin;
} else {
leftMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
UNDEFINED_MARGIN);
if (leftMargin == UNDEFINED_MARGIN) {
mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
leftMargin = DEFAULT_MARGIN_RESOLVED;
}
rightMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginRight,
UNDEFINED_MARGIN);
if (rightMargin == UNDEFINED_MARGIN) {
mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
rightMargin = DEFAULT_MARGIN_RESOLVED;
}
}
startMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginStart,
DEFAULT_MARGIN_RELATIVE);
endMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
DEFAULT_MARGIN_RELATIVE);
if (verticalMargin >= 0) {
topMargin = verticalMargin;
bottomMargin = verticalMargin;
} else {
topMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginTop,
DEFAULT_MARGIN_RESOLVED);
bottomMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginBottom,
DEFAULT_MARGIN_RESOLVED);
}
......
}
}
看了源码LayoutParams的组成也就一目了然了。with、height 是决定View大小的根本。而MarginLayoutParams 中的margin与padding决定View的内外边距,同样会影响View的最终显示大小。
3、LayoutParams是如何与View建立联系的
LayoutParams建立联系的方式可分为两种:
- XML中定义View
- 通过代码addView添加控件
XML中定义View设置LayoutParams属性我们每天都在使用,而这种通过XML设置的属性,最终会通过LayoutInflater的inflate方法进行xml解析,源码分析如下:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
省略....
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
root.addView(temp, params);
}
省略...
}
}
return result;
}
}
会先调用父窗口的generatLayoutParams方法获取MarginLayoutParams ,然后再addView的时候 把LayoutParams传进去。
代码addView添加控件,少了解析xml的流程,我们看看 addView() 都做了什么。
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
//获取View的布局参数LayoutParams
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
if (mOrientation == HORIZONTAL) {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
} else if (mOrientation == VERTICAL) {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
return null;
}
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
// ...
if (!checkLayoutParams(params)) {//判断布局参数有效性
params = generateLayoutParams(params);// 如果传入的LayoutParams不合法,将进行转化操作
}
if (preventRequestLayout) { // 是否需要阻止重新执行布局流程
child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw)
} else {
child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw)
}
// ...
}
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p != null;
}
二、MeasureSpec类到底是什么
1、MeasureSpec的定义
Android中MeasureSpec的正确定义是:测量规格类。可以将他理解为是测量View大小的一个依据。它的作用是决定一个View的的大小(宽/高),当然了,决定一个View的具体大小并不只是依据这一个参数,还依据每个View的LayoutParams参数。这个下面会具体说明。
2、MeasureSpec的组成
- 测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)
- 测量规格(MeasureSpec):32位、int类型
- 测量模式(mode):占MeasureSpec的高2位
- 测量大小(size):占MeasureSpec的低30位
(1)、SpecMode有哪些类型
测量模式(Mode)的类型有3种:
UNSPECIFIED:无限模式,父视图不约束子视图View
EXACTLY:精确模式
AT_MOST:最大模式
具体说明如下:
具体的计算方式,下面我们会根据源码来进行分析。
(2)、为什么要SpecMode+SpecSize的组合这样设计MeasureSpec
MeasureSpec类 用1个变量封装了2个数据(size,mode):通过使用二进制,将测量模式(mode) & 测量大小(size)打包成一个int值来,并提供了打包 & 解包的方法。
该措施的目的 = 减少对象内存分配
3、MeasureSpec的计算
(1)MeasureSpec的使用
// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)
// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
(2)MeasureSpec源码分析
public class MeasureSpec {
// 进位大小 = 2的30次方
// int的大小为32位,所以进位30位 = 使用int的32和31位做标志位
private static final int MODE_SHIFT = 30;
// 运算遮罩:0x3为16进制,10进制为3,二进制为11
// 3向左进位30 = 11 00000000000(11后跟30个0)
// 作用:用1标注需要的值,0标注不要的值。因1与任何数做与运算都得任何数、0与任何数做与运算都得0
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000
// 通过高2位
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000
public static final int EXACTLY = 1 << MODE_SHIFT;
// AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* makeMeasureSpec()方法
* 作用:根据提供的size和mode得到一个详细的测量结果吗,即measureSpec
**/
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
// measureSpec = size + mode;此为二进制的加法 而不是十进制
// 设计目的:使用一个32位的二进制数,其中:32和31位代表测量模式(mode)、后30位代表测量大小(size)
// 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
}
/**
* getMode()方法
* 作用:通过measureSpec获得测量模式(mode)
**/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
// 即:测量模式(mode) = measureSpec & MODE_MASK;
// MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0)
//原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
// 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
}
/**
* getSize方法
* 作用:通过measureSpec获得测量大小size
**/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
// size = measureSpec & ~MODE_MASK;
// 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
}
}
(3)、上面讲了那么久MeasureSpec,那么MeasureSpec值到底是如何计算得来?
答:子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,即:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定。如下图:
具体计算逻辑封装在getChildMeasureSpec()里,如下源码分析:
/**
* 源码分析:getChildMeasureSpec()
* 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
* 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
**/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//参数说明
* @param spec 父view的详细测量值(MeasureSpec)
* @param padding view当前尺寸的的内边距和外边距(padding,margin)
* @param childDimension 子视图的布局参数(宽/高)
//父view的测量模式
int specMode = MeasureSpec.getMode(spec);
//父view的大小
int specSize = MeasureSpec.getSize(spec);
//通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
int size = Math.max(0, specSize - padding);
//子view想要的实际大小和模式(需要计算)
int resultSize = 0;
int resultMode = 0;
//通过父view的MeasureSpec和子view的LayoutParams确定子view的大小
// 当父view的模式为EXACITY时,父view强加给子view确切的值
//一般是父view设置为match_parent或者固定值的ViewGroup
switch (specMode) {
case MeasureSpec.EXACTLY:
// 当子view的LayoutParams>0,即有确切的值 比如"layout_width" = "100dp"
if (childDimension >= 0) {
//子view大小为子自身所赋的值,模式大小为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
// 当子view的LayoutParams为MATCH_PARENT时(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小为父view大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
// 当子view的LayoutParams为WRAP_CONTENT时(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view决定自己的大小,但最大不能超过父view,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)。 也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
case MeasureSpec.AT_MOST:
//同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小,但同样的,child的尺寸上限也是父控件的尺寸上限size
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
//child想要根据自己逻辑决定大小,那就自己决定呗
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
// 多见于ListView、GridView 等系统控件
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小为子自身所赋的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子View大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子View大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这样看源码好像有点不是太明了,没关系,总结如下:
以父视图为视角:
以子View为视角:
三、View树的绘制流程
1、View树的绘制流程是什么
源码分析:
/**
* 源码分析:ViewRootImpl.performTraversals()
*/
private void performTraversals() {
// 1. 执行measure流程
// 内部会调用performMeasure()
measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);
// 2. 执行layout流程
performLayout(lp, mWidth, mHeight);
// 3. 执行draw流程
performDraw();
}
从上面的performTraversals()
可知:View
的绘制流程从顶级View(DecorView)
的ViewGroup
开始,一层一层从ViewGroup
至子View
遍历测绘,即:自上而下遍历、由父视图到子视图、每一个 ViewGroup
负责测绘它所有的子视图,而最底层的 View 会负责测绘自身,如下图:
而View的绘制流程无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性。
- 绘制的流程 =
measure
过程、layout
过程、draw
过程,具体如下
源码流程分析如下:
2、View的绘制由谁来负责
(1)、measure流程
(2)、layout流程
(3)、draw流程