View的测量
今天讲的是View的测量,这里就涉及到一个变量MeasureSpec;该变量google开发人员将其设计得很巧妙;里面其实装了两个值:Mode和Size;需要了解他们之间怎么转换可以看看我之前写的 View静态类MeasureSpec
入正题的View的测量当然看它的onMeasure()方法
onMeasure()
onMeasure源码就是再调用一个setMeasuredDimension()方法;
先看看源码对onMeasure该方法的解释:
Measure the view and its content to determine the measured width and the
measured height. This method is invoked by {@link #measure(int, int)} and
should be overridden by subclasses to provide accurate and efficient
measurement of their contents.
大概意思:测量该View和它的内容决定这测量的宽高。该方法被measure()方法调用和继承它的子类需要重写该方法以提供测量其内容。那就是说一般我们的自定义View都需要重写此方法。继续看源码:
setMeasuredDimension()
This method must be called by {@link #onMeasure(int, int)} to store the
* measured width and measured height. Failing to do so will trigger an
* exception at measurement time.
大概意思:该方法一定要被onMeasure()调用去存储测量后的宽高。不这样做就会在测量时间上出现异常。
所以我们的自定义View最终调用setMeasureDimension()即可,无需关心它里面干什么。
getDefaultSize(int size, int measureSpec)
这个就是setMeasureDimension方法传入的参数。看看干什么的:
getDefaultSize这里简单解释一下:
- MeasureSpec.UNSPECIFIED 为父类不确定模式,大小由子类View确定
- MeasureSpec.AT_MOST 为最大化模式,即在这个撑满这个最大值
- MeasureSpec.EXACTLY 为精确模式,即平时具体设定多少Dp,Sp或MATCH_PARENT都是基于这个模式;
以上三种模式是我基于写代码看源码时的自己理解,具体你可以看下面官方的解释:
回来解释一下getDefaultSize方法,其实就是作了一个十分简单的测量:当UNSPECIFIED模式时,取传入来的默认值,即源码传入的0;
当AT_MOST和EXACTLY模式,获取measureSpec的size值。这种情况下,会出现什么问题呢?
就是如果我们的自定义View不重写onMeasure方法时候;
在布局文件写WRAP_CONTENT与MATCH_PARENT的时候效果是一样的。当然具体的Dp就当然是具体啦。
所以我们在写自定义View时候不重写onMeasure该方法,就会发现WRAP_CONTENT是没有效果的。原因就是这样啦。
其实有看个我的ImageView源码分析ImageView核心源码分析都会发现每个View都会对onMeasure方法作不同程度的处理。下面看看一个简单例子到底Android系统是如何测量宽高的。
例子分析Android测量
<com.text.CostomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#bbbaaa"
>
<Button
android:text="@string/hello_world"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#aaabbb"
android:id="@+id/textView1" />
</com.text.CostomViewGroup >
布局文件的代码很简单:一个自定义的ViewGroup和Button
自定义CostomViewGroup中的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//调用ViewGroup类中测量子类的方法
measureChildren(widthMeasureSpec, heightMeasureSpec);
//调用View类中默认的测量方法
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}
效果图:
Paste_Image.png
measureChildren(widthMeasureSpec, heightMeasureSpec);
Paste_Image.png Paste_Image.png上面的截图是测量子View的,其实也很简单:
将可以显示的子View进行获取measureSpec;而子View的MeasureSpec是由parentMeasureSpec和padding和子View自己的宽高决定的。即下面这句代码:
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);
最后就是child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
即测量子View的真正宽高。这里measure方法就会去调用onMeasure方法。
这里总结一下:
- 一个ViewGroup的宽高是由里面的子View决定的。当然这里只是个抽象描述。具体应该包含padding、MeasureSpec的Mode、子View的宽高;
- ChildView即子View的宽高是由 自己的宽高(即在布局文件写的宽高)、padding、父MeasureSpec决定
- 最后一个其实在上面两个总结的基础之上得到:View的测量是一个迭代进行的,相互制约。
最后再看看getChildMeasureSpec(int spec, int padding, int childDimension)
//传入的参数:parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width
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) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
最终总结
看完上边的方法我用自己的理解作了下面的总结:
当父是EXACTLY 模式下:
- 子View有具体的数值就取具体的数值(前提不能大于父的宽高值)
- 子View是MATCH_PARENT,就给父的宽高值它
- 子View是WRAP_CONTENT,就给父的最大值它(该值只是一个最大值非子View的最终值)
当父是AT_MOST模式下:
- 子View有具体的数值就取具体的数值(前提不能大于父的宽高值)
- 子View是MATCH_PARENT,就给父的宽高值它,这里与上面不同的是子View的Mode为AT_MOST,因为需要后面child.measure(childWidthMeasureSpec, childHeightMeasureSpec);方法中的onMeasure给它具体值和父为AT_MOST确定不了自己的大小。
- 子View是WRAP_CONTENT,就给父的最大值它(该值只是一个最大值非子View的最终值)情况和上面一样。
最后我想说一下,View的测量其实你只要记住一个View的宽高值是由父View的模式和lp的宽高和padding影响就足够了。因为你看源码(View的源码和ViewGroup的源码)会发现当MatchaParent或WrapContent的时候,源码是没有给出你一个确切的值。WrapContent更是需要你自己定义一个值,如ImageView是WrapContent的时候就是获取图片的宽高值。