Wen

Android 深入了解View的绘制过程

写Android App的时候,经常要获取或者改变各种layout或者view的宽度和高度,但这个信息又经常得不到,其实只要对Android中view绘制流程的内部工作原理有一个了解即可解决此类问题。

Android中View的添加是树状结构。


Android添加View是按照一个树的结构添加的,这棵树的Root是一个叫DectorView的class,并且它是一个FrameLayout的子类。这也就是为什么如果你的最外面的layout如果是FrameLayout的话,可以在layout的xml文件中省略FrameLayout,用<merge> tag来代替。一般写layout一般都用xml来写,而xml本身的结构就是树结构,两者在结构上不谋而合。一般layout就是下面这样的结构。

常见的setContentView()就是生成用户控制的根部的那个View,然后下面的都是通过LayoutInflater.inflate()来加载布局,生成View,并通过addView()一层一层的从上到下添加到树上。各个view之间的消息传递也是按照这个树形结构来传递的,或者向上,或者向下。

View、ViewGroup和Layout的关系


ViewGroup继承ViewLayout继承ViewGroup。基本的UI类都是继承View类的,一个View在屏幕上占据一个矩形区域。ViewGroupView的容器,负责对其中的多个View的管理,虽然ViewGroup继承View,但ViewGroupView之间是组合的设计模式。Widget是一个包,包含了android内置的UI类,比如ListView, ImageView等,里面的类都继承View。下面这张图把这些关系说的很清楚。

View的绘制流程


重点来了,每一个View的绘制过程都必须先后经历三个最主要的阶段,即onMeasure()onLayout()onDraw()

1. onMeasure()

每一个View都会在OnMeasure()这个方法里设置它的height和width。我们先看View这个类的源代码:

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

其中setMeasuredDimension()是决定这个View大小的代码,不管之前如何设置的这个view的height和width,如果你此处手动给了值,那么这个view最终的大小就以这个地方给的值为准。我们来看一个简单的例子:

1
2
3
4
5
6
7
public class MyView extends View {
......
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(200, 200);
}
}

这样的话就把View默认的Measure流程覆盖掉了,也就是说,不管在定义layout的那个xml文件中如何设置MyView这个视图的宽和高,最终在界面上显示的大小都将会是200*200。

需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。但getWidth()或者getHeight()需要在onLayout()之后才能得到,我们下面马上讲到。

比如想调用getMeasuredWidth()或者getMeasuredHeight()得到View的大小,可以在当前View override onMeasure()这个方法, 并且在onMeasure()中调用完super.onMeasure()之后调用这两个方法。另外在onLayout()或者onDraw()里调用也都可以。

讲到这里我们简单说一下getWidth()getMeasuredWidth()的区别。以上面的代码为例,如果在MyView没有覆盖调用onMeasure()并人为setMeasuredDimension(),那么这两个方法返回的结果应该是一样的。但如果像我们上面给的例子那样,onMeasure()被覆盖了,getWidth()返回的结果将是200,也即setMeasuredDimension()中设置的值,而getMeasuredWidth()返回的则是我们人为改变前的值,也即没有覆盖调用onMeasure()时应该得到的值。

2. onLayout()

measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。

1
public void layout(int left, int top, int right, int bottom)

这个方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的,如果某个view覆盖重写这个方法,就可以人为固定其位置。但一般某个view在屏幕上的具体位置都是其parent决定的,所以一般情况是,在父类的onLayout()里,调用子类的layout()函数,来固定子类的位置。

onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了。那么getWidth()方法和getMeasureWidth()方法本质上到底有什么区别呢?首先getMeasureWidth()方法在measure过程结束后就可以获取到了,而getWidth()方法要在layout过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。举个栗子,看下面的代码:

1
2
3
4
5
6
7
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, 200, 200);
}
}

这样getWidth()方法得到的值就是200 - 0 = 200,不会再和getMeasuredWidth()的值相同了。当然这种做法充分不尊重measure过程计算出的结果,通常情况下是不推荐这么写的。通常情况下,这个方法都是写成下面这种,让两者保持一致:

1
2
3
4
5
6
7
8
9
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}

3. onDraw()

measure和layout的过程都结束后,接下来就进入到draw的过程了,在这里才真正地开始对视图进行绘制。每个视图根据想要展示的内容来自行绘制,比如看TextViewImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了不少独有的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西,具体我们这里就不讲了。

Reference: http://blog.csdn.net/guolin_blog/article/details/16330267

转载请注明出处:http://zhaowen.io/post/Android_Dive_Deep_In_View/