我们在写Android App的时候,经常要获取或者改变各种layout或者view的宽度和高度,或者想实现一些customized的UI控件,这个时候经常会遇到各种各样没有任何想法的问题,这个时候就需要对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继承View,Layout继承ViewGroup。基本的UI类都是继承View类的,一个View在屏幕上可以占据一个矩形区域。ViewGroup是View的容器,负责对其中的多个View的管理,虽然ViewGroup继承View,但ViewGroup和View之间是组合的设计模式。Widget是一个包,包含了android内置的UI类,比如ListView, ImageView等,里面的类都继承View。下面这张图把这些关系说的很清楚。
(三)View的绘制流程
每一个View的绘制过程都必须先后经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw()。
1. onMeasure()
每一个View都会在OnMeasure()这个方法里设置View的height和width。我们先看View里的源代码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
其中setMeasuredDimension()是决定这个View大小的代码,不管你之前如何设置这个view的height和width,如果你此处手动给了值,那么这个view最终的大小就以这个地方给的值为准。我们来看一个简单的例子:
public class MyView extends View {
......
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(200, 200);
}
}
这样的话就把View默认的测量流程覆盖掉了,不管在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。
需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。但getWidth()或者getHeight()需要在onLayout()之后才能得到,我们下面马上讲到。
但如果想利用getWidth()或者getHeight()得到View的大小的时候,可以在当前View override onMeasure(), 并且在onMeasure()中调用完super.onMeasure()之后调用getWidth, 也可以在onLayout()或者onDraw()里调用都可以。
讲到这里我们可以说一下getWidth()和getMeasuredWidth()的区别。以上面的代码为例,如果在MyView里没有覆盖调用onMeasure()并人为setMeasuredDimension(),那么这两个方法返回的结果应该是一样的。但如果像我们上面给的例子那样,getWidth返回的结果将是200,即setMeasuredDimension()中设置的值,而getMeasuredWidth()返回的将是我们人为改变前的值,即没有覆盖调用onMeasure()时应该得到的值。
2. onLayout()
measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。
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()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。比如下面的代码:
@Override
protected void onLayout(boolean changed, int l, , int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, 200, 200);
}
}
这样getWidth()方法得到的值就是200 - 0 = 200,不会再和getMeasuredWidth()的值相同了。当然这种做法充分不尊重measure()过程计算出的结果,通常情况下是不推荐这么写的。通常情况下,这个方法都是写成下面这种,让两者保持一致:
@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的过程了.在这里才真正地开始对视图进行绘制。每个视图根据想要展示的内容来自行绘制,比如TextView、ImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西,具体我们这里就不讲了。
Reference: http://blog.csdn.net/guolin_blog/article/details/16330267