一、前期基础知识储备
了解View的绘制流程很大程度上可以帮助我们自定义View的实现,那么本篇文章就将从实现自定义View开始,了解自定义View的常见步骤,然后抓取自定义View的关键步骤,然后在View的绘制流程中重点关注其实现方式。与常规的View绘制方式的分析有所不同(笔者还未达到那个水平,源码暂时扣不动,所以换了一个角度看待这个问题,有取巧嫌疑)
(1)Android中自定义View的步骤:
①自定义View属性;
②在View的构造方法中获得自定义的属性;
③重写onMeasure(); --> 并不是必须的,大部分的时候还需要覆写
④重写onDraw();
(2)View在Activity中显示经历的步骤:
View在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout和draw。
测量:onMeasure()决定View的大小;
布局:onLayout()决定View在ViewGroup中的位置;
绘制:onDraw()决定以何种方式绘制这个View。
所以通过自定义View步骤和View显示步骤的分析,可以知道onMeasure()方法和onDraw()方法是在View绘制流程中需要重点关注的。
二、View的measure过程
(1)为什么自定义View要使用onMeasure()方法?
分析:Android系统在绘制View前,也必须对View进行测量,即告诉系统该画一个多大的View,这个过程在onMeasure()方法中进行。
Android系统给我们提供了一个短小精悍的类——MeasureSpec类,通过它来帮助我们测量View。MeasureSpec是一个32位的int值,其中高2位为测量模式,低30位为测量的大小,在计算中使用位运算是为了提高并优化效率。测量模式有三种:
UNSPECIFIED
父控件不对你有任何限制,你想要多大给你多大,通常在绘制自定义View时使用。
EXACTLY—View默认
父控件已经知道你所需的精确大小,要么为指定值要么为match_parent属性。
AT_MOST—自定义View使用到onMeasure()方法的根本原因
即最大值模式,当控件的layout_width属性或者layout_height属性指定为wrap_content时,控件大小一般随着控件的子控件或内容的变化而变化,但是子控件的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。
解答:View类默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能用EXACTLY模式,控件可以响应你指定的具体宽高值。而如果要让自定义View支持wrap_content属性,那么就必须重写onMeasure()方法来指定wrap_content时的大小。
(2)如何进行View的测量measure
①重写onMeasure()方法:
@Override
Protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
setMeasuredDimension(
measureWidth(widthMeasureSpec);//自定义的measureWidth()
measureHeight(heightMeasureSpec);//自定义的measureHeight()
)//利用自定义的两个方法对宽高重新定义
}//传入的参数即为开篇时提到的强大的类——MeasureSpec类
②实现自定义的measureWidth()measureHeight()和方法
private int measureWidth(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);//取出测量模式
int specSize = MeasureSpec.getSize(measureSpec);//取出测量大小
if(specMode == MeasureSpec.EXACTLY){
result = specSize;//EXACTLT时,直接使用指定值或者match_parent
}else{
result = 200;//人为给出指定大小
If(specMode ==MeasureSpec.AT_MOST){
result = Math.min(result,specSize);//取小
}
}
return result;
};//这段代码可以作为自定义View时的onMeasure()方法的模板代码
三、View的draw过程
当测量好一个View之后,我们就可以简单地重写onDraw() 方法,并在Canvas对象上来绘制所需要的图形。首先我们来了解一下利用系统2D绘图API所必须要使用到的Canvas对象。
要想在Android的界面中绘制相应的图像,就必须在Canvas上进行绘制。Canvas就像是一个画板,使用Paint就可以在上面作画了。通常需要通过继承View并重写它的onDraw()方法来完成绘图。
onDraw()方法中有一个参数就是Canvas canvas对象。
Canvas canvas = new Canvas(bitmap);//创建canvas对象时,传入bitmap
Bitmap与通过这个bitmap创建的Canvas画布是紧紧联系在一起的,这个过程我们称之为装载画布。这个bitmap用来存储所有绘制在Canvas上的像素信息。所以当你通过这种方式创建了Canvas对象后,后面调用的所有的Canvas.drawXXX方法都发生在这个bitmap上。例如,首先在onDraw()方法中绘制两个bitmap:
canvas.drawBitmap(bitmap1,0,0,null);
canvas.drawBitmap(bitmap2,0,0,null);
而对于bitmap2,我们将它装载到另一个Canvas对象中,代码如下,
Canvas mCanvas = new Canvas(bitmap2);
在其他地方使用Canvas对象的绘图方法在装载bitmap2的Canvas对象上进行绘图,代码:mCanvas.drawXXX
通过mCanvas将绘制效果作用在bitmap2上,再刷新View时会发现通过onDraw()方法画出来的bitmap2已经发生了改变,这就是因为bitmap2承载了在MCanvas上所进行的绘图操作。
注:Canvas—画板;bitmap—画布;paint—画笔
四、ViewGroup的绘制过程
ViewGroup容器控件会去管理其内的子View,其中一个管理项目就是负责子View的显示大小。当ViewGroup的大小为wrap_content时,ViewGroup就需要对View进行遍历,以便获得所有子View的大小,从而来决定自己的大小。而在其他两种模式下则会通过其指定的值来进行设置。
ViewGroup在测量是通过遍历所有子View,从而调用子View的Measure方法来获得每个子View的测量结果,前面第二节中对View的测量,就是在这里进行的。当子View测量完毕后,就需要将子View放到合适的位置,这个过程就是View的Layout过程。ViewGroup在执行Layout过程时,同样是使用遍历来调用子View的Layout方法,并指定具体显示的位置,从而来决定其布局位置。
在自定义ViewGroup时,通常会去重写onLayout()方法来控制其子View显示位置的逻辑。同样,如果需要支持wrap_content属性,那么它还必须重写onMeasure()方法,这点与View是相通的。
注:ViewGroup一般不需要绘制,因为它本身没有需要绘制的东西。