一、Android控件树
Android控件大致分为两类,即ViewGroup控件和View控件。ViewGroup控件可以包含多个View控件,并且负责管理其中的View控件。如图3.1所示,整个界面上的ViewGroup和View构成了一棵控件树,上层控件负责下层子控件的测量与绘制,并传递交互事件。
在每棵控件树的顶部,都有一个ViewParent对象,这就是整棵树的控制核心,所有的交互管理事件都由它来统一调度和分配,从而可以对整个视图进行整体控制。
Activity中使用的findViewById()方法,就是在控件树中以深度优先遍历查找对应的元素的。通常调用setContentView()方法设置一个布局,使得布局内容真正地显示出来。
二、UI界面架构图
图3.2展示了Android的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中的视图树的第二层装载了一个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。