在了解自定义View之前,首先需要知道View系统的绘制流程是从ViewRoot的performTraversals()方法中开始的,然后在其内部调用View的measure()方法对View进行测量,在measure()方法结束后,继续会在该方法内调用View的layout()方法来对视图进行布局,在layout()结束后,便会继续在该方法内调用View的draw()方法来绘制视图,至此,View的绘制流程结束。
ViewRoot中的代码如下:
private void performTraversals() {
final View host = mView;
...
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
draw(fullRedrawNeeded);
}
View的绘制主要涉及三个函数:
onMeasure():测量View和其内容已决定它的测量宽高。
onLayout():当这个视图应该给每个孩子分配一个大小和位置的时候调用。
onDraw():绘制视图
接下来我们来对View的源码进行分析,以加深对View的绘制流程的理解。
1、onMeasure()
前面我们已经知道,View的绘制流程是从measure开始的,接下来就来分析mesure的源码:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
........
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...........
}
这里只列出核心代码,measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。而在该方法内核心就是调用了onMeasure()方法,接下来让我们看看onMeasure()的源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我们先看看getDefaultSize()是干什么的,如下:
该方法比较简单,在方法里调用getDefaultSize()
和setMeasuredDimension(),
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;
}
之后会在
onMeasure()
方法中调用
setMeasuredDimension()
方法来储存测量的宽度和测量高度。这样
measure
过程就结束了。在方法里面根据传入的参数先是解析出
View
宽高的
mode,
然后在根据
mode
得到宽高的大小值
size
。
通过上面对源码的分析,我们已经知道View的measure的过程了,下面我们对measure过程中的相关知识点进行说明。
1)onMeasure()方法的两个参数信息:widthMeasureSpec和heightMeasureSpec
这两个值分别用于确定视图的宽度和高度的模式和大小,可以通过MeasureSpec解析出来。MeasureSpec包括specMode和specSize即模式和大小。
1.模式
模式包括"EXACTLY","AT_MOST"及"UNSPECIFIED"三种。可以通过Messure.getMode()获取。
EXACTLY(具体的值为 1 << 30 即为:2^30): 表示父视图希望子视图的大小应该是由specSize的值来决定的。例如在配置文件中,将View的大小设置为"固定的大小"或”ATCH_PARENT"。
AT_MOST(具体的值为 2 << 30 即为:2^31): 子视图最多只能是specSize中指定的大小。例如在配置文件中,将View设置为"wrap_content"。
UNSPECIFIED(具体的值为0) : 父视图对于子视图没有任何限制,开发人员可以将视图按照自己的意愿设置成任意的大小。
2. 大小
可以通过MeasureSpec.getSize()来获取。不过需要注意的是,通过该方法获取的大小是配置文件中定义的大小。如果我们自己对该View有特殊的大小要求,则需要根据情况进行处理。
分析完View的绘制流程,接下来让我们看看ViewGroup的绘制流程。
因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次measure过程。因此ViewGroup中定义了一个measureChildren()方法来去测量子视图的大小,如下所示:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
在该方法中会去遍历当前布局下的所有子视图,然后逐个调用measureChild()方法来测量相应子视图的大小,如下:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
//根据ViewGroup的widthMeasureSpec和heightMeasureSpec以及View自身的布局
从而确定每个子View的widthMeasureSpec和heightMeasureSpec.
final int childWidthMeasureSpec = getChildMeasureSpec(
parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(
parentHeightMeasureSpec, mPaddingTop + mPaddingBottom,
lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
而在该方法中最终调用子视图的measure()方法,并把计算出的MeasureSpec传递进去,之后的流程就和前面所介绍的一样。
以上就是系统自动测量View的过程,当然我们如果不想使用系统默认的测量方式,可以按照自己的意愿进行定制只需重写onMeasure()方法中并在放方法中调用setMeasuredDimension()即可。特别提醒:在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,之前调用这两个方法得到的值都会是0。
综上所述,我们知道视图大小的控制是由父视图、布局文件、和视图本身共同完成的,父视图会提供给子视图参考的大小,开发人员可以在XML文件中指定视图的大小,最后视图本身会决定自身的大小。
2、onLayout()
measure过程结束后,视图的大小就已经测量好了,ViewRoot的performTraversals()方法会在measure()结束后调用View的layout()方法来确定视图的位置,如下:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言。从最后两个参数可以看到,把measure中测量出的宽度和高度传到了该layout()方法中.接下来看看Layout()的源码。
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t,
r, b) : setFrame(l, t, r, b);
if (changed
|| (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>) li.mOnLayoutChangeListeners
.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL,
oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
在看方法中首先判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重,接下来会调用onLayout()方法。
观察源码你会发现View中的onLayout()方法是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。
而ViewGroup中的onLayout()方法是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法,否则其中的控件是无法显示的.
在measure和layout都结束了,我们来区分下width和measureWidth的区别:
1)调用的时间不同
getMeasureWidth()在measure()过程结束后就可以获取到了
getWidth()方法要在layout()过程结束后才能获取到。
2)计算方式不同
getMeasureWidth()方法的值是通过setMeasuredDimension()方法来进行设置
getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
3、onDraw()
ViewRoot的performTraversals()方法会在layout()结束后调用View的draw()方法来开始对视图进行绘制。
源码中此方法共分为六步,一般2和5步时不需要的,draw的步骤如下:
1.draw the background,
3.Draw view's content
4.Draw children
6.Draw decorations (scrollbars for instance)
第三步骤核心:onDraw(canvas)方法,需要用到Canvas类,可查阅API了解其功能。