(学习参考书:Android群英传)

一、Android控件架构

Android中的控件大致被分为两大类,即ViewGroup控件和View控件。ViewGroup控件作为父控件可以包含多个View控件,并对其进行管理。通过ViewGroup整个界面上的控件形成了一个树形结构即控件树,通常在activity中使用findViewById()就是在控件树中以深度优先遍历来查找对应元素的。

android app控件布局 简述android app控件架构_android app控件布局

每个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;
}

对代码的解释:

  1. 在onMeasure方法中,调用自定义的measureWidth()和measureHeight()方法,分别对宽高进行重新定义,参数是宽和高的MeasureSpec对象。
  2. 在自定义方法中,从MeasureSpec对象中提取出具体的测量模式和大小
  3. 通过判断测量的模式,给出不同的测量值。当为EXACTLY时直接指定specSize即可
  4. 当为其他两种模式时,需要给一个默认的大小。
  5. 特别的如果指定为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() //从XML加载组件后回调
onSizeChanged() //组件大小改变时回调
onMeasure() //回调该方法来进行测量
onLayout() //回调该方法来确定显示的位置
onTouchEvent() //监听到触摸事件时回调

通常情况下,不需要重写所有的方法,只需要重写特定条件的回调方法即可。一般有以下三种方法来实现自定义的控件:

(1)对现有控件进行拓展
一般来说,我们可以在onDraw()方法中对原生控件行为进行拓展。
重写onDraw()方法,程序调用super.onDraw(canvas)方法来实现原生控件的功能。但是在之前和之后都可以实现自己的逻辑,分别在系统绘制文字前后,完成自己的操作。

@Override
protected void onDraw(Canvas canvas) {
    //在回调之前实现自定义逻辑,如果对TextView来说即是绘制文本内容之前
    super.onDraw(canvas);
    //在回调之后实现自定义逻辑,如果对TextView来说即是绘制文本内容之后
}

(2)通过组合实现复合控件
创建复合控件可以很好的创建出具有重用功能的控件集合。这种方式通常都需要一个合适的ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件。具体用法如下:

  1. 定义属性:只需要在res资源目录下创建一个属性定义xml文件,然后在<declare-styleable标签下通过<attr标签指定属性。
  2. 创建Java类继承需要的ViewGroup,使用以下代码获取在XML布局文件中的自定义属性
    TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.控件名) 并在Java代码构造方法中完成定义属性的赋值,最后获取完所有的属性值后,要调用TypedArray的recyle方法完成资源的回收
  3. 组合控件:将需要添加进复合控件布局的控件获取实例后,设置前面所获取的具体属性值,然后使用addView()模板添加到定义的复合控件模板中
  4. 如果要使用控件的点击事件,为了实现不同的功能,不能直接在UI模板中添加具体的实现逻辑,只能通过接口回调的思想,将具体的实现逻辑交给调用者
  5. 引用UI模板:在引用前,需要指定引用第三方控件的名字空间。如果使用自定义属性,就需要创建自己的命名空间;使用自定义的view需要在声明控件时指定完整的包路径。

(3)重写View来实现全新的控件
创建一个全新的自定义控件难点在于绘制控件和实现交互,通常需要继承View类,重写它的onDraw()、onMeasure()方法来实现绘制逻辑,同时通过重写onTouchEvent()等触控事件来实现交互逻辑。当然还可以像实现控件方式那样通过引入自定义属性丰富自定义View的可定制性。

五、自定义ViewGroup

ViewGroup存在的目的就是为了对其子View进行管理,为其子View添加显示、响应的规则。自定义ViewGroup需要重写onMeasure()方法来对子View进行测量,重写onLayout()方法来确定子View的位置,重写onTouchEvent()方法来增加响应时间。

六、事件拦截机制

在事件传递中,我们只关心onInterceptTouchEvent()方法,而dispatchTouchEvent()虽然是事件分发的第一步,但一般情况下,不太会改写这个方法。事件处理都是执行onTouchEvent()方法。