ScrollView 與ListView 滑動衝突解決

weixin_34208283發表於2017-06-25

轉載請標明出處:http://www.jianshu.com/p/4cbe7515ef12
本文出自:http://www.jianshu.com/u/a1251e598483

首先說聲抱歉,真的好久沒有動筆寫東西了,如果說是因為工作太忙,那是扯淡,還是因為懶,你看今年馬上要過去一半了,感嘆光陰似箭,歲月蹉跎,遂動筆寫下最近遇到的問題,以及解決方案。

需求:

實現一個頁面上整體內容可以上下滾動,頁面的上半部分是一些固定內容,下半部分是一個列表,列表中每個item都可以點選展開,展開的部分內容條目不固定,預設隱藏,點選時展開。

這樣的佈局也很常見對吧,下面用listview 就可以了,外面用scrollView包裹就可以實現頁面內容整體上下滾動。但是你會返現下面的listview不管有幾條資料都會預設展示第一條資料,多條資料只能在一條item高度的範圍內滾動。這樣的效果顯然不是我們想要的,那就要解決。

解決辦法

首先要了解ScrollView的工作原理,常用它來佈局一個內容超過當前頁面,需要往下滑才能看到完整內容。可以把ScrollView看作是一種特殊的、加長版的LinearLayout,系統在載入佈局時其實要知道整個佈局的長度,才能將整個頁面顯示出來。而ListView的item個數是會動態變化的,如果在ScrollView中加入ListView會讓系統無法準確載入,導致上述情況的發生。

你可能已經在網上搜尋過其他解決方案了。
1:給listview 設定固定的高度。這個我看著就覺得不靠譜,如果你的listview的資料多少是個固定值,你經過計算後那個高度就是你固定listview的高度,那你完全可以用這個,但是這個對於我們這些資料條數不固定的就不能用這種方法了;
2:可以根據ListView中Item數量,動態設定ListView的height,使全部item得到顯示。
就是一個方法,動態計算listView 的數目,根據高度,計算出總高度。

 /**
     * scrollview與listview合用會出現listview只顯示一行多點。此方法是為了定死listview的高度就不會出現以上狀況
     * 算出listview的高度
     */
    public static void setListViewHeight(ListView listView) {
        ListAdapter listAdapter = listView.getAdapter();
        if (listAdapter == null) {
            return;
        }
        int totalHeight = 0;
        for (int i = 0; i < listAdapter.getCount(); i++) {
            View listItem = listAdapter.getView(i, null, listView);
            listItem.measure(1, 1);
            totalHeight += listItem.getMeasuredHeight();
        }
        ViewGroup.LayoutParams params = listView.getLayoutParams();
        params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)) + listView.getPaddingTop() + listView.getPaddingBottom();
        listView.setLayoutParams(params);
    }

這個方案呢,的確可以解決scrollView 裡有listView ,整體上下滾動,listView 顯示不全的問題,正常的要解決這個問題的看到這就行了,已經可以解決你的問題了。

但是沒辦法解決我的問題,不要忘了我的需求是listView 可以正常滾動,在點選每個item時動態展開檢視詳情。其實就是在每個item中隱藏了部分佈局,根據點選item事件動態控制,這部分的item是Visible 還是Gone的事,由於詳情的資料預設是隱藏的,在計算item的高度時是沒有,也不能把隱藏的佈局高度也計算在內的,所以這個方案不行。
3: 自定義 一個MyListView 繼承listView ,重寫裡面的 onMeasure()方法

/**
* Integer.MAX_VALUE >> 2,如果不設定,系統預設設定是顯示兩條
*/
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}

瞭解自定義view 的肯定知道 onMeasure()是什麼意思,makeMeasureSpec()方法中 Integer.MAX_VALUE >> 2
在Android中,一個控制元件所佔的模式和大小是通過一個整數int來表示的,這裡很多同學就疑惑了,一個int值是怎麼來表示模式的大小的,這裡來看一張圖片:


688934-6be9eddb3fbc3bbf

原來,Android裡面把int的最高2兩位來表示模式,最低30位來表示大小。

測量View大小使用的是onMeasure函式,我們可以從onMeasure的兩個引數中取出寬高的相關資料:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthsize = MeasureSpec.getSize(widthMeasureSpec);      //取出寬度的確切數值
        int widthmode = MeasureSpec.getMode(widthMeasureSpec);      //取出寬度的測量模式
        int heightsize = MeasureSpec.getSize(heightMeasureSpec);    //取出高度的確切數值
        int heightmode = MeasureSpec.getMode(heightMeasureSpec);    //取出高度的測量模式
    }

從上面可以看出 onMeasure 函式中有 widthMeasureSpec 和 heightMeasureSpec 這兩個 int 型別的引數, 毫無疑問他們是和寬高相關的, 但它們其實不是寬和高, 而是由寬、高和各自方向上對應的測量模式來合成的一個值:
測量模式一共有三種, 被定義在 Android 中的 View 類的一個內部類View.MeasureSpec中:

3種模式
1):UNSPECIFIED模式,官方意思是:父佈局沒有給子佈局強加任何約束,子佈局想要多大就要多大,說白了就是不確定大小
2)EXACTLY模式,官方意思是:父佈局給子佈局限定了準確的大小,子佈局的大小就是精確的,父親給多大就是多大
3)AT_MOST模式,官方意思是:父佈局給定了一個最大的值,子佈局的大小不能超過這個值,當然可以比這個值小

private static final int MODE_SHIFT = 30;

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;

不確定模式是0左移30位,也就是int型別的最高兩位是00
精確模式是1左移30位,也就是int型別的最高兩位是01
最大模式是是2左移30位,也就是int型別的最高兩位是10

所以呼叫了makeMeasureSpec方法,這個方法是用來生成一個帶有模式和大小資訊的int值的,第一個引數Integer.MAX_VALUE >> 2,這個引數是傳的一個大小值,為什麼是這個值呢,我們現在已經知道了,我們要生成的控制元件,它的大小最大值是int的最低30位的最大值,我們先取Integer.MAX_VALUE來獲取int值的最大值,然後左移2位就得到這個臨界值最大值了
當然,我們在手機上的控制元件的大小不可能那麼大,極限值就那麼大,實際肯定比那個小,所以這個模式就得選擇MeasureSpec.AT_MOST了,最後將生成的這個大小傳遞給父控制元件就可以了,super.onMeasure(widthMeasureSpec, expandSpec),這個函式只改變的是控制元件的高度,寬度沒有改變,實際開發當中不管listview有多少條資料,都能一次性展現出來。

最後選用第三種方式完美實現。

相關文章