View系列 - MeasureSpec全面解析

前言

兵马未动,粮草先行。在View的整个measure过程中,MeasureSpec就是这场战争的后勤补给军,贯穿了整场战争。要想真正理解measure的过程,就必须要百分之百的掌握MeasureSpec!相信很多人在学习MeasureSpec源码的时候,经常会看到这样的东西:

private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY     = 1 << MODE_SHIFT;
public static final int AT_MOST     = 2 << MODE_SHIFT;

又或者在View的源码中经常看到这样的东西:

//这个在requestLayout方法中会用到
static final int PFLAG_FORCE_LAYOUT = 0x00001000;
static final int PFLAG_LAYOUT_REQUIRED = 0x00002000;

这TM到底是什么鬼?每次看安卓的源码的时候看到这种东西就一头雾水,不知所云。感觉Google的工程师都这么diao的吗?其实这并没有多神奇,这就是普通的位运算!!Google的源码中使用了大量的位运算,MeasureSpec的实现原理也不例外!所以,要想搞懂Google的源码,先来看看位运算吧。

1、神奇的位运算

1.1位运算

在数学中,数字分为:二进制、十进制、十六进制,我们在生活中使用最多的是十进制,但是在计算机中所有的运算都是基于二进制的,在代码中我们用 “0x” 来代表十六进制,例如 0x12就代表十六进制的12(涉及二进制、十进制、十六进制的转化不是本文的重点,不再细说)。

敲黑板!!假设我们用二进制0001代表面包bread,二进制0010代表牛奶milk,现在我们有一个变量flag = 0000;

flag |= bread;    // 0000 与 0001 做 “或” 操作,结果 flag = 0001
flag |= milk;     //0001 与 0010 做 “或” 操作, 结果 flag = 0011
//此时的flag从0000变为0011,也就是说此时的flag身上已经有bread和milk两种标记了
//那么如何检测flag中含有这两种标记呢?
if (flag & milk != 0) {
	//0011 与 0010 做 “与” 操作,结果为 0010 != 0, 判断成立
	//也就说明了flag中包含milk
}
//如何删除flag中的milk标记呢?
//先进行milk的 “非” 操作,结果为 1101,在让 1101 与 flag(0011)做 “与” 操作,
//结果为0001,也就是bread,这样就把milk从flag中移除了
flag &= ~milk

//移除milk后,flag变为0001,此时在判断flah中是否含有milk
if (flag & milk != 0) {
	// 0001 与 0010 做 “与” 操作,结果为0,判断不成立,说明此时flag中已经没有milk的标记了
}

1.2位运算应用

接下来就用一个例子说明位运算在代码中的神奇之处!如下图,有两个按钮 modeA 和 modeB,在两种模式下有三个开关,每次切换mode后,都需要改变开关的状态:

在modeA下:开关1打开,开关2打开,开关3关闭。

在modeB下:开关1关闭,开关2打开,开关3打开。
图片

常规做法:

定义一个对象Mode:

//用Mode来记录不同模式下所有开关的状态
class Mode {
	//代表开关的状态
	public boolean switch1;
	public boolean switch2;
	public boolean switch3;
}
class Operator {
	private Mode mode = new Mode();
	
	void main() {
		//下面在modeA和modeB的按钮的点击事件中,分别获取各个开关的状态
		modeABtn.setOnClickListener(new OnClickListener {
			void onCLick() {
				mode.switch1 = true;
				mode.switch2 = true;
				mode.switch3 = false;
			}
		})
		modeBBtn.setOnClickListener(new OnClickListener {
			void onClick() {
				mode.switch1 = false;
				mode.switch2 = true;
				mode.switch3 = true;
			}
		})
	}
}

可见,在两种模式下,每种模式需要保存三种状态,但是如果有十种状态呢?二十种?五十种…由于整个逻辑的时间复杂度是m*n,随着状态的增加,时间复杂度会非常大。下面来看一下使用位运算是怎么来解决这种问题的

位运算:
class Operator {
	private int flag;
	//十六进制的1、2、4分别对应二进制的0001、0010、0100,大家应该会发现一个规律,
	//那就是二进制数每次只有一个1,并且这个1每次都往前一位(1000,00010000,00100000  等等)
	//这样就可以保证每一种状态的唯一性。
	//还有一点大家可能疑问会比较多,那就是我为什么此处要使用十六进制的数,而不使用十进制的呢?
	//其实不管是十六进制还是十进制,只要最终转化成的二进制符合上述的规律即可,
	//尤其是在Google的源码中你可能还经常会看见位移运算符:1<<2,最终得到的结果也是0100。
	//但是使用十六进制是因为十六进制与二进制转化比较容易表示的也更直观,
	//十六进制数的每一位直接对应二进制的四位数。
	public static final int SWITCH_1_OPEN = 0x1;
	public static final int SWITCH_2_OPEN = 0x2;
	public static final int SWITCH_4_OPEN = 0x4;
	public int MODE_A_STATUS = SWITCH_1_OPEN | SWITCH_2_OPEN;
	public int MODE_B_STATUS = SWITCH_2_OPEN | SWITCH_3_OPEN;
	public int flag;
	
	void main() {
		//下面在modeA和modeB的按钮的点击事件中,分别设置各个开关的状态
		modeABtn.setOnClickListener(new OnClickListener {
			void onCLick() {
				flag = MODE_A_STATUS;
			}
		})
		modeBBtn.setOnClickListener(new OnClickListener {
			void onClick() {
				flag = MODE_B_STATUS;
			}
		})
	}

	 
}

怎么样?就是这么神奇!在普通的方法中我们需要声明一个专用的Mode类来存储开关的状态,在按钮的点击事件中修改mode对象的数据。但是在位运算的做法中,我们只需保存一个int类型的flag即可达到效果。并且当状态无限增多时,我们只需要相应的更改一次MODE_A_STATUS或者MODE_B_STATUS的值,即可达到效果,时间复杂度降低为m+n。

2、MeasureSpec类

前面讲了很多基础知识铺垫,现在开始进入本篇的正题,MeasureSpec是View的静态内部类,类的主要结构如下:

public static class MeasureSpec {
	 //int类型的变量占32位,用高2代表mode,低30代表size
    private static final int MODE_SHIFT = 30;
    //0x3二进制为0011,左移30位后变为1100....00.... ,也就是说高两位是有效数值位
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    //0代表UNSPECIFIED
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //1左移30位代表EXACTLY  0100....0000....
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //2左移30位代表AT_MOST  1000....0000....
    public static final int AT_MOST     = 2 << MODE_SHIFT;
	 
	 //根据size和mode制作一个MeasureSepc的int类型数值
	 public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,@MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    //将目标值与MODE_MASK做 “与” 操作,就可以得到高2位的数值
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    //先将MODE_MASK做 “非” 操作后变为高两位为0,后30位为1的二进制数,然后再与目标值做 “与” 操作,就可以得到后30的具体size
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

可见,其实MeasureSpec类内部全是静态常量以及静态方法,它本身其实就是一个工具类。在View的测量流程中,宽高尺寸传递的是一个整型的数值,这个数值对应一个32位的二进制数,由两部分组成:高两位(mode)和低30位(size)。在理解了MeasureSpec的原理后,再看measure流程就已经很简单了。

3、MeasureSpec在View中的使用

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

view的onMeasure方法中会接收父类中传来的widthMeasureSpec、heightMeasureSpec,最终会调用setMeasuredDimension来保存具体的尺寸,而具体的尺寸就是通过getDefaultSize方法来解析的

    public static int getDefaultSize(int defaultSize, int measureSpec) {
        int result = size;
        //调用MeasureSpec.getMode方法,获取高2位的数值
        int specMode = MeasureSpec.getMode(measureSpec);
        //调用MeasureSpec.getSize方法,获取低30位的具体size
        int specSize = MeasureSpec.getSize(measureSpec);
        //根据不同的mode,返回不同的尺寸
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

可见在View的默认onMeasure方法中,AT_MOST和EXACTLY的效果是一样的,也就是说当我们自定义一个继承自View的视图时,如果不自己重写onMeasure方法,那么当这个自定义的view在布局文件中期望使用wrap_content布局类型的时候,其实他的效果是match_parent.

4、MeasureSpec在ViewGroup中的使用

ViewGroup类并没有重写View的onMeasure方法,因为在不同的布局(LinearLayout、RelativeLayout)中测量的规则是不一样的,所以ViewGroup把测量的任务交给了他的子类。但是ViewGroup还是提供了普通的测量View的方法,供继承ViewGroup的子类使用:

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);
            }
        }
    }

在measureChildren方法中遍历当前所有的子View,如果这个View是Visiable的,那么就调用measureChild方法

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    //首先获取child的LayoutParams对象,通过LayoutParams就可以拿到当前View在布局里的layout_width、layout_heigt等参数。
    final LayoutParams lp = child.getLayoutParams();
    //调用getChildMeasureSpec方法获取当前View的widthMeasureSpec,
    //此处需要注意该方法传入的参数:parentWidthMeasureSpec是当前ViewGroup的parent传进来的测量参数,
    //也就是当前ViewGroup的测量参数。mPaddingLeft + mPaddingRight是当前ViewGroup的左右内边距,
    //因为当前是获取的widthMeasureSpec,所以只需获取左右内边距即可。lp.width对应布局文件中child的layou_width参数
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    //由于是获取heightMeasureSpec,所以改为上下的内边距
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    //最终调用view的measure方法
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

最后来看一下getChildMeasureSpec方法

/**
* spec: 父布局的MeasureSpec
* padding: 父布局的内边距
* childDimension: 实际对应着view在布局文件中的layout_width和layout_height
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //获取父布局的mode
        int specMode = MeasureSpec.getMode(spec);
        //获取父布局的size
        int specSize = MeasureSpec.getSize(spec);
        //specSize - padding就是当前父布局减去自己内边距后还剩余的尺寸
        int size = Math.max(0, specSize - padding);
        
        //最后根据这两个参数确定最终View的布局约束MeasureSpec
        int resultSize = 0;
        int resultMode = 0;
        
        //敲黑板!!根据父布局的mode来确认子View最后的MeaureSpace,可见子View最终的尺寸到底是多大,
        //是由父View的尺寸以及子View的尺寸共同决定的
        switch (specMode) {
        // 如果父布局的尺寸是确定的,也就是match_parent或者具体的数值
        case MeasureSpec.EXACTLY:
            //LayoutParams.MATCH_PARENT = -1
            //LayoutParams.WRAP_CONTENT = -2
            //所以当childDimension的值大于等于0的时候,就说明在布局文件中是具体的数值
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 子View的布局是MATCH_PARENT,也就是父View有多大尺寸我就要多大尺寸,
                //所以子View的最终尺寸是固定的
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子View的布局是WRAP_CONTENT,他想要包含自己的内容,
                //所以他的尺寸是不确定的,但是可以确定的是他的尺寸最大不能超过他的父View,
                //所以父View把当前最大的可用的尺寸size交给子View
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 如果父View也是WRAP_CONTENT,也就是说父View的尺寸暂时也不确定,
        //需要看子View的尺寸是多少,但是可以确定的是父View的最大尺寸size是确定的,就是specSize
        case MeasureSpec.AT_MOST:
            //childDimension的值大于等于0的时候,就说明在布局文件中是具体的数值
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 子View是MATCH_PARENT想要父VIew的所有可用的尺寸,
                //但是此时父View的尺寸还不能确定具体是多少,只能确定最大尺寸是spaceSize,
                //所以此时子VIew的spaceMode也是AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子VIew是WRAP_CONTENT包含自己的内容,所以父View把自己能用的最大尺寸交给子View
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 这种情况几乎很少见
        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;
        }
        //最终经过上边的各种条件后,确定子View的最终MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

在网上有很多EXACTLY、AT_MOST与match_parent、wrap_content相对应的映射关系,这里就不细说了。

5、DectorView

可能大家还会有一个疑问:子View的MeasureSpec是由父View决定的,那父View的父View又是谁呢?总得有一个根,通过这个根来开始整个View体系的测量。这个根就是DectorView,他继承自FrameLayout,是我们在手机屏幕中看到的视图中最根部的一个View。在这个DectorView开始测量的时候,他的宽高布局参数是一个mode为EXACTLY,size为屏幕尺寸的MeasureSpec,所以这个DectorView始终都是填满手机屏幕的。

ok!大功告成!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值