问题概述
一个View想要展示到界面上要经过三个流程:
- measure过程,也就是确定View的长和宽
- layout过程,确定View的位置
- draw过程,将View绘制到界面上
本文首先介绍measure流程,measure从其字面意思就是测量的意思.也就是获取View的尺寸.
安卓的测量分成两种,一种是View;另一种是ViewGroup。ViewGroup可以认为是布局,其实安卓中的五大布局就是继承了这个类,View就是一般的控件,里面不包含其他View.
因此可以把测量的过程看成从ViewGroup开始,遍历其中的所有的子View(包含ViewGroup),对其一一进行测量.
ViewGroup的测量过程
先大概看一下ViewGroup测量的调用流程:
measure–>onMeasure–>measureChildren–>measureChild–>getChildMeasureSpec–>child.measure
首先要注意ViewGroup其实是View的一个子类,measure和onMeasure方法是在View里面实现的,ViewGroup中并没有重写;另外一般自定义ViewGroup时重写onMeasure和onLayout方法,因此调用流程会和上面不一样,但是基本思路是一样,而且在重写的onMeasure方法中一般都会用到getChildMeasureSpec这个方法,所以先看一下默认的ViewGroup测量过程:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
上面一段代码的大意就是对每一个子View进行测量。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
//获取child的横向尺寸,把相关的padding值传进去,padding值代表父View的内间距
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//调取子View的具体测量尺寸
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
首先对上面的”获取child的横向尺寸“意思是获取child的”可能尺寸“,也就是一种测量,不是最后确认的尺寸,是根据父View的大小和限制条件得出子View可能的尺寸。
measure得出的是一个int整数,高两位代表测量模式,低30位代表测量的尺寸.MeasureSpec这个类对获取尺寸和测量模式方法进行了封装:
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
接下来就要通过getChildMeasureSpec这个方法获取子View的测量
getChildMeasureSpec这个方法很重要,理解其代码对安卓布局有很大的意义,代码如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取测量的模式和尺寸
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//padding代表父View的内间距,因此子View的测量要再次基础上
int size = Math.max(0, specSize - padding);
//保存根据计算得到的最后的测量模式和尺寸
int resultSize = 0;
int resultMode = 0;
//子View的测量结果和父View的测量模式和尺寸都有关系
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面代码的基本意思就是根据父View的测量模式和子view的测量模式得出子View的宽高,其对应关系如下表:
因此ViewGroup的测量过程就是首先获取所有子View的测量,然后实现ViewGroup自身的测量过程。
View的测量过程
对于View来说,其measure过程是measure—>onMeasure–>setMeasuredDimension
其中setMeasuredDimension在View中的实现过程就是简单的将传入的参数赋值给View的成员变量,也就是确定了大小.
一般在自定义View时,只需要重写View的onMeasure方法,加上自己定义的measure过程,最后调用setMeasuredDimension即可.
自定义实现View
首先要明确在自定义View的时候要针对wrap_content的情况做特殊处理,因为在上面的表格中,子View为match_parent和wrap_parent时候的size都是一样的,只是测量模式不同,这样的话,不做处理的话会和match_parent属性造成一样的结果.看了安卓源码的TextView或者其他的View都对wrap_parent做了特殊处理,处理的思路是这样的,父View给了子View一个测量,这个测量模式是AT_MOST,大小为size,父View给了多大并不代表子View就要用这么大,只要不超过父View给定的尺寸就行,因此子View会先获取内容的大小contentSize,然后和size比较,如果contentSize小于或等于size,则把子View的尺寸定为contentSize,按照这样的思路我们实现一个自定义的View.
当自定义View为wrap_content属性时,我们定义一个默认大小DEFAULT_SIZE,使得View的尺寸为DEFAULT_SIZE。
代码如下:
public class MyView extends View {
private int DEFAULT_SIZE=100;
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
private int measureWidth(int widthMeasureSpec) {
return widthMeasureSpec;
}
private int measureHeight(int heightMeasureSpec) {
int resultSize = 0;
int specMode = MeasureSpec.getMode(heightMeasureSpec);
int specSize = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
resultSize = specSize;
} else if (specMode == MeasureSpec.AT_MOST) {
//把当前View的尺寸定为DEFAULT_SIZE和父View限制尺寸的最小值
resultSize = DEFAULT_SIZE;
resultSize = Math.min(resultSize, specSize);
}
return resultSize;
}
}