ScrollView與Listview滑動衝突解決

在實際應用中,經常會碰到非常規的佈局要求,比如說在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.EXACTLY
LayoutParams.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(2的29次方)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章