前言:虽然梦想为了现实暂时会妥协,但终有一天,它将会实现
很多人认为自定义控件中最不能理解的就是onMeasure方法了,只知道它是用来测量控件大小的,至于什么时候测量,怎么测量就不太清楚了。而且网上很多的博客都是错误的(当然作者可能是理解的,这东西很多时候需要意会很难言传)。这篇文章我们就深入学习onMeasure相关的知识点,带着问题,我们一步步揭开onMeasure的神秘面纱。注意:本文不打算分析源码,会以打log的形式来分析。源码分析的话会在后期单独来写的。
言归正传,接触到一个类,当你不太了解他,如果贸然翻阅源码只会让你失去方向,不知从哪里下手;所以我们应该从官方文档着手,看看它是个什么东西,里面有哪些属性和方法,都是用来干嘛的。下面我们看看官方文档对View的介绍:
View这个类代表用户界面组件的基本构建块。View在屏幕上占据一个矩形区域,并负责绘制和事件处理。View是用于创建交互式用户界面组件(按钮、文本等)的基础类。它的子类ViewGroup是所有布局的父类,它是一个可以包含其他view或者viewGroup并定义它们的布局属性的看不见的容器。
实现一个自定义View,你通常会覆盖一些framework层在所有view上调用的标准方法。你不需要重写所有这些方法。事实上,你可以只是重写onDraw(android.graphics.Canvas)。
然后需要知道有一些方式都是干什么用的,你不用去别处找了,我已经帮你找到了:
画红笔表示不常用(至今没见过)
从上面官方文档介绍我们可以知道,View是所有控件(包括ViewGroup)的父类,它里面有一些常见的方法(上表)
由于View绘制中onMeasure最难理解,下面开始讲解onMeasure:
1. onMeasure什么时候会被调用
onMeasure方法的作用是测量控件的大小,什么时候需要测量控件的大小呢?我们举个例子,做饭的时候我们炒一碗菜,炒菜的过程我们并不要求知道这道菜有多少分量,只有在菜做熟了我们要拿个碗盛放的时候,我们才需要掂量拿多大的碗盛放,这时候我们就要对菜的分量进行估测。
而我们的控件也正是如此,创建一个View(执行构造方法)的时候不需要测量控件的大小,只有将这个view放入一个容器(父控件)中的时候才需要测量,而这个测量方法就是父控件唤起调用的。当控件的父控件要放置该控件的时候,父控件会调用子控件的onMeasure方法询问子控件:“你有多大的尺寸,我要给你多大的地方才能容纳你?”,然后传入两个参数(widthMeasureSpec和heightMeasureSpec),这两个参数就是父控件告诉子控件可获得的空间以及关于这个空间的约束条件(好比我在思考需要多大的碗盛菜的时候我要看一下碗柜里最大的碗有多大,菜的分量不能超过这个容积,这就是碗对菜的约束),子控件拿着这些条件就能正确的测量自身的宽高了。
2. onMeasure方法执行流程
上面说到onMeasure方法是由父控件调用的,所有父控件都是ViewGroup的子类,ViewGroup是一个抽象类,它里面有一个抽象方法onLayout,这个方法的作用就是摆放它所有的子控件(安排位置),因为是抽象类,不能直接new对象,所以我们在布局文件中可以使用View但是不能直接使用 ViewGroup。
在给子控件确定位置之前,必须要获取到子控件的大小(只有确定了子控件的大小才能正确的确定上下左右四个点的坐标),而ViewGroup并没有重写View的onMeasure方法,也就是说抽象类ViewGroup没有为子控件测量大小的能力,它只能测量自己的大小。但是既然ViewGroup是一个能容纳子控件的容器,系统当然也考虑到测量子控件的问题,所以ViewGroup提供了三个测量子控件相关的方法(measuireChildren\measuireChild\measureChildWithMargins),只是在ViewGroup中没有调用它们,所以它本身不具备为子控件测量大小的能力,但是他有这个潜力哦。
为什么都有测量子控件的方法了而ViewGroup中不直接重写onMeasure方法,然后在onMeasure中调用呢?因为不同的容器摆放子控件的方式不同,比如RelativeLayout,LinearLayout这两个ViewGroup的子类,它们摆放子控件的方式不同,有的是线性摆放,而有的是叠加摆放,这就导致测量子控件的方式会有所差别,所以ViewGroup就干脆不直接测量子控件,他的子类要测量子控件就根据自己的布局特性重写onMeasure方法去测量。这么看来ViewGroup提供的三个测量子控件的方法岂不是没有作用?答案是NO,既然提供了就肯定有作用,这三个方法只是按照一种通用的方式去测量子控件,很多ViewGruop的子类测量子控件的时候就使用了ViewGroup的measureChildxxx系列方法;还有一个作用就是为我们自定义ViewGroup提供方便咯,自定义ViewGroup我会在以后的博客中专门探讨,这里就不大费篇章了。
测量的时候父控件的onMeasure方法会遍历他所有的子控件,挨个调用子控件的measure方法,measure方法会调用onMeasure,然后会调用setMeasureDimension方法保存测量的大小,一次遍历下来,第一个子控件以及这个子控件中的所有子控件都会完成测量工作;然后开始测量第二个子控件…;最后父控件所有的子控件都完成测量以后会调用setMeasureDimension方法保存自己的测量大小。值得注意的是,这个过程不只执行一次,也就是说有可能重复执行,因为有的时候,一轮测量下来,父控件发现某一个子控件的尺寸不符合要求,就会重新测量一遍。
下面举个简单的例子:
下面是测量的时序图:
3. MeasureSpec类
在学习onMasure方法之前,我们要先了解他的参数中的一个类MeasureSpec,知己知彼才能百战百胜 。
跟踪一下源码,发现它是View中的一个静态内部类,是由尺寸和模式组合而成的一个值,用来描述父控件对子控件尺寸的约束,看看他的部分源码,一共有三种模式,然后提供了合成和分解的方法:
/**
* measurespec封装了父控件对他的孩子的布局要求。
* 一个measurespec由大小和模式。有三种可能的模式:
*/
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//父控件不强加任何约束给子控件,它可以是它想要任何大小。
public static final int UNSPECIFIED = 0 << MODE_SHIFT; //0
//父控件决定给孩子一个精确的尺寸
public static final int EXACTLY = 1 << MODE_SHIFT; //1073741824
//父控件会给子控件尽可能大的尺寸
public static final int AT_MOST = 2 << MODE_SHIFT; //-2147483648
/**
* 根据给定的尺寸和模式创建一个约束规范
*/
public static int makeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
/**
* 从约束规范中获取模式
*/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* 从约束规范中获取尺寸
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
这样说起来还是有点抽象,举一个小栗子大家就知道这三种约束到底是什么意思。我们自定义一个View,为了方便起见,让它继承Button,布局文件中设置不同的宽高条件,然后在onMeasure方法中打印一下他的参数(int widthMeasureSpec, int heightMeasureSpec)到底是个什么东东:
public class MyView extends Button {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取宽的模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取高的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸
int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸
Log.e("itydl", "宽的模式:"+widthMode);
Log.e("itydl", "高的模式:"+heightMode);
Log.e("itydl", "宽的尺寸:"+widthSize);
Log.e("itydl", "高的尺寸:"+heightSize);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
情形1,让按钮包裹内容:
<?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:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.itydl.lsn_ui_analysis.MainActivity">
<com.itydl.lsn_ui_analysis.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="按钮------我是按钮"
android:background="#ff0000"
/>
</LinearLayout>
log打印:
表示父亲是确定的大小,孩子的layoutParams为wra_content的话,它的宽高模式为AT_MOST 宽高尺寸为父控件的剩余大小(可能需要减掉padding值)。
情形2,让按钮填充父窗体:
<?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:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.itydl.lsn_ui_analysis.MainActivity">
<com.itydl.lsn_ui_analysis.MyView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="20sp"
android:text="按钮------我是按钮"
android:background="#ff0000"
/>
</LinearLayout>
log打印:
表示父亲是确定的大小,孩子的layoutParams为match_parent的话,它的宽高模式为EXACTLY,宽高尺寸为父控件的剩余大小(可能需要减掉padding值)。
情形3,给按钮的宽设置为具体的值:
<?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:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.itydl.lsn_ui_analysis.MainActivity">
<com.itydl.lsn_ui_analysis.MyView
android:layout_width="100dp"
android:layout_height="match_parent"
android:textSize="20sp"
android:text="按钮------我是按钮"
android:background="#ff0000"
/>
</LinearLayout>
表示父亲是确定的大小,孩子的layoutParams为写死的一个具体大小值,那么孩子对应的宽高模式为EXACTLY,宽高尺寸就为layoutParams对应的值大小。
其实除了这三种之外,还有很多情况。咱们自定义View的宽高模式以及尺寸受到父亲的MeasureSpec和自己的LayoutParams有关系,此外还和View的margin及padding有关。
下面提供一张表格,来说明View的MeasureSpec创建规则:
针对上标做一个总结:
4. 从ViewGroup的onMeasure到View的onMeasure
①. ViewGroup中三个测量子控件的方法:
通过上面的介绍,我们知道,如果要自定义ViewGroup就必须重写onMeasure方法,在这里测量子控件的尺寸。子控件的尺寸怎么测量呢?ViewGroup中提供了三个关于测量子控件的方法,在下面给出:
1
/**
*遍历ViewGroup中所有的子控件,调用measuireChild测量宽高
*/
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);
}
}
}
2
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
3
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这三个方法分别做了那些工作大家应该比较清楚了吧?measureChildren 就是遍历所有子控件挨个测量(系统帮我们遍历所有的孩子进行测量),最终测量子控件的方法就是measureChild(我们需要自己遍历进行测量) 和measureChildWithMargins (我们需要自己遍历进行测量)了(显然,在自定义ViewGroup的时候它有多个孩子有时候可能直接调用measureChildren方法交给系统给我们进行测量),我们先了解几个知识点:
measureChildWithMargins跟measureChild的区别就是父控件支不支持margin属性
支不支持margin属性对子控件的测量是有影响的,比如我们的屏幕是1080x1920的,子控件的宽度为填充父窗体,如果使用了marginLeft并设置值为100;
在测量子控件的时候,如果用measureChild,计算的宽度是1080,而如果是使用measureChildWithMargins,计算的宽度是1080-100 = 980。怎样让ViewGroup支持margin属性?
ViewGroup中有两个内部类ViewGroup.LayoutParams和ViewGroup. MarginLayoutParams,MarginLayoutParams继承自LayoutParams ,这两个内部类就是VIewGroup的布局参数类,比如我们在LinearLayout等布局中使用的layout_width\layout_hight等以“layout_ ”开头的属性都是布局属性。在View中有一个mLayoutParams的变量用来保存这个View的所有布局属性。LayoutParams和MarginLayoutParams 的关系
LayoutParams 中定义了两个属性(现在知道我们用的layout_width\layout_hight的来头了吧?):
<declare-styleable name= "ViewGroup_Layout">
<attr name ="layout_width" format="dimension">
<enum name ="fill_parent" value="-1" />
<enum name ="match_parent" value="-1" />
<enum name ="wrap_content" value="-2" />
</attr >
<attr name ="layout_height" format="dimension">
<enum name ="fill_parent" value="-1" />
<enum name ="match_parent" value="-1" />
<enum name ="wrap_content" value="-2" />
</attr >
</declare-styleable >
MarginLayoutParams 是LayoutParams的子类,它当然也延续了layout_width\layout_hight 属性,但是它扩充了其他属性:
< declare-styleable name ="ViewGroup_MarginLayout">
<attr name ="layout_width" /> <!--使用已经定义过的属性-->
<attr name ="layout_height" />
<attr name ="layout_margin" format="dimension" />
<attr name ="layout_marginLeft" format= "dimension" />
<attr name ="layout_marginTop" format= "dimension" />
<attr name ="layout_marginRight" format= "dimension" />
<attr name ="layout_marginBottom" format= "dimension" />
<attr name ="layout_marginStart" format= "dimension" />
<attr name ="layout_marginEnd" format= "dimension" />
</declare-styleable >
是不是对布局属性有了一个全新的认识?原来我们使用的margin属性是这么来的。
为什么LayoutParams 类要定义在ViewGroup中?
大家都知道ViewGroup是所有容器的基类,一个控件需要被包裹在一个容器中,这个容器必须提供一种规则控制子控件的摆放,比如你的宽高是多少,距离那个位置多远等。所以ViewGroup有义务提供一个布局属性类,用于控制子控件的布局属性。为什么View中会有一个mLayoutParams 变量?
我们在之前学习自定义控件的时候学过自定义属性,我们在构造方法中,初始化布局文件中的属性值,我们姑且把属性分为两种。一种是本View的绘制属性,比如TextView的文本、文字颜色、背景等,这些属性是跟View的绘制相关的。另一种就是以“layout_”打头的叫做布局属性,这些属性是父控件对子控件的大小及位置的一些描述属性,这些属性在父控件摆放它的时候会使用到,所以先保存起来,而这些属性都是ViewGroup.LayoutParams定义的,所以用一个变量保存着。怎样让ViewGroup支持margin属性?
这个知识点比较多,不打算在这里讲,后面再加入这个知识点吧。
②. getChildMeasureSpec方法
measureChildWithMargins跟measureChild 都调用了这个方法,其作用就是通过父控件的宽高约束规则和父控件加在子控件上的宽高布局参数生成一个子控件的约束。我们知道View的onMeasure方法需要两个参数(父控件对View的宽高约束),这个宽高约束就是通过这个方法生成的。有人会问为什么不直接拿着子控件的宽高参数去测量子控件呢?打个比方,父控件的宽高约束为wrap_content,而子控件为match_perent,是不是很有意思,父控件说我的宽高就是包裹我的子控件,我的子控件多大我就多大,而子控件说我的宽高填充父窗体,父控件多大我就多大。最后该怎么确定大小呢?所以我们需要为子控件重新生成一个新的约束规则。只要记住,子控件的宽高约束规则是父控件调用getChildMeasureSpec方法生成。
getChildMeasure方法代码不多,也比较简单,就是几个switch将各种情况考虑后生成一个子控件的新的宽高约束,这个方法的结果能够用一个表来概括:
进行了上面的步骤,接下来就是在measureChildWithMarginsh或者measureChild中 调用子控件的measure方法测量子控件的尺寸了。
③. View的onMeasure
View中onMeasure方法已经默认为我们的控件测量了宽高,我们看看它做了什么工作:
protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
* 为宽度获取一个建议最小值
*/
protected int getSuggestedMinimumWidth () {
return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground.getMinimumWidth());
}
/**
* 获取默认的宽高值
*/
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;
}
从源码我们了解到:
如果View的宽高模式为未指定,他的宽高将设置为android:minWidth/Height =”“值与背景宽高值中较大的一个;这里的mMinWidth 就是在布局文件中写最小宽度android:minWidth所写的值,mBackground.getMinimumWidth()表示设置了背景的最小值。
- 如果View的宽高 模式为 EXACTLY (具体的size ),最终宽高就是这个size值;
- 如果View的宽高模式为EXACTLY (填充父控件 ),最终宽高将为填充父控件;
- 如果View的宽高模式为AT_MOST (包裹内容),最终宽高也是填充父控件。
也就是说如果我们的自定义控件在布局文件中,只需要设置指定的具体宽高,或者MATCH_PARENT 的情况,我们可以不用重写onMeasure方法。
但如果自定义控件需要设置包裹内容WRAP_CONTENT ,我们需要重写onMeasure方法,为控件设置需要的尺寸;默认情况下WRAP_CONTENT 的处理也将填充整个父控件。
④. setMeasuredDimension
onMeasure方法最后需要调用setMeasuredDimension方法来保存测量的宽高值,如果不调用这个方法,可能会产生不可预测的问题。
这篇博客我们学习了onMeasure方法测量控件大小的流程,以及里面执行的一些细节,总结一下知识点:
1、测量控件大小是父控件发起的
2、父控件要测量子控件大小,需要重写onMeasure方法,然后调用measureChildren或者measureChildWithMargin方法
3、on Measure方法的参数是通过getChildMeasureSpec生成的
如果我们自定义控件需要使用wrap_content,我们需要重写onMeasure方法
测量控件的步骤:
父控件的
onMeasure->measureChildren
measureChildWithMargin->getChildMeasureSpec->子控件measure->onMeasure->setMeasureDimension->
父控件onMeasure结束调用setMeasureDimension保存自己的大小
至此onMeasure相关的知识已经讲解完毕了,下一篇自定义ViewGroup,会解决本篇遗留的一个问题:怎样让ViewGroup支持margin属性?