在了解自定义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的绘制流程的理解。
前面我们已经知道,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文件中指定视图的大小,最后视图本身会决定自身的大小。
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()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
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了解其功能。