一、View的绘制流程
主要是:测量(measure)、布局(layout)、绘制(draw)三大流程。
1、对于一个普通View(不是容器)
主要是关心测量和绘制两个过程,测量可以确定自身的宽、高、大小,绘制可以显示出view的具体内容(呈现在屏幕上的)。
2、对于ViewGroup(容器控件)主要是关心测量和布局两个过程,测量不仅仅要测量自身还要测量所有的子view,布局主要是指定所有子view在自身上的位置。
3、具体实现是重写onMeasure、onLayout、onDraw方法,在这些方法中进行编码处理。
二、View的测量
1、view的测量主要是由自身的MeasureSpec决定的,而自身的MeasureSpec又由父容器的MeasureSpec和自身的LayoutParams决定的。
2、MeasureSpec包含SpecMode和SpecSize两部分,SpecMode是测量规则,SpecSize是在一定规则下的测量大小。
3、SpecMode分为三种模式
a、UNAPECIFIED:父容器对view没有任何限制,要多大给多大,该模式多为系统自己使用,自定义一般不考虑该模式。
b、EXACTLY:父容器已经知道view所需要的精确大小(SpecSize)。
c、AT_MOST:父容器给了一个最大值(SpecSize)。
当父容器的SpecMode为EXACTLY时:
如果view的LayoutParams为match_parent和具体的值,父容器会为其指定为EXACTLY模式。
如果view的LayoutParams为wrap_content,父容器会为其指定为AT_MOST模式。
当父容器的SpecMode为AT_MOST时:
如果view的LayoutParams为具体的数值,父容器会为其指定为EXACTLY模式。
如果view的LayoutParams为match_parent,父容器会为其指定为AT_MOST模式。
如果view的LayoutParams为wrap_content,父容器会为其指定为AT_MOST模式。
父view在测量子view之前会先调用getChildMeasureSpec方法确定子view的MeasureSpec,下面看getChildMeasureSpec的源码:
getChildMeasureSpec的源码:
//确定子view的MeasureSpec的具体方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父view自己的模式和大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//父view的大小减去padding值,就是现在子view可用的空间大小
int size = Math.max(0, specSize - padding);
//这个变量存的是最终子view测量大小
int resultSize = 0;
//这个变量存的是最终子view的测量模式,如果这个子view是viewGroup的话,那么这个测量模式是要给子view的下级子view使用的,就这样一层一层的递归
int resultMode = 0;
//对于viewGroup来说,resultSize和resultMode除了用于确定自身大小外,还要传给下级子view
switch (specMode) {
//父容器的模式是EXACTLY,说明父容器的大小是精确值(父容器的大小已经确定了)
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//子view的LayoutParams的值是具体的值,比如100dp,那么子view的大小就用这个100dp,子view的模式也是精确值模式
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
//子view的LayoutParams的值是MATCH_PARENT,父view是精确值,所以子view的大小就是父view的可用大小(也是精确值),子view的模式也是精确值模式
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.
//子view的LayoutParams的值是WRAP_CONTENT,父view是精确值,但是子view自己的大小是不确定的(最大为父view的可用size),所以子view的模式是最大模 式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//父容器的模式是AT_MOST,说明父容器的大小是不确定的(父容器的大小是一个最大值,这个最大值是父容器的上层父容器给的)
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
//子view的LayoutParams的值是具体的值,比如100dp,那么子view的大小就用这个100dp,子view的模式也是精确值模式
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.
//子view的LayoutParams的值是MATCH_PARENT,那么子view的大小就是父view的可用大小,而父view的模式是AT_MOST,说明父view的大小是不确定的
//,所以子view的大小也是不确定的,子view的模式是AT_MOST模式
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.
//子view的LayoutParams的值是WRAP_CONTENT,父view的大小不确定,子view自身的大小也不确定,所以子view的模式是AT_MOST模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//这种模式一般是系统自己用的,自定义控件一般不考虑这种情况
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;
}
//最终根据resultSize和resultMode生成子view的MeasureSpec
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
综上所述:如果view的SpecMode为EXACTLY时,其大小是确定的,不需要做特殊处理。如果view的SpecMode为AT_MOST时,其大小是不确定的,所以在测量时需要视 情况设置一个默认值,否则wrap_content是无效的、不显示内容的。
4、 示例代码实现如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
//获取父容器为其指定的测量模式和测量尺寸
int widthSpecMode =MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize =MeasureSpec.getSize(widthMeasureSpec);
int hightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hightSpecSize =MeasureSpec.getSize(heightMeasureSpec);
//宽或者高的SpecMode是AT_MOST时就设置一个默认值,如果不是就用SpecSize
if (widthSpecMode ==MeasureSpec.AT_MOST && hightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(500,500);
} else if (widthSpecMode ==MeasureSpec.AT_MOST) {
setMeasuredDimension(500,hightSpecSize);
} else {
setMeasuredDimension(widthSpecSize,500);
}
}
三、View的绘制
1、view的绘制是通过重写onDraw方法实现的,可以在onDraw里使用canvas、paint绘制图形来实现自定义效果。
2、需要注意的是在绘制的时候需要考虑padding的影响,如果不做处理padding会无效,因为padding是跟view本身有关的。不用关心margin,因为margin是跟父容器相关的,跟view自身无关。
3、为了可以方便的在xml中改变效果,还需要对外提供自定义属性。
四、ViewGroup的测量和布局
1、viewGroup的onMeasure方法中,既要测量自身的大小,又要测量子view的大小,测量子view的大小可以使用measureChildren测量所有子view,也可以自己写for循环遍 历测量子view,调用的方法是measureChild和measureChildWithMargins(这两个方法是测量单个子view的)。下面对这些方法的源码进行分析:
measureChildren的源码:
//测量所有子view
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
//循环测量子view
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//执行测量子view的方法
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChild的源码:
//具体测量一个子view的方法
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//获取到子view的参数LayoutParams,后面会用
final LayoutParams lp = child.getLayoutParams();
//根据LayoutParams里面设置的宽的match_parent或者wrap_content(即lp.width),在结合父view的MeasureSpec来确定子view的宽的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
//根据LayoutParams里面设置的高的match_parent或者wrap_content(即lp.height),在结合父view的MeasureSpec来确定子view的高的MeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//上面的几行代码是确定子view的MeasureSpec的,这行代码是真正进行子view测量的,将上面确定下来的子view的MeasureSpec传给子view。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
2、 onMeasure示例代码实现如下:
/**
* 模拟水平方向可滑动的LinearLayout的测量过程,这里不考虑padding和margin的影响
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取父容器为其指定的测量模式和测量尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int hightMode = MeasureSpec.getMode(heightMeasureSpec);
int hightSize = MeasureSpec.getSize(heightMeasureSpec);
//测量所有子view的宽和高
measureChildren(widthMeasureSpec, heightMeasureSpec);
//测量自身的宽和高
int measureWidth = 0;
int measureHight = 0;
int childCount = getChildCount();
if (childCount != 0) {
//计算出由子view决定的宽度
for (int i = 0; i < childCount;i++) {
measureWidth +=getChildAt(i).getMeasuredWidth();
}
//计算出由子view决定的高度(选取子view中高度最大值为其测量高度)
for (int j = 0; j < childCount;j++) {
if(getChildAt(j).getMeasuredHeight() > measureHight) {
measureHight =getChildAt(j).getMeasuredHeight();
}
}
}
if (widthMode == MeasureSpec.AT_MOST && hightMode ==MeasureSpec.AT_MOST) {
setMeasuredDimension(measureWidth,measureHight);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(measureWidth,hightSize);
} else if (hightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize,measureHight);
}
}
3、容器控件的布局,主要是指定每一个子view在自身上的位置,重写onLayout方法,代码实现如下:
/**
* 对子view进行布局,这里不考虑padding和margin的影响
*
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right,int bottom) {
//每个子view的左起点
int childLeft = 0;
//子view的个数
int childCount = getChildCount();
//为每个子view指定位置
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.layout(childLeft, 0,childLeft + childView.getMeasuredWidth(), childView.getMeasuredHeight());
childLeft +=childView.getMeasuredWidth();
}
}
五、在现有控件的基础上进行自定义
上面所说的自定义控件,都是直接继承View或者ViewGroup的,实际开发中有很多需求是不需要重头自己定义一个控件的,可以继承一个现有控件,去重写其特定的某一个方法来扩展功能。具体用哪种方式去实现,要具体情况具体分析了,如何选取一种最适合的自定义方式,是值得思考的,也是一个难点。