Android View 绘制流程一般分为三个过程:measure、layout、draw
一、mesure
android整个ui界面,本质上是多个View的树,其中ViewGroup是View的子类,他可以包含着多个子View和ViewGroup(ViewGroup之间也可以嵌套),这样形成了一个View树(如下图)。而measure函数的作用是为整个View树计算实际的大小,设置每个View对象的布局(窗口)大小。实际对应属性就是View类中的mMeasuredHeight(高)和mMeasuredWidth(宽)。
在View类中measure过程主要涉及一下三个函数:
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
前两个函数都是final类,所以不能重载,为此在ViewGroup派生的非抽象类中我们必须重载onMeasure函数。实现measure的原理是:假如View还有子View,则measure子View,知道所有的子View都measure完后,再measure自己。ViewGroup中的measureChild或measureChildWithMargins就是实现这个功能的。
在具体介绍测量原理之前,我们还是先了解一些基础知识。即measure函数的参数是由MeasureSpec类的makeMeasureSpec函数方法生成的一个32位整数,该整数的高2位表示模式(mode),低30位则是具体的尺寸大小(specSize)。
MeasureSpec有三种模式(mode),分别是:UNSPCIFIED、EXACTLY和AT_MOST,各表示的意义如下:
模式(mode) | 含义 |
UNSPCIFIED | 对于空间的尺寸来说,没有任何参考意义 |
EXACTLY | specSize代表的是精确尺寸 |
AT_MOST | specSize代表的是最大可获取的尺寸 |
那么对于一个View的上述mode和specSize值是如何获取到的呢,他们是根据View的LayoutParams:
①当参数为fill_parent或者match_parent时,Mode为EXACTLY,specSize为剩余的所有空间。
②当参数为具体数值,譬如像素值(px或dp),Mode为EXACTLY,specSize为传入的数值。
③当参数为wrap_content时,Mode为AT_MOST,specSize运行时决定。
上面提供的Mode和specSize仅仅是程序员对View的一个期望尺寸,最终一个View对象能从父视图那的到多大的允许尺寸则由子视图的期望尺寸和父视图的能力尺寸(可提供尺寸)两方面决定。关于期望尺寸的设定,可以通过在布局资源文件中定义的android:layout_width和android:layout_height来设定,也可以通过代码在addView函数调用时传入LayoutParams参数来设定。父View的能力尺寸归根到底就是DecorView,这个尺寸是全屏的,由手机的分辨率来决定。期望尺寸、能力尺寸和最终允许尺寸的关系,我们可以通过阅读measureChild或measureChildWithMargins都会调用的getChildMeasureSpec函数的源码来获得,下面简单说明三者之间的关系:
父视图能力尺寸 | 子视图期望尺寸 | 子视图最终允许尺寸 |
EXACTLY+size1 | EXACTLY+size2 | EXACTLY+size2 |
EXACTLY+size1 | fill_parent/match_parent | EXACTLY+size1 |
EXACTLY+size1 | wrap_content | AT_MOST+size1 |
AT_MOST+size1 | EXACTLY+size2 | EXACTLY+size2 |
AT_MOST+size1 | fill_parent/match_parent | AT_MOST+size1 |
AT_MOST+size1 | wrap_content | AT_MOST+size1 |
UNSPCIFIED+size1 | EXACTLY+size2 | EXACTLY+size2 |
UNSPCIFIED+size1 | fill_parent/match_parent | UNSPCIFIED+0 |
UNSPCIFIED+size1 | wrap_content | UNSPCIFIED+0 |
上述表格是子视图最终得到的允许尺寸,显然1、4、7项没有对size1和size2进行比较,所以允许尺寸是可以大于父视图的能力尺寸的,这个时候最终的视图尺寸应该是多少呢?AT_MOST和UNSPECIFIED的View又该如何决策最终的尺寸呢?
通过Demo演示得到的结果,如果size2比size1大,在不使用滚动条的情况下,子视图超出部分将会被裁减掉,该父亲视图中如果在该子视图后面还有其他视图,那么也将会被裁减掉,但是通过调用其getVisibility函数,还是显示该控件是可见的,所以裁减后的控件依然是有的,只是用户没法看到;在使用滚动效果的情况下,就能将原本被裁减掉的控件通过滚动显示出来。
对于第二个问题,根据源码View的onMeasure函数调用的getDefaultSize函数获知,在默认情况下,控件都有一个最小尺寸,该值可以通过android:minHeight和android:minWidth来设置(无设置缺省为0),在设置了背景的情况下,背景drawable的最小尺寸与前面设置的最小尺寸进行比较,取大值,作为控件的最小尺寸。在UNSPCIFIED情况下,就选用这个最小尺寸,其他情况则根据允许尺寸来。不过这个是默认规则,通过Demo发现,TextView在AT_MOST+size的情况下,并不是以size作为控件的最终尺寸,结果在发现的TextView源码中,重载了onMeasure函数,有价值的代码如下:
……
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
……
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
……
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
……
二、Layout
上述measure过程达到的结果是设定了视图的宽和高,layout过程的作用就是设定视图在父视图中的四个点(分别对应View的四个成员变量,mLeft、mTop、mRight、mBottom) 。同样layout也是被final修饰符限定的不能被重载,不过在ViewGroup中onLayout函数被abstract修饰,即所有派生自ViewGroup的类必须实现onLayout函数,从而实现对其包含的所有的子视图的布局设定。
那么上述的measure结果与layout有什么关系呢,截取ViewRoot和FrameLayout两个类中的部分代码如下:
//ViewRoot的performTraversals函数measure之后对layout的调用代码
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
//FrameLayou的onLayout函数部分源码
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
……
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft = parentLeft;
int childTop = parentTop;
final int gravity = lp.gravity;
if (gravity != -1) {
final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (horizontalGravity) {
case Gravity.LEFT:
childLeft = parentLeft + lp.leftMargin;
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = parentRight - width - lp.rightMargin;
break;
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (getChildCount() > 2) {
throw new IllegalStateException("NPullToFreshContainer can host only two direct child");
}
View headView = getChildAt(0);
View contentView = getChildAt(1);
if(headView != null){
headView.layout(0, -HEAD_VIEW_HEIGHT + mTatolScroll, getMeasuredWidth(), mTatolScroll);// mTatolScroll是下拉的位移值
}
if(contentView != null){
contentView.layout(0, mTatolScroll, getMeasuredWidth(), getMeasuredHeight());
}
if (mFirstLayout) {
HEAD_VIEW_HEIGHT = getChildAt(0).getMeasuredHeight();
mFirstLayout = false;
}
}
1、调用background.draw(canvas)绘制该View的背景
2、调用onDraw(canvas)方法绘制视图本身(每个View都需要重载该方法,ViewGroup不需要实现该方法)
3、调用dispatchDraw(canvas)方法绘制子视图(ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,其内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法)
4、调用onDrawScrollBars(canvas)绘制滚动条
为了说明measure、layout和draw过程的连续性,摘得draw中的源码如下:
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
上述代码中的mBackgroundSizeChanged是个私有成员变量,源码中只能在View的onScrollChanged(int l, int t, int oldl, int oldt) 、layout过程调用的setFrame(int left, int top, int right, int bottom) 和setBackgroundDrawable(Drawable d)这三个函数中对其修改为true。
到这里,除了具体的绘制外,我们对从Activity到View的绘制流程应该比较清楚了。