自定义View(二):深入解析onMeasure()
1、onMeasure()方法的作用
在自定义View中有三个非常重要的方法,onMeasure()、layout()、onDraw(),分别负责View的测量、滑动和绘画,首先我来介绍一下onMeasure()的用法。
首先我们来测试一下View的测量规律,新建一个MyView类继承View,并实重写它的四个构造方法,然后再在xml引用一个MyView,分别测试layout_width和layout_hight为match_parent和wrap_content有什么不同。
看看运行结果:
不出所料,MyView确实是沾满了父布局的空间。然后我们再来测试一下wrap_content。
再运行一次我们可以看到MyView也是占满整个父布局的,如果想要自定义wrap_content则必须要重写onMeasure()方法。
2、源码解析
话不多说,上源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
在onMeasure()方法里面只调用了一个setMeasuredDimension()方法方法,很显然是用来设置View的宽和高的,宽度和高度都是同样原理,我们先以宽度为例看看getSuggestedMinimumWidth()方法源码
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
可以看到这个方法的大概意思就是如果View没有设置背景则返回mMinWidth,如果有设置背景的话则返回背景的宽度,mMinWidth的值默认为0,可以通过setMinimunWidth()方法来设置。我们把重点放在getDefaultSize()方法上,看源码。
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; }
参数size就是上面我所说的getSuggestedMinimumWidth()所返回的值,specSize就是父布局最大的宽度值。然后我先来给大家介绍一下specMode的三种情况分别是什么意思。
UNSPECIFIED:未指定模式,View想多大就多大,不受父布局的控制,一般用于系统内部的测量;
AT_MOST:最大模式,当属性为wrap_content的时候就会被调用,限制了view的最大值为父控件的值,也就是specSize;
EXACTLY:精确模式,当属性为macth_parent的时候就会被调用;
现在可以很清晰知道为什么属性为wrap_content的时候为什么view会占满父控件的所有空间了,所以想要自定义wrap_content情况先View的大小只要重写此方法就可以,由于这个是静态方法不能被重写,我们直接复制改一个方法名字就好了。
3、方法重写
再次运行的时候我们可以发现再也没有出现wrap_content情况下占满全屏,而是它的宽高度都为0了,原因是没有设置背景,getSuggestedMinimumHight()和getSuggestedMinimumWidth()都返回了0,我们可以引用自定义属性来测量View的大小,关于自定义属性的获取,不懂的同学可以看看我上一篇文章。上完整代码
MyView.java
public class MyView extends View{ private int mhight,mwidth; public MyView(Context context) { super(context); } public MyView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyView); //自定义属性 mhight = array.getDimensionPixelSize(R.styleable.MyView_default_hight, 100); mwidth = array.getDimensionPixelSize(R.styleable.MyView_default_width, 100); array.recycle(); //释放资源 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getMyViewDefaultSize(mwidth, widthMeasureSpec), getMyViewDefaultSize(mhight, heightMeasureSpec)); } //重写getDefaultSize public int getMyViewDefaultSize(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: result=size; break; case MeasureSpec.EXACTLY: result = specSize; break; } return result; } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.measuredemo.MainActivity"> <com.measuredemo.MyView android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_gravity="center" android:background="@android:color/darker_gray" app:default_hight="100dp" app:default_width="200dp" /> </FrameLayout>
attr.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyView"> <!--name:自定义属性名,format:自定义属性数据类型--> <attr name="default_width" format="dimension"></attr> <attr name="default_hight" format="dimension"></attr> </declare-styleable> </resources>
运行结果:
4、源码链接
https://github.com/Hasagit/OnMeasureDemo.git