android开发游记:ScrollView嵌套ListView,ListView完全展开及makeMeasureSpec测量机制原理分析

来源:互联网 发布:百度云网络连接失败 编辑:程序博客网 时间:2024/05/22 03:38

在实际应用中,经常会碰到非常规的布局要求,比如说在ScrollView里嵌套ListView,ScrollView和ListView都是可以滚动的控件,这样布局看似很奇怪,但是有些效果又不得不这样做。比如说:一个长布局中有部分是列表格式,布局长度又超过屏幕高度,这样的情况就得使用这种布局了。

    <ScrollView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:layout_alignParentLeft="true"        android:fillViewport="true">        <LinearLayout            android:id="@+id/showin"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:orientation="vertical" >            <ListView                android:layout_width="match_parent"                android:layout_height="match_parent"                android:background="@color/white"/>        </LinearLayout>    </ScrollView>

但是实际试过之后就会发现这样做有一个问题,无论ListView的高度怎么设置,都会只显示一行的高度,那是由于ListView的父容器测量模式为UNSPECIFIED的时候,ListView的高度默认为一个item的高度,ListView中源码如下:

if (heightMode == MeasureSpec.UNSPECIFIED) {    heightSize = mListPadding.top + mListPadding.bottom + childHeight +                   getVerticalFadingEdgeLength() * 2;}

这样我们就重写ListView的onMeasure方法,来自定义高度:

    /**     * 重写该方法,达到使ListView适应ScrollView的效果     */    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);        super.onMeasure(widthMeasureSpec, expandSpec);    }

像如上这样重写onMeasure方法,就能让ScrollView嵌套中的ListView完全填满。仔细看,会发现上面的代码很奇怪,下面解释下其原理,不想了解的可以跳过了。

原理分析:

查看上面的代码我们发现我们把高度写成了一个固定值expandSpec ,这个值是这样计算出来的

expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);

从上面代码可以看出,把整数类型的最大值右移了2位,作为size传入,另外把一个AT_MOST的常量作为mode传入。

为什么要传入这样一个奇葩的参数呢,这就android的实体测量机制有关了,android中规定,测量的值(高度或宽度)为一个int类型,但不是普通的int,而是一个进过处理的int,在view视图中我们制定一个高度需要2个参数,1个是具体的值,一个是测量模式,测量模式就是我们在布局中经常用到的MATCH_PARENT 、WRAP_CONTENT。他们是一个int型的常量,对应的值分别是:

LayoutParams.MATCH_PARENT  对应 MeasureSpec.EXACTLYLayoutParams.WRAP_CONTENT  对应 MeasureSpec.AT_MOST

而EXACTLY和AT_MOST的值是:

        private static final int MODE_SHIFT = 30;              public static final int EXACTLY     = 1 << MODE_SHIFT;    //填满父控件高度        public static final int AT_MOST     = 2 << MODE_SHIFT;    //自适应当前控件高度

可以看到,分别是把1和2左移30位的值。为什么会这样呢?

android中把测量出的int做了处理,int的长度时32位,把前2位作为标志位标示了测量模式,如EXACTLY、AT_MOST
把后30位作为测量的具体高度或宽度。
也就是说,把一个int分成了2部分,使一个int值同时拥有了模式和具体数值的2部分信息!

EXACTLY的值是1向左进位30,就是01 00000000000…(01后跟30个0)
AT_MOST的值是2向左进位30,就是10 00000000000…(10后跟30个0)

所以我们在调用MeasureSpec.makeMeasureSpec(size,mode)方法时,传入的size参数要把Integer.MAX_VALUE右移2位,因为前两位会被认为是标志,而不是值。这样我们传入的参数才会被认为是最大的int类型的值,同时传入AT_MOST作为模式,那么前两位就会被赋值为10,那么我们来实际计算一下,我们调用MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST)的实际结果是多少:

int类型长度是32位,其中包含1个标准位,所有最大值Integer.MAX_VALUE为 2的31次方 :2147483648
16进制下为0x7fffffff,二进制为:01 11111111111…(01后跟30个1)
由于前两位在android测量基础下是无效的标志位,所以我们右移2位的结果为000111111111111…
调用makeMeasureSpec方法后会把前两位替换为AT_MOST的前两位(其实是直接相加)
结果为:100111111111… 十六进制:9fffffff,十进制:-1610612737

所以你调试的时候后发现,返回的值为-1610612737 ,就是这个原因,因为前2位是无效的标志位,所以其实这个数所代表的最大值是 2的29次方:536870912。所以对于android测量机制来理解我们传入的-1610612737,是这样理解的:

-1610612737  代表: 测量模式为AT_MOST,最大高度为536870912(229次方)
1 0