android自定义控件(6)- onMeasure()方法中的MeasureSpec
今天的任务就是详细研究一下protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。如果只是说要重写什么方法有什么用的话,还是不太清楚。先去源码中看看为什么要重写onMeasure()方法,这个方法是在哪里调用的:
一、源码中的measure/onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
实际上是在View这个类中的public final void measure(int widthMeasureSpec, int heightMeasureSpec)方法中被调用的:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... onMeasure(widthMeasureSpec, heightMeasureSpec); ... }
1、measure()
可以看到,measure()这个方法是一个由final来修饰的方法,意味着不能够被子类重写.measure()方法的作用是:测量出一个View的实际大小,而实际性的测量工作,Android系统却并没有帮我们完成,因为这个工作交给了onMeasure()来作,所以我们需要在自定义View的时候按照自己的需求,重写onMeasure方法.而子控件又分为view和viewGroup两种情况,那么测量的流程是怎样的呢,看一下下面这个图你就明白了:
2、onMeasure
onMeasure(int widthMeasureSpec, int heightMeasureSpec)中,两个参数的作用: widthMeasureSpec和heightMeasureSpec这两个int类型的参数,看名字应该知道是跟宽和高有关系,但它们其实不是宽和高,而是由宽、高和各自方向上对应的模式来合成的一个值:其中,在int类型的32位二进制位中,31-30这两位表示模式,0~29这三十位表示宽和高的实际值.其中模式一共有三种,被定义在Android中的View类的一个内部类中:View.MeasureSpec:
①UNSPECIFIED:表示默认值,父控件没有给子view任何限制。------二进制表示:00
②EXACTLY:表示父控件给子view一个具体的值,子view要设置成这些值的大小。------二进制表示:01
③AT_MOST:表示父控件个子view一个最大的特定值,而子view不能超过这个值的大小。------二进制表示:10
二、MeasureSpec
MeasureSpe描述了父View对子View大小的期望.里面包含了测量模式和大小.我们可以通过以下方式从MeasureSpec中提取模式和大小,该方法内部是采用位移计算.
int specMode = MeasureSpec.getMode(measureSpec);//得到模式
int specSize = MeasureSpec.getSize(measureSpec);//得到大小
也可以通过MeasureSpec的静态方法把大小和模式合成,该方法内部只是简单的相加.
MeasureSpec.makeMeasureSpec(specSize,specMode);
每个View都包含一个ViewGroup.LayoutParams类或者其派生类,LayoutParams中包含了View和它的父View之间的关系,而View大小正是View和它的父View共同决定的。
我们平常使用类似于RelativeLayout和LinearLayout的时候,在其内部添加view的时候,不管是布局文件中加入还是在代码中使用addView方法添加,实际上都会调用这个onMeasure方法,而measure和onMeasure中的两个参数,是由各级父控件往子控件/子view进行一层层传递的。我们可以在xml中定义Layout的宽和高的具体的值或宽高的填充方式:matchparent/wrapcontent,也可以在代码中使用LayoutParams设置,而实际上这里设置的值就会对应到上面的measure和onMeasure方法中的两个参数的模式,对应关系如下:
具体的值(如width=200dp)和matchparent/fillparent,对应模式中的MeasureSpec.EXACTLY
包裹内容(width=wrapcontent)则对应模式中的MeasureSpec.AT_MOST
系统调用measure方法,从父控件到子控件的heightMeasureSpec的传递是有一套对应的判断规则的,列表如下:
一个view的宽高尺寸,只有在测量之后才能得到,也就是measure方法被调用之后。大家都应该使用过View.getWidth()和View.getHeight()方法,这两个方法可以返回view的宽和高,但是它们也不是在一开始就可以得到的,比如oncreate方法中,因为这时候measure方法还没有被执行,测量还没有完成,我们可以来作一个简单的实验:自定义一个MyView,继承View类,然后在OnCreate方法中,将其new出来,通过addview方法,添加到现在的布局中。然后调用MyView对象的getWidth()和getHeight()方法,会发现得到的都是0。
onMeasure通过父View传递过来的大小和模式,以及自身的背景图片的大小得出自身最终的大小,然后通过setMeasuredDimension()方法设置给mMeasuredWidth和mMeasuredHeight.
普通View的onMeasure逻辑大同小异,基本都是测量自身内容和背景,然后根据父View传递过来的MeasureSpec进行最终的大小判定,例如TextView会根据文字的长度,文字的大小,文字行高,文字的行宽,显示方式,背景图片,以及父View传递过来的模式和大小最终确定自身的大小.
三、ViewGroup的onMeasure
ViewGroup是个抽象类,本身没有实现onMeasure,但是他的子类都有各自的实现,通常他们都是通过measureChildWithMargins函数或者其他类似于measureChild的函数来遍历测量子View,被GONE的子View将不参与测量,当所有的子View都测量完毕后,才根据父View传递过来的模式和大小来最终决定自身的大小.
在测量子View时,会先获取子View的LayoutParams,从中取出宽高,如果是大于0,将会以精确的模式加上其值组合成MeasureSpec传递子View,如果是小于0,将会把自身的大小或者剩余的大小传递给子View,其模式判定在前面表中有对应关系.
ViewGroup一般都在测量完所有子View后才会调用setMeasuredDimension()设置自身大小,如第一张图所示.
可能看到现在,还是没搞清楚Android系统通过measure和onmeasure一层层传递参数的具体方法。在研究这个问题之前,先来看一下最简单的helloworld的UI层级关系图:
为了方便起见,这里我们使用requestWindowFeature(Window.FEATURE_NO_TITLE);去除标题栏的影响,只看层级关系。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="${relativePackage}.${activityClass}" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world" /> </RelativeLayout>
package com.example.hello; import android.app.Activity; import android.os.Bundle; import android.view.Window; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); } }
UI层级关系图:
可以发现最简单的helloworld的层级关系图是这样的,最开始是一个PhoneWindow的内部类DecorView,这个DecorView实际上是系统最开始加载的最底层的一个viewGroup,它是FrameLayout的子类,然后加载了一个LinearLayout,然后在这个LinearLayout上加载了一个id为content的FrameLayout和一个ViewStub,这个实际上是原本为ActionBar的位置,由于我们使用了requestWindowFeature(Window.FEATURE_NO_TITLE),于是变成了空的ViewStub;然后在id为content的FrameLayout才加载了我们的布局XML文件中写的RelativeLayout和TextView。
那么measure方法在系统中传递尺寸和模式,必定是从DecorView这一层开始的,我们假定手机屏幕是320*480,那么DecorView最开始是从硬件的配置文件中读取手机的尺寸,然后设置measure的参数大小为320*480,而模式是EXCACTLY,传递关系可以由下图示意:
扩展阅读:
MeasureSpec的三个模式详解UNSPECIFIED,EXACTLY和AT_MOST
The basic definition of how a View is sized goes like this:
MeasureSpec.EXACTLY - A view should be exactly this many pixels regardless of how big it actually wants to be.
MeasureSpec.AT_MOST - A view can be this size or smaller if it measures out to be smaller.
MeasureSpec.UNSPECIFIED - A view can be whatever size it needs to be in order to show the content it needs to show.
MeasureSpec.AT_MOST will be applied to views that have been set to WRAP_CONTENT if the parent view is bound in size. For example, your parent View might be bound to the screen size. It's children will be also bound to this size, but it might not be that big. Thus, the parent view will set the MeasureSpec to be AT_MOST which tells the child that it can be anywhere between 0 and screen size. The child will have to make adjustments to ensure that it fits within the bounds that was provided.
In special cases, the bounds do not matter. For example, a ScrollView. In the case of a ScrollView, the height of the child Views are irrelevant. As such, it will supply an UNSPECIFIED to the children Views which tells the children that they can be as tall as they need to be. The ScrollViewwill handle the drawing and placement for them.
翻译一下(直接用软件翻译过来的,不明白的话,下面会通过例子说明):
View的大小基本定义如下:
MeasureSpec.EXACTLY -表示父控件已经确切的指定了子View的大小。
MeasureSpec.AT_MOST - 表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。
MeasureSpec.UNSPECIFIED -父控件没有给子view任何限制,子View可以设置为任意大小。
如果父视图的大小已经指定 则 MeasureSpec.AT_MOST将应用于已设置为WRAP_CONTENT 的子视图。 例如,你的父视图可能绑定到屏幕大小。 它的孩子也会绑定到这个大小,但它可能不是那么大。因此,父视图将MeasureSpec设置为AT_MOST ,它告诉孩子它可以在0和屏幕之间的任何地方。 孩子必须进行调整,以确保它符合提供的界限。
在特殊情况下,界限无关紧要。 例如,一个ScrollView 。 在ScrollView的情况下,子视图的高度是不相关的。 因此,它将向孩子提供一个UNSPECIFIED视图,告诉孩子他们可以像他们需要的一样高。ScrollView将处理它们的绘图和放置。
模式 | 数值 | 描述 |
---|---|---|
UNSPECIFIED | 0 (0x00000000) | 父控件没有给子view任何限制,子View可以设置为任意大小。 |
EXACTLY | 1073741824 (0x40000000) | 表示父控件已经确切的指定了子View的大小。 |
AT_MOST | -2147483648 (0x80000000) | 表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。 |
例子解析
这里分为两个部分例子,例子一是父布局是LinearLayout,子布局是LinearLayout,例子二是是父布局是ScrollView,子布局是LinearLayout.
1)例子一解释EXACTLY和AT_MOST
布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.example.transdotnavi.widget.NormalDot android:id="@+id/dot1" android:layout_width="20dp" android:layout_height="match_parent" android:background="#F7F6F5" > </com.example.transdotnavi.widget.NormalDot> </LinearLayout>
子控件,对于具体解释可以看下面代码里的注释
/** * @author Administrator * 2016-10-22 * * 普通的圆点导航器 * 用于测试父控件是layout,子控件是layout的情况 * 具体可以看layout.xml,里面定义了一个LinearLayout父布局,和一个NormalDot子布局 * 测试机器:160dpi 480*800 */ public class NormalDot extends LinearLayout { public NormalDot(Context context) { super(context); } @SuppressLint("NewApi") public NormalDot(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public NormalDot(Context context, AttributeSet attrs) { super(context, attrs); } // MeasureSpec.AT_MOST = -2147483648 [0x80000000]; // MeasureSpec.EXACTLY = 1073741824 [0x40000000]; // MeasureSpec.UNSPECIFIED = 0 [0x0]; // 父布局LinearLayout固定width,height是match_parent(其实如果设置为wrap_content它的宽高也是等于屏幕宽高) // 情况一: // 当宽或高设为确定值时:即width=20dp,height=30dp,或者为match_parent。它会使用MeasureSpec.EXACTLY测量模式(表示父控件已经确切的指定了子View的大小) // 情况二: // 当宽或高设为wrap_content时,它会使用MeasureSpec.AT_MOST测量模式(表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小) // 情况三: // MeasureSpec.UNSPECIFIED,一般是在特殊情况下出现,如在父布局是ScrollView中才会出现这种测量模式 // 注意: // 父视图可能绑定到屏幕大小。 它的孩子也会绑定到这个大小,但它可能不是那么大。 // 因此,父视图将MeasureSpec设置为AT_MOST , // 它告诉孩子它可以在0和屏幕之间的任何地方。 孩子必须进行调整,以确保它符合提供的界限。 // 通过一个例子解释上面面这句化的意思 // 父布局宽高都设置为match_parent,这时候父布局大小就是屏幕大小,这时候,他的子视图设置为width=20dp,height=wrap_content // 看打出了的日志可以看到宽是20,高是800,这样的话我们应该能在屏幕的左边看到一条白色的宽为20的竖线, // 但是事实上,我们在界面是没有看到这条竖线的,这是因为子布局可以占据0到屏幕大小这个范围,但是子布局通过调整,将height设置为0了,所以 // 我们看不到任何图像 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int mode = MeasureSpec.getMode(widthMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); int mode2 = MeasureSpec.getMode(heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); Log.i("lgy", "mode:"+mode+" width:"+width); Log.i("lgy", "mode2:"+mode2+" height:"+height); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }
2)例子二 解析UNSPECIFIED模式
布局文件
<?xml version="1.0" encoding="utf-8"?> <com.example.transdotnavi.widget.LScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.transdotnavi.widget.NormalDot2 android:id="@+id/dot2" android:layout_width="50dp" android:layout_height="50dp" android:background="#F7F6F5" > <!-- <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="TextView" /> --> </com.example.transdotnavi.widget.NormalDot2> </com.example.transdotnavi.widget.LScrollView>
子控件
/** * @author Administrator * 2016-10-22 * * 普通的圆点导航器 * 用于测试父控件是ScrollView,子控件是layout的情况 * 具体可以看layout2.xml,里面定义了一个ScrollView父布局,和一个NormalDot2子布局 * 测试机器:160dpi 480*800 */ public class NormalDot2 extends LinearLayout { public NormalDot2(Context context) { super(context); } /** * @param context * @param attrs * @param defStyleAttr */ @SuppressLint("NewApi") public NormalDot2(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // TODO Auto-generated constructor stub } /** * @param context * @param attrs */ public NormalDot2(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub } // MeasureSpec.AT_MOST = -2147483648 [0x80000000]; // MeasureSpec.EXACTLY = 1073741824 [0x40000000]; // MeasureSpec.UNSPECIFIED = 0 [0x0]; // 这里主要展示的是MeasureSpec.UNSPECIFIED测量模式 // 情况一:垂直的ScrollView // 父布局ScrollView宽高都是match_parent,子布局是一个NormalDot2设置宽高都是50dp // 从打出的日志可以看到,宽是以MeasureSpec.EXACTLY模式测量的,width=50, // 而高是以MeasureSpec.UNSPECIFIED模式测量的,height=0 // 这时候如果NormalDot2布局里没有任何控件,那么就不会显示任何东西 // 但如果在里面加个TextView,那么这个textView就会显示出来(当然这个textView是有内容的,否则也不会显示出来), // 但这时候height还是以MeasureSpec.UNSPECIFIED模式测量,height=0 // 情况二:水平的ScrollView // 父布局HorizontalScrollView宽高都是match_parent,子布局是一个NormalDot2设置宽高都是50dp // 从打出的日志可以看到,宽是MeasureSpec.UNSPECIFIED模式测量的,width=0, // 而高是以MeasureSpec.EXACTLY模式测量的,height=50 // 这时候如果NormalDot2布局里没有任何控件,那么就不会显示任何东西 // 但如果在里面加个TextView,那么这个textView就会显示出来(当然这个textView是有内容的,否则也不会显示出来), // 但这时候width还是以MeasureSpec.UNSPECIFIED模式测量,width=0 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int mode = MeasureSpec.getMode(widthMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); int mode2 = MeasureSpec.getMode(heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); Log.i("lgy", "============mode:"+mode+" width:"+width); Log.i("lgy", "=============mode2:"+mode2+" height:"+height); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }
父控件(ScrollView)
/** * @author LGY * @time 2016-10-23 * @action */ public class LScrollView extends HorizontalScrollView //ScrollView { /** * @param context */ public LScrollView(Context context) { super(context); // TODO Auto-generated constructor stub } /** * @param context * @param attrs * @param defStyleAttr */ public LScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // TODO Auto-generated constructor stub } /** * @param context * @param attrs */ public LScrollView(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub } /* (non-Javadoc) * @see android.widget.ScrollView#onMeasure(int, int) */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int mode = MeasureSpec.getMode(widthMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); int mode2 = MeasureSpec.getMode(heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); Log.i("lgy", "ScrollViewmode:"+mode+" width:"+width); Log.i("lgy", "ScrollViewmode2:"+mode2+" height:"+height); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }
4.源码地址
http://download.csdn.net/detail/lgywsdy/9757471
5.参考文章
http://stackoverflow.com/questions/16022841/when-will-measurespec-unspecified-and-measurespec-at-most-be-applied
参考链接