上一篇我们分析了一个xml布局文件到View的过程,顺便也对ActivityThread初窥一番,下面我们就将重点放在View的测量绘制和显示上。
我们首先应该清楚一个概念,即我们常说的“视图大小”,视图大小指的什么呢?应用程序开发时,我们经常在xml文件中使用android:layout_height和android:layout_width;属性来设置宽和高,这指的是视图的大小码?
当然不是!事实上,View系统希望我们明白,一个View画布(Canvas)是没有边界的,即无穷大,程序猿在自定义一个具体的View时,应该在onDraw()方法中像画布中绘制视图的界面,此时程序猿应该认为画布是无穷大的。也就是可以绘制任意多的图形,只要内存够大,但是实际应用中并不会绘制一个无穷大的界面,那么到底应该绘制一个多大的界面呢?对于不同的类型的View其绘制大小会有所不同,一般分为两种情况:一种是内容型视图,一种是图形型视图。
内容型视图会根据自己的内容多少来决定绘制多大,他是自己主宰自己的大小,比如TextView。而图形型视图会根据父视图来决定其大小,父视图觉得你应该多大你就应该多大。我们的layout.xml文件中使用的android:layout_height和android:layout_width;属性实际上是指父视图给该View设置的“窗口大小”,也就是该View所占用的面积了。这就是为什么用“layout_width”和“layout_height”而不是直接用“width”和“height”的原因。
因此,我们所说的“视图大小”实际上就是该View的布局大小,在View类中使用两个变量来保存其大小:mMeasuredWidth和mMeasuredHeight。而View类部还有四个变量,mLeft,mRight、mTop、mBottom。这四个变量是指View在父视图中所占的区域,mRight-mLeft就是mMeasuredWidth的大小,mTop-mButtom就是指mMedauredHeight的大小。
视图的测量(measure )过程本质上是把布局时使用的”相对值“转换为具体值的过程,即把WRAP_CONTENT以及MATCH_PARENT转换为具体的值,如果FrameWork中不适用相对值也就不需要measure过程了。直接layout了。
View系统启动measure过程是从RootView中调用host.measure()开始的,具体说应该是performTraversals()开始,我分析的源码是基于4.1.2的,所以貌似没有看到RootView这个类,但是有RootViewImpl这个类,由于performTraversals()方法很长,我们看下绘制的逻辑即可:
private void performTraversals() { ......<pre> boolean layoutRequested = mLayoutRequested && !mStopped; if (layoutRequested) { final Resources res = mView.getContext().getResources(); if (mFirst) { // make sure touch mode code executes by setting cached value // to opposite of the added touch mode. mAttachInfo.mInTouchMode = !mAddedTouchMode; ensureTouchModeLocally(mAddedTouchMode); } else { if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) { insetsChanged = true; } if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) { mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets); if (DEBUG_LAYOUT) Log.v(TAG, "Visible insets changing to: " + mAttachInfo.mVisibleInsets); } if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { windowSizeMayChange = true; if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) { // NOTE -- system code, won't try to do compat mode. Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else { DisplayMetrics packageMetrics = res.getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; } } } // Ask host how big it wants to be //问后下host(所有界面的根视图)需要多大,即调用measureHierarchy() windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight); }.......... host调用了measureHierarchy()方法来测量子视图需要多大,在这个方法中就会调用performMeasure()方法,由此View的measure()被调用,然后该方法回调onMeasure()。在一般情况下,host是一个ViewGroup实例,该ViewGroup会重载onMeasure(),当然如果host没有重载onMeasure(),则会执行View的onMeasure()。在一般情况下,程序猿需要在重载的onMeasure()方法中逐一对所包含的子视图指定measure过程。为了简化操作,ViewGroup类内部提供了measureChilddWithMargins(),源码如下:
<span style="font-size:18px;">/** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding * and margins. The child must have MarginLayoutParams The heavy lifting is * done in getChildMeasureSpec. * * 此方法用于告诉子视图去测量他本身 */ protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { //获取子视图的LayoutParams的参数 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); //注意到,这里又会调用View的measure()方法来进行测量 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
由上面的源码可以看出,该方法内部会进行一定的参数调整,然后会调用子视图的measure(),这就又会调用onMeasure()方法,因此如果子视图是一个ViewGroup实例,则应该继续调用measureChildWithMargins()方法对下一层的子视图进行measure操作,如果子视图是一个具体的View实例,则在重载的onMeasure()方法内部就不需要再次调用measureChildWithMargins()方法了,从而一次measure()过程结束。
以上过程是View系统定义的一个框架模型,在这个模型中,ViewGroup是一个abstract类型的,应用程序必须实现一个具体的ViewGroup实例,在该实例中,会调用measureChildWithMargins()方法对下一层的子视图进行measure操作,比如LinearLayout、RelativeLayout类里面都有此函数频繁调用操作。但是这个调用不是必需的,因为measure操作的结果仅仅是把layout_width和layout_height的相对值转换为具体值,这些值将在layout过程中辅助父视图对子视图进行布局操作。如果某个ViewGroup实例对子视图的布局不依赖子视图的大小就不需要对所包含的子视图进行measure操作了。
View的measure()方法如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag mPrivateFlags &= ~MEASURED_DIMENSION_SET; //调用onMeasure()进行视图本身测量 onMeasure(widthMeasureSpec, heightMeasureSpec) // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
首先,在View的内部有一个叫MeasureSpec的内部类,源码如下:
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; /** * Measure specification mode: The parent has not imposed any constraint * on the child. It can be whatever size it wants. *意义:没有限制,此时View的设计者可以根据自身的特性设置视图的大小 */ public static final int UNSPECIFIED = 0 << MODE_SHIFT; /** * Measure specification mode: The parent has determined an exact size * for the child. The child is going to be given those bounds regardless * of how big it wants to be. * * 意义:确定的。意思是父视图希望子视图的大小应该是specSize中指定的大小。 * 一般情况下,View的设计者应该遵守该指示,将View的measuredHeight和measuredWidth设置成指定的值 * 当然也可以不遵守 */ public static final int EXACTLY = 1 << MODE_SHIFT; /** * Measure specification mode: The child can be as large as it wants up * to the specified size. * * 意义:最多。意思是子视图的大小最多是specSize中指定的值,在一般情况下,View的设计者应该尽可能小的设置视图的大小, * 并且不能超过specSize,当然也可以超过specSize了</span> * * public static final int AT_MOST = 2 << MODE_SHIFT; /** * 这个方法会在确定measureSpec的时候被调用,在ViewGroupImpl中会被调用。 */ public static int makeMeasureSpec(int size, int mode) { return size + mode; } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } //此measureSpec参数是父视图传递过来的,这样父视图就确定了子视图的widthMeasureSpec和heightMeasureSpec public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } public static String toString(int measureSpec) { int mode = getMode(measureSpec); int size = getSize(measureSpec); StringBuilder sb = new StringBuilder("MeasureSpec: "); if (mode == UNSPECIFIED) sb.append("UNSPECIFIED "); else if (mode == EXACTLY) sb.append("EXACTLY "); else if (mode == AT_MOST) sb.append("AT_MOST "); else sb.append(mode).append(" "); sb.append(size); return sb.toString(); } }
以上是对measureSpec进行解释,该参数是父视图传递给子视图的,用来确定子视图的widthMeasureSpec和heightMeasureSpec值。那么最根本上的measureSpec是怎么产生的呢?这又要从ViewRoot中调用host.measure()开始了。
在host.measure()调用时,参数是childWidthMeasureSpec和childHeightMeasureSpec,这两个变量的赋值最初是通过getRootMeasureSpec()方法获得的,
//注意,此处赋值是在ViewRootImpl类的measureHierachy()中进行的 //本源码是基于4.1.2版本,其它版本未知 childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); //此方法调用会调用View的measure()方法等会看 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);其中参数desiredWindowHeight表示窗口希望大小,参数lp是一个WindowManager.LayoutParams 对象,而lp在一开始就创建了。WindowManager.layoutParams源码如下:
public LayoutParams() { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); type = TYPE_APPLICATION; format = PixelFormat.OPAQUE; }该类代码太多,只看lp的对象中参数是何时创建的吧,看到该参数一开始就都有MATCH_PARENT了,也就是说lp.height的值为MATCH_PARENT
紧接着看上面给childWidthMeasureSpec赋值的getRootMeasureSpec函数吧,该函数同样是在ViewRootImpl中,源码如下:
/* Figures out the measure spec for the root view in a window based on it's * layout params. * * @param windowSize * The available width or height of the window * * @param rootDimension * The layout params for one dimension (width or height) of the * window. * * @return The measure spec to use to measure the root view. * * 这段代码就是产生measureSpec的过程 */ private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; //rootDimension就是lp.width和lp.height switch (rootDimension) { //可以看出来,在xml中定义的相对值在这里会被转换为具体的大小 //由于默认创建的lp.width和lp.height是MATCH_PARECT类型 //所以最根本的measureSpec类型就是EXCTLY类型 case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. //注意,此时View的内部类MeasureSpec的三个模式在这里起作用哟 measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY) break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. //默认的就是EXACTLY类型了,所以我们布局的"fill_parent"是EXACTLY类型 measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec;
OK!确定了childWidthMeasureSpec和childHeightMeasureSpec两个参数后,紧接着调用 了 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);方法,看下源码:
/** *<span style="color:#000099;">此方法用来调用View的measure方法的,因此可以看到,View的整个测量的measure()方法从这里开始调用</span> * @param childWidthMeasureSpec 根据measureSpec获得的 * @param childHeightMeasureSpec 根据measureSpec获取的 */ private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { //子视图调用measure方法了 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }由此可以看到子视图的widthMeasureSpec和heightMeasureSpec(即View中measure()方法中赋值的参数)两个属性的来源过程。
下面上一个布局文件作为案例分析,布局文件如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:width="2" android:text="I will be King"/> </LinearLayout>
至此,我们完成了measure框架的基本概念和思路,下一篇会介绍在代码中是如何实现的,先对这一片文章进行一个总结:
首先在ViewRootImpl类中执行performTraversals()方法。。。好吧,调用的方法略多,总之,在ViewRootImpl类中会根据measureHierarchy()方法、performTraversals()方法、doTraversal()方法以及TraversalRunnable线程(该线程是ViewRootImpl的内部类)、scheduleTraversals()方法等这些方法之后,同时在之前初始化的一个WindowManager.LayoutParams会初始化lp.width和lp.height两个参数,然后再调用getRootMeasureSpec()方法来根据lp.width和lp.height的相对值获取一个measureSpec值,然后再调用performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)方法来调用View的measure()方法,然后回调View的onMeasure()方法并最终确定了子视图本身的大小,可以看出来,measureSpec是父视图在ViewRootImpl类中调用measure()方法的时候传递给子视图的,因此父视图在某种程度上决定了子视图的大小,这也就是为什么说layout_height和layout_width不是指一个View本身的大小的原因。与此同时,在父容器中,会调用measureChildWithMargins()方法不停的对子视图进行测量(如果有相对值的话),在调用这个方法的时候就会将measureSpec测出的结果传递给子视图测量方法。如此循环测量,直到xml布局文件中的视图全部测量完毕为止。