视图绘制流程
Android
中的任何一个布局、任何一个控件其实都是直接或间接继承自
View
的,如
TextView
、
Button
、
ImageView
、
ListView
等,任何一个视图都不可能凭空突然出现在屏幕上,它们都是要经过非常科学的绘制流程后才能显示出来的。每一个视图的绘制过程都必须经历三个最主要的阶段,即
onMeasure()
、
onLayout()
和
onDraw()
,下面我们逐个对这三个阶段展开进行探讨。
1 onMeasure()
measure
是测量的意思,那么
onMeasure()
方法顾名思义就是用于测量视图的大小的。
View
系统的绘制流程会
从
ViewRoot
的
performTraversals()
方法中开始,在其内部调用
View
的
measure()
方法。
measure()
方法接收两个参数,
widthMeasureSpec
和
heightMeasureSpec
,这两个值分别用于确定视图的宽度和高度的规格和大小。
MeasureSpec
的值由
specSize
和
specMode
共同组成的,其中
specSize
记录的是大小,
specMode
记录的是规格。
specMode
一共有三种类型,如下所示:
l EXACTLY :表示父视图希望子视图的大小应该是由 specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
l AT_MOST :表示子视图最多只能是 specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过 specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
l
UNSPECIFIED
:表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
onMeasure()
方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制。
public class MyView extends View {
......
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(200, 200);
}
}
这样的话就把
View
默认的测量流程覆盖掉了,不管在布局文件中定义
MyView
这个视图的大小是多少,最终在界面上显示的大小都将会是
200*200
。
需要注意的是,在
setMeasuredDimension()
方法调用之后,我们才能使用
getMeasuredWidth()
和
getMeasuredHeight()
来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是
0
。
由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在
XML
文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。
2 onLayout()
measure
过程结束后,视图的大小就已经测量好了,接下来就是
layout
的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。
ViewRoot
的
performTraversals()
方法会在
measure
结束后继续执行,并调用
View
的
layout()
方法来执行此过程
layout()
方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。
重写
onLayout()
方法来自定义一个布局
public class SimpleLayout extends ViewGroup {
public SimpleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@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, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}
onMeasure()
方法会在
onLayout()
方法之前调用,因此这里在
onMeasure()
方法中判断
SimpleLayout
中是否有包含一个子视图,如果有的话就调用
measureChild()
方法来测量出子视图的大小。
接着在
onLayout()
方法中同样判断
SimpleLayout
是否有包含一个子视图,然后调用这个子视图的
layout()
方法来确定它在
SimpleLayout
布局中的位置,这里传入的四个参数依次是
0
、
0
、
childView.getMeasuredWidth()
和
childView.getMeasuredHeight()
,分别代表着子视图在
SimpleLayout
中左上右下四个点的坐标。其中,调用
childView.getMeasuredWidth()
和
childView.getMeasuredHeight()
方法得到的值就是在
onMeasure()
方法中测量出的宽和高。
创建并使用自己的布局
<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher"
/>
</com.example.viewtest.SimpleLayout>
3 onDraw()
measure
和
layout
的过程都结束后,接下来就进入到
draw
的过程了。同样,根据名字你就能够判断出,在这里才真正地开始对视图进行绘制。
ViewRoot
中的代码会继续执行并创建出一个
Canvas
对象,然后调用
View
的
draw()
方法来执行具体的绘制工作。
View
是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察
TextView
、
ImageView
等类的源码,你会发现它们都有重写
onDraw()
这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助
Canvas
这个类,它会作为参数传入到
onDraw()
方法中,供给每个视图使用。
Canvas
这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西
public class MyView extends View {
private Paint mPaint;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
mPaint.setColor(Color.BLUE);
mPaint.setTextSize(20);
String text = "Hello View";
canvas.drawText(text, 0, getHeight() / 2, mPaint);
}
}
我们创建了一个自定义的
MyView
继承自
View
,并在
MyView
的构造函数中创建了一个
Paint
对象。
Paint
就像是一个画笔一样,配合着
Canvas
就可以进行绘制了。这里我们的绘制逻辑比较简单,在
onDraw()
方法中先是把画笔设置成黄色,然后调用
Canvas
的
drawRect()
方法绘制一个矩形。然后在把画笔设置成蓝色,并调整了一下文字的大小,然后调用
drawText()
方法绘制了一段文字
测试自己定义的
View
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.viewtest.MyView
android:layout_width="200dp"
android:layout_height="100dp"
/>
</LinearLayout>