标签:
我们的手机屏幕的布局其实是嵌套的,最外层是一个phoneWindow,这个view和手机屏幕一样大,里面是一个frameLayout,再里面才是我们自己写的布局文件。
我们在绘制控件前必须要经历measure的过程,这个过程需要从PhoneWindow不断的调用子view的measure方法(measure中会调用onMeasure),直到到达了最内部的view。
void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
这个方法真是简单直观,让人一下子觉得都没什么可说的了(真的么?)
没有返回值,只有两个参数,它竟然能去测量控件,我们来分析一下这个流程是怎么做的吧。
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做的,给他传递的参数是宽、和lp.width。这两个值我们太熟悉了,每个view里面都有。
1.view的width:这个不用多说,需要说明的是最外层的view宽度肯定和手机屏幕一样,而手机屏幕的宽度是确定的,于是这个值在最外层就有了初始值。
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的width、height来绘制自己。
②当外层view是wrap_content时,我们应在最外层view的width、height大小中尽可能小的绘制内层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本身何其父容器共同决定才好。以后我们会分析如何更好的自定义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); } }
参考自:
http://blog.csdn.net/aigestudio/article/details/42989325
http://blog.csdn.net/guolin_blog/article/details/16330267
标签:
原文地址:http://www.cnblogs.com/tianzhijiexian/p/4699615.html