自定义控件流程
View 和 ViewGroup
View的工作流程主要是指measure、layout和draw三大流程,即测量、布局和绘制。
View的位置参数
View的位置参数如下图所示:
通过上图,我们可以很方便的了解View的位置参数,View和MotionEvent提供的获取坐标的方法如下表所示:
方法 | 描述 |
---|---|
View的获取坐标的方法: | |
getTop() | 获取View自身的定边到其父布局顶边的距离。 |
getLeft() | 获取View自身的左边到父布局左边的距离。 |
getRight() | 获取View自身的右边到父布局左边的距离。 |
getBottom() | 获取View自身的底边到父布局顶边的距离。 |
MotionEvent获取坐标的方法: | |
getX() | 获取点击事件距离控件左边的距离,即视图坐标。 |
getY() | 获取点击事件距离控件顶边的距离,即视图坐标。 |
getRawX() | 获取点击事件距离整个屏幕左边的距离,即绝对坐标。 |
getRawY() | 获取点击事件距离屏幕顶边的距离,即绝对坐标。 |
View的测量
当我们对View和ViewGroup进行测量时,首先是获取它的宽高信息,获取的方式有如下三种:
方法 | 描述 |
---|---|
getMeasuredWidth() | 对View上的内容进行测量后得到的View内容占据的宽度。 |
getWidth() | View在设定好布局后整个View的宽度,也就是在onLayout之后。 |
getLayoutParams().width | 测量后就确定值,getLayoutParams.width比getMeasureWidth多了margin和padding。 |
所以,在自定义控件时,有时候会获取到宽高信息为0的情况,就要对照上面的表格进行排查。
(1)View的测量
在Measure过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量View的宽高。
MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小),可以通过getSize()和getMode()来获取相应的值。
测量的模式可以分为以下三种:
模式 | 描述 |
---|---|
EXACTLY | 即精确模式,当控件的layout_width或layout_height为具体数值时,系统使用的是EXACTLY模式。 |
AT_MOST | 最大值模式,当控件的layout_width或layout_height为wrap_content或match_parent时,控件的尺寸不能超过父控件允许的最大尺寸即可。 |
UNSPECIFIED | 未指定模式,它不指定其大小测量模式,View想多大就多大。 |
系统最终会调用setMeasuredDimension(int measuredWidth,int measuredHeight)方法将测量后的宽高传递进去,以完成测量操作。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
/**测量宽度的模板代码*/
private int measureWidth(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){ // 精确数值
result = specSize;
}else{ // 非精确数值
result = 200;
if(specMode == MeasureSpec.AT_MOST){// 自动包含
result = Math.min(result, specSize);// 取出指定大小与specSize中最小一个作为最后测量值。
}
}
return result;
}
/**测量高度的模板代码*/
private int measureHeight(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else{
result = 200;
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);
}
}
return result;
}
a) 布局文件中指定精确的宽高值是400px时,View会根据指定的宽高进行设定。
b) 当指定宽高属性为match_parent时,View会填充整个父布局。
c) 当指定宽高属性为wrap_content时,如果不重写onMeasure()方法则会填充整个父布局,重写的话则会根据内容自动包含。
(2)ViewGroup的测量
当ViewGroup大小为wrap_content时,需要对子View进行遍历,以便根据所有子View的大小,来确定自身的大小。
ViewGroup调用子View的measure()方法遍历测量后,获取到子View的测量结果,然后打包成MeasureSpec传递给子View。
// measureChild(View, int, int)为子组件添加Padding
measureChild(child, parentWidthMeasureSpec, parentHeightMeasureSpec);
// measureChildren(int, int)根据指定的高和宽来测量所有子View中显示参数非GONE的组件。
measureChildren(widthMeasureSpec, heightMeasureSpec);
// measureChildWithMargins(View, int, int, int, int)测量指定的子组件,为子组件添加Padding和Margin。
measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
View的绘制
装载画布
Android中,创建画布有两种方式:
Canvas canvas = new Canvas(); 或 Canvas canvas = new Canvas(bitmap);
当在创建画布传入bitmap对象时,bitmap和画布是紧紧相连的,这个过程我们称之为装载画布。
这个bitmap用来存储所有绘制在Canvas上的像素信息。且Canvas调用所有的Canvas.drawXXX方法都发生在该bitmap上。
装载画布时,当Canvas将绘制效果作用在bitmap时,刷新view就会改变bitmap,如果非装载画布模式下,改变的是bitmap对象,并让view重绘。
绘制解析
Android系统中要自定义view,首先需要了解Android的view加载机制。主要有三个方法:
1、onMeasure() //计算出view自身大小
2、onLayout() //仅在ViewGroup中,用来为子view指定位置(left,top)
3、onDraw() //view绘制内容
下面根据源码中的相关说明,进一步分析控件的绘制操作及顺序:
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
* 1. Draw the background 绘制控件设置的背景
* 2. If necessary, save the canvas' layers to prepare for fading 保存画布的图层和阴影信息
* 3. Draw view's content 重写onDraw(canvas)进行绘制
* 4. Draw children 绘制子控件,对应方法dispatchDraw(canvas)
* 5. If necessary, draw the fading edges and restore layers 绘制控件阴影渐变效果
* 6. Draw decorations (scrollbars for instance) 绘制滚动条,对应方法onDrawScrollBars(canvas)
*/
在第四步时,如果当前需要绘制的控件是ViewGroup,则需要通过dispatchDraw()方法绘制子控件,如果是View则不需要。
通常情况下ViewGroup不需要进行绘制,因为其本身没有需要绘制的东西,如果不是指定背景色,那么ViewGroup的onDraw方法不会被调用。
但是,ViewGroup会通过dispatchDraw()方法来绘制其子View。
下面我们看看onDraw()和dispatchDraw()的区别:
- 绘制View本身内容时,可以调用View.onDraw(Canvas canvas)方法。
- 绘制View的子View的内容时,可以调用diapatchDraw方法。