View、ViewGroup之onMeasure讲解
我们从demo开始,先看下面示例:
package lennie.org.lennie.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
/**
* Created by win7 on 2017/7/12.
* /
public class TestView extends View {
private final String TAG = "TestView";
private Paint mPaint;
private Rect mRect;
private String text = "123456";
public TestView(Context context) {
this(context, null);
}
public TestView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, -1);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(2);
mPaint.setTextSize(100);
mRect = new Rect();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.getTextBounds(text, 0, text.length(), mRect);
canvas.drawText(text, getWidth() / 2 - mRect.width() / 2, getHeight() / 2 + mRect.height() / 2, mPaint);
}
}
//xml----------------
<lennie.org.lennie.view.TestView
android:layout_below="@+id/button2"
android:layout_width="200dip"
android:layout_height="100dip"
android:textSize="20sp"
android:background="#17f4f0"
android:layout_centerVertical="true"/>
结果:
是不是跟XML的配置一样,宽和高都是自己设定的。好,接着我们改改动下layout_width、layout_height的值,改成wrap_content,看看结果是不是跟我们想的一样,宽高度是根据内容来的。
<lennie.org.lennie.view.TestView
android:layout_below="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:background="#17f4f0"
android:layout_centerVertical="true"/>
结果:
很奇怪是不是,既然是match_parent效果。为啥呢?为什么wrap_content会失效呢?该咋办?此时onMeasure的方法的用作就出来了,在了解onMeasure方法之前先了解MeasureSpec,通过它可以决定子控件大小的,所以的子控件都是按照它给出标准来的。后面会告诉你什么会这样。
/**
*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);
}
......
}
现在我们在重写onMeasure方法,然后修改XML属性值,接着打印相关log
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取宽的模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取高的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸
int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸
Log.v(TAG, "宽的模式:"+widthMode);
Log.i(TAG, "高的模式:"+heightMode);
Log.d(TAG, "宽的尺寸:"+widthSize);
Log.e(TAG, "高的尺寸:"+heightSize);
int widhtModel = MeasureSpec.getMode(widthMeasureSpec);
getMeasureSpec(widhtModel, widthMeasureSpec);
}
private int getMeasureSpec(int model, int size) {
// MeasureSpec.AT_MOST; // 父容器已经为子容器确定的大小,子容器应该遵守
// MeasureSpec.EXACTLY; // 父控件决定给孩子一个精确的尺寸
// MeasureSpec.UNSPECIFIED;// 父容器对子容器没有做任何限制,子容器可以任意大小
int defSize = 0;
switch (model) {
case MeasureSpec.EXACTLY:
Log.e(TAG, "EXACTLY ===>");
break;
case MeasureSpec.AT_MOST:
Log.e(TAG, "AT_MOST ===>");
break;
case MeasureSpec.UNSPECIFIED:
Log.e(TAG, "UNSPECIFIED ===>");
break;
}
return defSize;
}
layout_width和layout_height为wrap_content时:
layout_width和layout_height为match_parent时:
layout_width和layout_height为指定值时:
从上面三种场景跟官方文档可以总结出以下几点:
UNSPECIFIED 父控件没有对子控件施加任何约束,子控件可以得到任意想要的大小。但是布局文件好像必须设置宽高,目前还没找到与之对应的布局参数,使用较少
EXACTLY 父控件给子控件决定了确切大小,子控件将被限定在给定的边界里而忽略它本身大小。特别说明如果是填充父窗体,说明父控件已经明确知道子控件想要多大的尺寸了(就是剩余的空间都要了)
AT_MOST 子控件至多达到指定大小的值。包裹内容就是父窗体并不知道子控件到底需要多大尺寸(具体值),需要子控件自己测量之后再让父控件给他一个尽可能大的尺寸以便让内容全部显示但不能超过包裹内容的大小
那为什么设置成wrap_content和match_parent是一样的效果呢,这个我们可以从源码得知,onMeasure方法调用了setMeasuredDimension(int measuredWidth, int measuredHeight)方法,而传入的参数已经是测量过的默认宽和高的值了;我们看看getDefaultSize 方法是怎么计算测量宽高的。根据父控件给予的约束,发现AT_MOST (相当于wrap_content )和EXACTLY (相当于match_parent )两种情况返回的测量宽高都是specSize,而这个specSize正是我们上面说的父控件剩余的宽高,所以默认onMeasure方法中wrap_content 和match_parent 的效果是一样的,都是填充剩余的空间。
现在知道了onMeasure做用是干嘛的和怎么使用,好我们接下就深挖下onMeasure方法
onMeasure什么时候会被调用
创建一个View(执行构造方法)的时候不需要测量控件的大小,只有将这个view放入一个容器(父控件)中的时候才需要测量,而这个测量方法就是父控件唤起调用的。当控件的父控件要放置该控件的时候,父控件会调用子控件的onMeasure方法询问子控件:“你有多大的尺寸,我要给你多大的地方才能容纳你?”,然后传入两个参数(widthMeasureSpec和heightMeasureSpec),这两个参数就是父控件告诉子控件可获得的空间以及关于这个空间的约束条件,子控件拿着这些条件就能正确的测量自身的宽高了。
onMeasure方法执行流程
测量的时候父控件的onMeasure方法会遍历他所有的子控件,挨个调用子控件的measure方法,measure方法会调用onMeasure,然后会调用setMeasureDimension方法保存测量的大小,一次遍历下来,第一个子控件以及这个子控件中的所有子控件都会完成测量工作;然后开始测量第二个子控件…;最后父控件所有的子控件都完成测量以后会调用setMeasureDimension方法保存自己的测量大小。值得注意的是,这个过程不只执行一次,也就是说有可能重复执行,因为有的时候,一轮测量下来,父控件发现某一个子控件的尺寸不符合要求,就会重新测量一遍。
view与viewgroup的onMeasure
view与viewgroup的onMeasure区别在与ViewGroup没有为子控件测量大小的能力,它只能测量自己的大小。但是viewgroup提供了measuireChildren、measuireChild、measureChildWithMargins三个测量子控件相关的方法。下面来看看源码:
/** *遍历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); } } } /** * 测量某一个child的宽高 */ 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); } /** * 测量某一个child的宽高,考虑margin值 */ 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); }
从源码中可以看出measureChild和measureChildWithMargins都调用了getChildMeasureSpec方法,其作用就是通过父控件的宽高约束规则和父控件加在子控件上的宽高布局参数生成一个子控件的约束。我们知道View的onMeasure方法需要两个参数(父控件对View的宽高约束),这个宽高约束就是通过这个方法生成的。有人会问为什么不直接拿着子控件的宽高参数去测量子控件呢?打个比方,父控件的宽高约束为wrap_content,而子控件为match_perent,是不是很有意思,父控件说我的宽高就是包裹我的子控件,我的子控件多大我就多大,而子控件说我的宽高填充父窗体,父控件多大我就多大。最后该怎么确定大小呢?所以我们需要为子控件重新生成一个新的约束规则。只要记住,子控件的宽高约束规则是父控件调用getChildMeasureSpec方法生成。 和 measureChildWithMargins跟measureChild的区别就是父控件支不支持margin属性
- 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 =”“值与背景宽高值中较大的一个;
- 如果View的宽高 模式为 EXACTLY (具体的size ),最终宽高就是这个size值;
- 如果View的宽高模式为EXACTLY (填充父控件 ),最终宽高将为填充父控件;
- 如果View的宽高模式为AT_MOST (包裹内容),最终宽高也是填充父控件。
也就是说如果我们的自定义控件在布局文件中,只需要设置指定的具体宽高,或者match_parent 的情况,我们可以不用重写onMeasure方法。但如果自定义控件需要设置包裹内容wrap_content ,我们需要重写onMeasure方法,为控件设置需要的尺寸;默认情况下wrap_content 的处理也将填充整个父控件。这也就是我的Demo为什么设置wrap_content会有match_parent的效果。
- setMeasuredDimension
onMeasure方法最后需要调用setMeasuredDimension方法来保存测量的宽高值,如果不调用这个方法,可能会产生不可预测的问题。
总结
onMeasure方法测量控件大小的流程,以及里面执行的一些细节,总结一下知识点:
- 测量控件大小是父控件发起的
- 父控件要测量子控件大小,需要重写onMeasure方法,然后调用measureChildren或者measureChildWithMargin方法
- on Measure方法的参数是通过getChildMeasureSpec生成的
- 如果我们自定义控件需要使用wrap_content,我们需要重写onMeasure方法
测量控件的步骤:
父控件onMeasure->measureChildren`measureChildWithMargin->getChildMeasureSpec->
子控件的measure->onMeasure->setMeasureDimension->
父控件onMeasure结束调用setMeasureDimension`保存自己的大小