在Android中View的存在的方式一共有两种形式:
- 单一的View控件
- 可以包含其他View的ViewGroup
在了解View的绘制过程的时候,首先就要了解一下我们的Android的UI管理系统的层次关系:
如图所示:
从源码中其实我们很容易就知道每个Activity都会创建一个最基本的窗口系统 PhoneWindow 。 PhoneWindow 是Activity与View 交互的接口。 从图中我们又看到 DecorView , 在事件传递机制下,事件会传递给这个 DecorView 吗,然后子View就能接受到事件了。 在 DecorView 中我们可以看到 TitleView 和 ContentView 。
TitleView 通常就是 ActionBar ,而 ContentView 就是我们最常接触的,就是平时在 Activity 中通过setContentView() 给Activity设置的View .
绘制的整体流程
当一个启动一个Activity的时候,Android系统会根据Activity的布局对它进行绘制。绘制会从根视图ViewRoot的 performTraversals() 方法开始 , 从上往下的遍历整个视图树。然而对于View控件来说,View控件只负责控制自己,而ViewGroup来说,他只是负责通知自己的子View进行绘制。
ViewRootImpl # performTraversals
private void performTraversals(){
.....
//执行测量流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
.....
//执行布局流程
performLayout(lp, mWidth, mHeight);
......
//执行绘制流程
performDraw();
}
从ViewRootImpl 中可以看到的就是,视图的绘制会执行以下三个步骤,分别是 Measure (测量) 、Layout(布局)、Draw (绘制) 。
Measure
Measure 是用来计算View得到实际大小,由前面的分析可知,页面的绘制是从 performMeasure 方法开始的。
ViewRootImpl # performMeasure
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
...
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
从上面可以知道,performMeasure方法只是调用了 mView.measure(...) ,把具体的绘制交给了 View 。
View # measure
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
....
onMeasure(widthMeasureSpec, heightMeasureSpec);
....
}
View # onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
由上面可以得知到的一点就是performMeasure 最终会调用 View 或者 ViewGroup 的 measure 方法 ,而这里面实际上就是调用了 onMeasure 。
先对View分析
对于View来说,当调用到 onMeasure 的方法时候, 如果没有重写这个方法的话,那么默认的调用 getDefaultSize 来获取 View 的宽高。 源码如下:
View # getDefaultSize
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
从上面可以得知:对于View默认是测量很简单,大部分情况就是拿计算出来的MeasureSpec的size 当做最终测量的大小。
而对于一些派生出来的View ,如TextView 、ImageView 等,它们都对onMeasure方法系统了进行了重写。例如TextView 通常先去会先去测量字符的高度等,然后拿到View本身content这个高度,如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度。
再对ViewGroup分析
ViewGroup是特殊的View,然而在ViewGroup里面并没有实现 onMeasure 这个方法。而在不同的派生类中,各自实现了自身的 onMeasure 方法。对于DecorView 来说 ,其实就是一个FrameLayout,对于要测量时,一开始其实就是调用到了 FrameLayout 的 onMeasure 方法中 , 从 FrameLayout 中可以看到:
FrameLayout # onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
.....
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
if (mMeasureAllChildren || child.getVisibility() != GONE) {
....
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
....
}
}
....
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
....
}
从上面可以看到,其实 ViewGroup 的内部就是 遍历自己的子View,只要不是GONE的都会参与测量。然后等所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高。综上,父View是等所有的子View测量结束之后,再来测量自己。
Layout
Layout 过程用来确定View在父容器的布局位置,他是由父容器获取子View的位置参数后,调用子View的layout方法并将位置参数传入实现的,源码如下:
ViewRootImpl # performLayout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
....
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
.....
}
View # layout
public void layout(int l, int t, int r, int b) {
.....
onLayout(changed, l, t, r, b);
....
}
View # onLayout
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
onLayout 实际上就是一个空方法,对于ViewGroup来说,就应该实现这个方法。对于 子ViewGroup 来说,例如LinearLayout、RelativeLayout等,均重写了这个方法。
Draw
Draw操作用来将控件绘制出来,源码如下:
ViewRootImpl # performDraw
private void performDraw() {
....
draw(fullRedrawNeeded);
....
}
ViewRootImpl # draw
private void draw(boolean fullRedrawNeeded) {
....
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
.....
}
ViewRootImpl # drawSoftware
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
....
mView.draw(canvas);
....
}
最会就调用子View 的 Draw
public void draw(Canvas canvas) {
....
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
.....
drawBackground(canvas);
....
// Step 2, save the canvas' layers
.....
saveCount = canvas.getSaveCount();
.....
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
....
canvas.drawRect(left, top, left + length, bottom, p);
....
canvas.restoreToCount(saveCount);
....
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
从源码中我们很清晰的看到View绘制的流程
- 绘制View的背景
- 如果需要,保存canvas,为fading做准备
- 绘制View内容
- 绘制View的子View
- 如果需要的话,绘制View的fading边缘并恢复图层
- 绘制View的装饰(如滚动条)
measure(测量)方法的注意
从上面我们可以清楚了的明白了View的绘制过程了,从measure到layout再到Draw的一系列过程,最终View绘制了出来。然而有些时候我们想在Activity已启动的时候就做一件任务,这一件任务是获取某个View的宽/高。但是我们在onCreate或者onResume 获取View的宽和高却获取不了数值,测试如下:
<TextView
android:id="@+id/tv_main"
android:layout_width="250dp"
android:layout_height="35dp"
android:gravity="center"
android:text="Hello World!" />
MainActivity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTvMain = (TextView) findViewById(R.id.tv_main);
System.out.println("TextView 的高度为:"+mTvMain.getHeight());
System.out.println("TextView 的宽度为:"+mTvMain.getWidth());
}
运行结果如下:
TextView 的高度为:0
TextView 的宽度为:0
实际上在onCreate、onStart、onResume中均无法正确得到某
个View的宽和高信息,这是因为View的measure过程和Activity的生命周期方法不是同步
执行的因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量
完毕了。
如果想要拿取View的宽和高又应怎么做呢?下面介绍三种方法。
1. onWindowFocusChanged
onWindowFocusChanged 这个方法的含义是:View已经初始化完毕了,宽/高已经准备
好了,这个时候去获取宽/高是没问题的。需要注意的是,onWindowFocusChanged会被调
用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说,当Activity
继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行onResume
和onPause,那么onWindowFocusChanged也会被频繁地调用。
代码如下:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
System.out.println("TextView 的高度为:"+mTvMain.getHeight());
System.out.println("TextView 的宽度为:"+mTvMain.getWidth());
}
}
运行结果:
TextView 的高度为:70
TextView 的宽度为:500
2. view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable
的时候,View也已经初始化好了。
代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTvMain = (TextView) findViewById(R.id.tv_main);
mTvMain.post(new Runnable() {
@Override
public void run() {
System.out.println("TextView 的高度为:"+mTvMain.getHeight());
System.out.println("TextView 的宽度为:"+mTvMain.getWidth());
}
});
}
运行结果:
TextView 的高度为:70
TextView 的宽度为:500
3. ViewTreeObsener
使用ViewTrecObserver的众多回调可以完成这个功能,比如使用
OnGlobalLayoutListener 这个接口,当View树的状态发生改变或者View树内部的View的
可见性发现改变时,onGlobalLayout方法将被回调,因此这是获取View的宽和高一个很好
的时机。需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次。
代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTvMain = (TextView) findViewById(R.id.tv_main);
ViewTreeObserver viewTreeObserver = mTvMain.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mTvMain.getViewTreeObserver().removeOnGlobalLayoutListener(this);
System.out.println("TextView 的高度为:" + mTvMain.getHeight());
System.out.println("TextView 的宽度为:" + mTvMain.getWidth());
}
});
}
运行结果:
TextView 的高度为:70
TextView 的宽度为:500