(学习参考书:Android群英传)
一、Android控件架构
Android中的控件大致被分为两大类,即ViewGroup控件和View控件。ViewGroup控件作为父控件可以包含多个View控件,并对其进行管理。通过ViewGroup整个界面上的控件形成了一个树形结构即控件树,通常在activity中使用findViewById()就是在控件树中以深度优先遍历来查找对应元素的。
每个activity都包含一个Window对象,在Android中Window对象通常用PhoneWindow来实现。PhoneWindow将一个DecorView设置为整个应用窗口的根View。
二、View的测量
Android系统在绘制View前,也必须对View进行测量,在onMeasure()方法中进行。Android系统提供了一个设计短小精悍却功能强大的类——MeasureSpec类,通过它来帮助完成测量。MeasureSpec是一个32位int值,其中高两位为测量的模式,低30位位测量的大小。 测量模式分为:
- EXACTLY:精确值模式,将控件长宽指定为准确数值
- AT_MOST:最大值模式,只要控件的尺寸不超过父控件允许的最大尺寸,控件大小一般随着内容大小变化(wrap_content)
- UNSPECIFIED:不指定大小测量模式,View想多大就多大,通常情况下在绘制自定义View时才会使用
View类默认的onMeasure()方法只支持EXACTLY,因此在自定义控件时不重写onMeasure()方法的话,也只能使用EXACTLY模式,控件可以响应match_parent属性,但如果想让控件支持wrap_content属性必须重写onMeasure()方法来指定大小。 通过MeasureSpec这一个类,我们就获取了View的测量模式和View想要绘制的大小。有了这些信息,就可以控制View最后显示的大小。 对View进行测量,需要重写View的onMeasure()方法,代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));
}
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;
}
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);
}
}
return result;
}
对代码的解释:
- 在onMeasure方法中,调用自定义的measureWidth()和measureHeight()方法,分别对宽高进行重新定义,参数是宽和高的MeasureSpec对象。
- 在自定义方法中,从MeasureSpec对象中提取出具体的测量模式和大小
- 通过判断测量的模式,给出不同的测量值。当为EXACTLY时直接指定specSize即可
- 当为其他两种模式时,需要给一个默认的大小。
- 特别的如果指定为wrap_content即AT_MOST模式,需要与specSize中较小的一个来作为最后的测量值。
三、View的绘制
当测量好一个View之后,简单地重写onDraw()方法,并在Canvas对象上来绘制所需要的图形。 重写的onDraw()方法中有一个参数,也即就是Canvas对象。使用这个对象就可以绘图了;如果在其他地方创建new一个Canvas,需要传入一个bitmap对象,即装载画布。 依靠这个bitmap创建的Canvas与bitmap紧紧联系,所有的Canvas.drawXxx()方法都发生在这个bitmap上。
四、自定义View
在自定义View时,通常会去重写onDraw()方法来绘制View的显示内容。如果该View还需要wrap_content属性,那么还必须重写onMeasure()方法。另外通过自定义attrs属性,还可以指定新的属性配置值。在View中通常有以下比较重要的回调方法。
onFinishflate()
onSizeChanged()
onMeasure()
onLayout()
onTouchEvent()
通常情况下,不需要重写所有的方法,只需要重写特定条件的回调方法即可。一般有以下三种方法来实现自定义的控件:
(1)对现有控件进行拓展 一般来说,我们可以在onDraw()方法中对原生控件行为进行拓展。 重写onDraw()方法,程序调用super.onDraw(canvas)方法来实现原生控件的功能。但是在之前和之后都可以实现自己的逻辑,分别在系统绘制文字前后,完成自己的操作。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
(2)通过组合实现复合控件 创建复合控件可以很好的创建出具有重用功能的控件集合。这种方式通常都需要一个合适的ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件。具体用法如下:
- 定义属性:只需要在res资源目录下创建一个属性定义xml文件,然后在<declare-styleable标签下通过<attr标签指定属性。
- 创建Java类继承需要的ViewGroup,使用以下代码获取在XML布局文件中的自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.控件名) 并在Java代码构造方法中完成定义属性的赋值,最后获取完所有的属性值后,要调用TypedArray的recyle方法完成资源的回收 - 组合控件:将需要添加进复合控件布局的控件获取实例后,设置前面所获取的具体属性值,然后使用addView()模板添加到定义的复合控件模板中
- 如果要使用控件的点击事件,为了实现不同的功能,不能直接在UI模板中添加具体的实现逻辑,只能通过接口回调的思想,将具体的实现逻辑交给调用者
- 引用UI模板:在引用前,需要指定引用第三方控件的名字空间。如果使用自定义属性,就需要创建自己的命名空间;使用自定义的view需要在声明控件时指定完整的包路径。
(3)重写View来实现全新的控件 创建一个全新的自定义控件难点在于绘制控件和实现交互,通常需要继承View类,重写它的onDraw()、onMeasure()方法来实现绘制逻辑,同时通过重写onTouchEvent()等触控事件来实现交互逻辑。当然还可以像实现控件方式那样通过引入自定义属性丰富自定义View的可定制性。
五、自定义ViewGroup
ViewGroup存在的目的就是为了对其子View进行管理,为其子View添加显示、响应的规则。自定义ViewGroup需要重写onMeasure()方法来对子View进行测量,重写onLayout()方法来确定子View的位置,重写onTouchEvent()方法来增加响应时间。
六、事件拦截机制
在事件传递中,我们只关心onInterceptTouchEvent()方法,而dispatchTouchEvent()虽然是事件分发的第一步,但一般情况下,不太会改写这个方法。事件处理都是执行onTouchEvent()方法。
|