第三章 Android控件架构(一)

一、Android控件树

Android控件大致分为两类,即ViewGroup控件和View控件。ViewGroup控件可以包含多个View控件,并且负责管理其中的View控件。如图3.1所示,整个界面上的ViewGroup和View构成了一棵控件树,上层控件负责下层子控件的测量与绘制,并传递交互事件。


图3.1 View树结构

在每棵控件树的顶部,都有一个ViewParent对象,这就是整棵树的控制核心,所有的交互管理事件都由它来统一调度和分配,从而可以对整个视图进行整体控制。

Activity中使用的findViewById()方法,就是在控件树中以深度优先遍历查找对应的元素的。通常调用setContentView()方法设置一个布局,使得布局内容真正地显示出来。

二、UI界面架构图

图3.2展示了Android的UI界面架构图。


图3.2 UI界面架构图

如图3.2所示,每个Activity都包含了一个Window对象,通常由PhoneWindow来实现。
DecorWindow是整个应用窗口的根,是窗口界面的顶层视图,封装了一些窗口的通用方法,将要显示的具体内容呈现在PhoneWindow上。这里面所有View的监听事件,都通过WindowManagerService来接收,并通过Activity对象来回调响应的onClickListener。
DecorWindow将屏幕分成两部分:TitleWindow和ContentWindow。其中ContentWindow是一个ID为content的Framelayout,activity_main.xml就是设置在这样一个Framelayout里。

通过以上过程,可以建立起一个标准视图树,如图3.3所示。


图3.3 标准视图树

图3.3中的视图树的第二层装载了一个LinearLayout,作为ViewGroup,这一层的布局会根据对应的参数设置为不同的布局。
图3.3所示的是一个最常用的布局——上面TitleBar,下面Content。如果想让界面全屏,则需要通过设置requestWindowFeature(Window.FEATURE_NO_TITLE)来设置布局。在第二层设置布局,所以调用requestWindowFeature()方法一定要在setContentView()方法之前才能生效。

在代码中,onCreate()方法中调用setContentView()方法后,ActivityManagerService会回调onResume()方法,此时系统才会把整个DecorView添加到PhoneWindow中,并让其显示出来,从而完成最终界面的绘制。

三、View的测量

这个过程在onMeasure()方法中进行。测量模式可以分为以下三种。

  • EXACTLY

精确模式。layout_width、layout_height指定为具体数值事或者为match_parent属性时,系统使用该模式。

  • AT_MOST

最大值模式。layout_width、layout_height指定为wrap_content时。

  • UNSPECIFIED

不指定大小测量模式。View想多大就多大,通常在绘制自定义View时才会使用。

View类默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义组件的时候,要让自定义View支持wrap_content属性,那么就必须重新onMeasure()方法来指定wrap_content时的大小。

通过MeasureSpec这一个类,就可以获取View的测量模式和View想要绘制的大小。有了这些信息,我们就可以控制View最后显示的大小。

下面通过一个简单的例子,颜色如何进行View的测量。首先,要重写onMeasure()方法,该方法如下所示。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

查看super.onMeasure()方法,可以发现,系统最终会调用serMeasuredDimension(int measuredWidth, int measuredHeight)方法将测量后的宽高值设置进去。因此,示例中重写onMeasure()方法代码如下。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
            measureWidth(widthMeasureSpec),
            measureHeight(heightMeasureSpec));
}

其中,measureWidth()、measureHeight()方法是用来自定义测量值的自定义函数。以measureWidth()方法为例。

private int measureWidth(int measureSpec) {
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);  // 从MeasureSpec对象中提取具体的测量模式
    int specSize = MeasureSpec.getSize(measureSpec);  // 从MeasureSpec对象中提取具体的大小

    if(specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else {
        result = 200;  // UNSPECIFIED模式
        if (specMode == MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
    }
    return result;
}

通过这两个方法,可以实现对宽高值的自定义。

默认情况下,当指定宽高属性为wrap_content时,如果不重写onMeasure()方法,系统会默认填充整个父布局。在上述示例中,当指定wrap_content属性时,View获得了一个默认值200px,所以系统不再默认填充父布局,而是默认使用200px整个大小值。

四、View的绘制

当测量好一个View之后,就可以简单地重写onDraw()方法,并在Canvas对象上绘制所需要的图形。

Canvas对象就像是一个画板,使用Paint可以在上面作画。通常通过继承View并重写它的onDraw()方法来完成绘图。一般情况下,可以使用重写View类中的onDraw()方法来绘图,onDraw()方法有一个参数是Canvas对象,可以使用该参数进行绘图。而在其他地方,则需要创建一个Canvas对象,如下所示

Canvas  canvas = new Canvas(bitmap);

传入参数bitmap,称为装载画布。这个bitmap用来存储所有绘制在canvas撒花姑娘的像素信息,即后面所有的canvas.drawXXX方法都发生在这个bitmap上。

虽然使用了Canvas的绘制API——canvas.drawXXX方法,但其实并没有将图形直接绘制在指定的bitmap上,而是通过改变bitmap,然后让View重绘,从而显示改变后的bitmap。

理解了Canvas对象之后,不管是多么复杂的控件,都可以分解为简单的图形单元,然后调用绘制API进行绘制。

五、ViewGroup的测量与绘制

ViewGroup其中一个功能就是负责子View的显示大小。当ViewGroup的大小为wrap_content时,ViewGroup需要遍历所有子View的大小,从而决定自己的大小。其他模式下则会通过其具体的指定值来设置自身大小。

前面所说的子View的测量就是在ViewGroup遍历时进行的。测量完子View之后,就需要将子View放到合适的位置,这个过程就是View的Layout过程。ViewGroup在执行Layout过程时,同样使用遍历来调用子View的Layout方法,并指定其具体显示的位置。

在自定义ViewGroup时,通常会去重写onLayout()方法来控制其子View显示位置的逻辑。如果需要支持wrap_content属性,那么它还需要重写onMeasure()方法,这点和View是相同的。

ViewGroup通常情况下不需要绘制,但是它会使用dispatchDraw()方法来绘制其子View。

你可能感兴趣的:(第三章 Android控件架构(一))