onMeasure流程解析
0、预备知识
我们的手机屏幕的布局其实是嵌套的,最外层是一个phoneWindow,这个view和手机屏幕一样大,里面是一个frameLayout,再里面才是我们自己写的布局文件。
我们在绘制控件前必须要经历measure的过程,这个过程需要从最外层的PhoneWindow开始进行。phonewindow调用内部frameLayout的measure,frameLayout又调用内部view的onMeasure,依次类推。总之就是不断的调用自己内部view的measure方法(measure中会调用onMeasure),直到到达了最内部的view。其实说白了就是这么个流程,下面就是onMeasure方法:
void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
这个方法真是简单直观,让人一下子觉得都没什么可说的了(真的么?)
没有返回值,只有两个参数,我们唯一需要分析的就是这两个参数了。然而,通过上面的分析,我们已经知道了这两个参数肯定是从它外层的view传来的,毕竟外层的view调用了它的measure嘛,而onMeasure又会调用measure,额,貌似又没啥可说的了。姑且说下这两个参数是怎么传进来的,参数的意义又是什么吧。
1.childWidthMeasureSpec和childHeightMeasureSpec
首先最外层的view调用了performTraversals方法,得到了childWidthMeasureSpec和childHeightMeasureSpec两个值:
private void performTraversals() { // ………省略宇宙尘埃数量那么多的代码……… if (!mStopped) { // ……省略一些代码 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // ……省省省 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } // ………省略人体细胞数量那么多的代码……… }
我们看看上面代码中的三行代码是什么意思:
第一行、第二行的操作一样,我们分析第一行就得了。得到widthMeasureSpec的途径是调用getRootMeasureSpec做的,给他传的是width和lp.width。这两个值我们太熟悉了,每个view里面都有。
1.view的width:这个不用多说,需要说明的是最外层的view(phoneWindow)宽度肯定和手机屏幕一样,而手机屏幕的宽度是确定的,于是这个值在最外层就有了初始值。
2.lp.width:
TextView view = (TextView) findViewById(R.id.text); ViewGroup.LayoutParams lp = view.getLayoutParams(); lp.width; lp.height;
这个东西我们也常见,布局属性嘛!需要说明的是最外层view的lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定。
现在我们知道参数了意义了,而且这些参数都是我们熟悉的,但我好好奇,这个getRootMeasure中到底做了什么不得了的事情!
// 参数解释: width/height lp.width/lp.height private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window不能调整其大小,强制使根视图大小与Window一致 measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window可以调整其大小,为根视图设置一个最大值 measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window想要一个确定的尺寸,强制将根视图的尺寸作为其尺寸 measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
我们终于遇到不清楚的常量了,不过不用怕!常量嘛,其实就是标识的作用,来看看这些常量是什么意思:
1. EXACTLY
表示父视图“希望”子视图的大小应该是由specSize的值来决定的!
系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
2. AT_MOST
表示子视图“最多”只能是specSize中指定的大小!
开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
3. UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制!
这种情况比较少见,不太会用到。
现在分析如下:
①当外层view是match_parent时,我们允许内层view在外层view的所在区域内绘制自己。
②当外层view是wrap_content时,我们应在最外层view的所在区域内尽可能小的绘制内层view。
③如果外层view有固定大小,那么我们内层view的绘制区域就是lp.xxx,因为这时候lp.xxx = 确定值,所以在这种情况下内层view的绘制区域就是外部view的绘制区域。
最终我们通过MeasureSpec.makeMeasureSpc来把这两个值拼接起来,变成一个64位的值,传递下去。
我隐隐的感到,以后我们还是要把这个拼接好的值解析出来的,感觉MeasureSpec以后还会见到。
2.探寻onMeasure方法
view的measure如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { // 省略部分代码…… /* * 判断当前mPrivateFlags是否带有PFLAG_FORCE_LAYOUT强制布局标记 * 判断当前widthMeasureSpec和heightMeasureSpec是否发生了改变 */ if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // 如果发生了改变表示需要重新进行测量此时清除掉mPrivateFlags中已测量的标识位PFLAG_MEASURED_DIMENSION_SET mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // 测量View的尺寸 onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); setMeasuredDimension((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } /* * 如果mPrivateFlags里没有表示已测量的标识位PFLAG_MEASURED_DIMENSION_SET则会抛出异常 */ if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } // 如果已测量View那么就可以往mPrivateFlags添加标识位PFLAG_LAYOUT_REQUIRED表示可以进行布局了 mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } // 最后存储测量完成的测量规格 mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension }
过程如下:
判断传入的参数是否和之前的一样,如果一样,那么就不做改变(这点是优化中需要注意的,在setVisible中也有类似的判断)。
如果是强制测量,那么不管现在的参数和之前的一不一样,都进行重新测量。
如果有缓存则用缓存(这里的缓存也如我所料,用了LongSparseLongArray这种散列表),没有缓存就调用onMeasure。
测量完毕后设置标志位并且存储测量后的数据,最后把这些结果放入缓存。
多说一点:我们发现用缓存后调用了一个方法:setMeasureDimension。我们有理由推断,在onMeasure中肯定也需要调用这个方法来设置最终的view大小。
我们知道可以重写onMeasure来做自己的测量工作,在此之前我们先来看看默认的实现方案,之后我们就可以照猫画虎地做啦。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
这里面其实就一个setMeasureDimension,验证了之前的猜想。这个方法传入了width和height,来最终确定控件的大小。getDefaultSize方法给人的感觉是通过传入的值和建议的值的得到一个最终合理的值,我感觉里面有可能用到了各种比较、取大小、取上下限等操作。
/** * Utility to return a default size. Uses the supplied size if the * MeasureSpec imposed no constraints. Will get larger if allowed * by the MeasureSpec. * * @param size Default size for this view * @param measureSpec Constraints imposed by the parent * @return The size this view should be. */ 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; }
好嘛,MeasureSpec你又出现了!现在我们把之前传过来的值分前32位后32位得到了mode和size,如果外层view没有设置任何测量标准,那么就用推荐的size,否则就是用外层view传来的size。
注意:上述代码中当模式为AT_MOST和EXACTLY时均会返回计算出的测量尺寸,还记得上面我们说的PhoneWindow、DecorView么从它们那里获取到的测量规格层层传递到我们的自定义View中,这就是为什么我们的自定义View在默认情况下不管是math_parent还是warp_content都能占满父容器的剩余空间的原因。
得到系统推荐的size也很简单:
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
如果背景为空那么我们直接返回mMinWidth最小宽度否则就在mMinWidth和背景最小宽度之间取一个最大值,反正就是得到控件的最小大小。哈哈,这里又印证了我的猜想,用到了比较和取大小来得到最终的结果。
3.自定义onMeasure
我们已经分析清楚了这样一个流程,那么我们就来自定义一个onMeasure方法吧~
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(300, 300); }
哈哈,太简单了,完全没有任何难度嘛。咦,之前说道的UNSPECIFIED、EXACTLY、AT_MOST这些常量好像没啥用了,size也没有用到。是不是简单的有点过分,不太好吧。我们之前解释了这些常量的意思(忘记了请回到上面看看,顺便看看红字),他们其实也就是个建议,至于开发者想要怎么设置view的大小,android框架是不管的。虽然如此,我们还是应该做个听话的孩子,让view的最终测量尺寸由view本身和外层view共同决定才好。下面的代码展示了我们应如何更好的自定义onMeasure。
下面给个源码:
package com.example.kale.text; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.View; /** * @author Jack Tony * @date 2015/8/3 */ public class TestView extends View { Bitmap mBitmap; public TestView(Context context) { this(context, null); } public TestView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TestView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } public void initView() { mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.kale); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 声明一个临时变量来存储计算出的测量值 int resultWidth = 0; // 获取宽度测量规格中的mode int modeWidth = MeasureSpec.getMode(widthMeasureSpec); // 获取宽度测量规格中的size int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); /* * 如果外层view心里有数 */ if (modeWidth == MeasureSpec.EXACTLY) { // 取外层view给的大小 resultWidth = sizeWidth; } /* * 如果外层view没数 */ else { // 可要自己看看自己需要多大了 resultWidth = mBitmap.getWidth(); /* * 如果外层view给的是一个限制值 */ if (modeWidth == MeasureSpec.AT_MOST) { // 那么自己的需求就要跟限制比比看谁小要谁 resultWidth = Math.min(resultWidth, sizeWidth); } } resultWidth += getPaddingLeft() + getPaddingRight(); int resultHeight = 0; int modeHeight = MeasureSpec.getMode(heightMeasureSpec); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); if (modeHeight == MeasureSpec.EXACTLY) { resultHeight = sizeHeight; } else { resultHeight = mBitmap.getHeight(); if (modeHeight == MeasureSpec.AT_MOST) { resultHeight = Math.min(resultHeight, sizeHeight); } } resultHeight += getPaddingTop() + getPaddingBottom(); // 设置测量尺寸 setMeasuredDimension(resultWidth, resultHeight); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawBitmap(mBitmap, getPaddingLeft(), getPaddingRight(), null); } }
4.viewGroup的measureChild
measureChildWithMargins和measureChildren类似只是加入了对Margins外边距的处理,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); } } }
这里的操作是遍历viewgroup中的view,然后在view可见的前提下调用measureChild方法。这里传入三个值,参数都是大家熟悉的,就不错过多说明了。
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { // 获取子元素的布局参数 final LayoutParams lp = child.getLayoutParams(); /* * 将父容器的测量规格已经上下和左右的边距还有子元素本身的布局参数传入getChildMeasureSpec方法计算最终测量规格 */ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); // 调用子元素的measure传入计算好的测量规格 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
getChildMeasureSpec这个方法和getRootMeasureSpec很相似,那么我们主要就是看看getChildMeasureSpec方法是如何确定最终测量规格的:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { // 获取父容器的测量模式和尺寸大小 int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); // 这个尺寸应该减去内边距的值 int size = Math.max(0, specSize - padding); // 声明临时变量存值 int resultSize = 0; int resultMode = 0; /* * 根据模式判断 */ switch (specMode) { case MeasureSpec.EXACTLY: // 父容器尺寸大小是一个确定的值 /* * 根据子元素的布局参数判断 */ if (childDimension >= 0) { //如果childDimension是一个具体的值 // 那么就将该值作为结果 resultSize = childDimension; // 而这个值也是被确定的 resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT // 那么就将父容器的大小作为结果 resultSize = size; // 因为父容器的大小是被确定的所以子元素大小也是可以被确定的 resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT // 那么就将父容器的大小作为结果 resultSize = size; // 但是子元素的大小包裹了其内容后不能超过父容器 resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.AT_MOST: // 父容器尺寸大小拥有一个限制值 /* * 根据子元素的布局参数判断 */ if (childDimension >= 0) { //如果childDimension是一个具体的值 // 那么就将该值作为结果 resultSize = childDimension; // 而这个值也是被确定的 resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT // 那么就将父容器的大小作为结果 resultSize = size; // 因为父容器的大小是受到限制值的限制所以子元素的大小也应该受到父容器的限制 resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT // 那么就将父容器的大小作为结果 resultSize = size; // 但是子元素的大小包裹了其内容后不能超过父容器 resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.UNSPECIFIED: // 父容器尺寸大小未受限制 /* * 根据子元素的布局参数判断 */ if (childDimension >= 0) { //如果childDimension是一个具体的值 // 那么就将该值作为结果 resultSize = childDimension; // 而这个值也是被确定的 resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT // 因为父容器的大小不受限制而对子元素来说也可以是任意大小所以不指定也不限制子元素的大小 resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT // 因为父容器的大小不受限制而对子元素来说也可以是任意大小所以不指定也不限制子元素的大小 resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } // 返回封装后的测量规格 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
我们通过上面的代码知晓了内部view的最终测量值是由它自身和外部的viewGroup共同决定的,最终把这个算好的值传入到了内部view的measure中,measure由会把这些值传到onMeasure中。需要再次说明的是,我们虽然在onMeasure中接收到了这些值,但它们仅仅是一个建议,我们是仍旧还是可以随意指定的~
参考自:
http://blog.csdn.net/aigestudio/article/details/42989325
http://blog.csdn.net/guolin_blog/article/details/16330267