ScrollView中的那些嵌套坑

ScrollView嵌套ListView,如果不做任何處理的情況下,一般Listview只會出現(xiàn)一項,這是因為ScrollView無法獲取ListView的正常高度

布局示例
//布局情況一
<ScrollView>
  <ListView/>
</ScrollView>  

//布局情況二
<ScrollView>
  <LinearLayout>
   ...
   <ListView/>
   ...
  </LinearLayout>
</ScrollView>  

上面兩種情況是我們在平時的使用中比較容易見到的情況,尤其是情況二。

先說如何解決問題:

第一種方法重寫ListView中的onMeasure()方法

@Override 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec,
        MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2,MeasureSpec.AT_MOST));
}

第二種方式手動去計算ListView中每個Item的高度,然后將這個高度傳給ListView,讓ScrollView知道自己的子布局有多高,這樣的話ListView就會正常顯示了,這個方法在ListView.setAdapter()后調(diào)用即可。

public void measureListViewChildHeight(){
        ArrayAdapter listAdapter = (ArrayAdapter) mListView.getAdapter();
        if (listAdapter == null)
            return;
        int desiredWidth = View.MeasureSpec.makeMeasureSpec(mListView.getWidth(), View.MeasureSpec.UNSPECIFIED);
        int totalHeight = 0;
        View view = null;
        for (int i = 0; i < listAdapter.getCount(); i++) {
            //獲取每個item的view
            view = listAdapter.getView(i, view, mListView);
            if (i == 0)
                view.setLayoutParams(new AbsListView.LayoutParams(desiredWidth, AbsListView.LayoutParams.WRAP_CONTENT));
            view.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);
            //計算總高度
            totalHeight += view.getMeasuredHeight();
        }
        ViewGroup.LayoutParams params = mListView.getLayoutParams();
        //加上divider的高度
        params.height = totalHeight + (mListView.getDividerHeight() * (listAdapter.getCount() - 1));
        mListView.setLayoutParams(params);
        mListView.requestLayout();
    }

注意:第二種解決方法不適用布局情況一,親測在布局情況一下,ScrollView的getMeasureHeight()為0或者負數(shù),這樣的話ListView只能顯示出一行的數(shù)據(jù),雖然你正確的設(shè)置了ListView的LayoutParams,但是經(jīng)過測試ListView.getMeasureHeight()的值為一行數(shù)據(jù)的高度,猜想是因為ListView在onMeasure()時獲得一行Item的高度后就把設(shè)置死了,有一行Item的高度就能顯示數(shù)據(jù)了,而且還可以滾動。

那為什么會產(chǎn)生這種情況?

我們打開ScrollView的源碼,在最前面的類說明中我們看到了,官方都不建議我們使用這種嵌套行為,因為這樣會導(dǎo)致ListView的所有重要優(yōu)化失敗,以處理大型列表,因為它有效地強制ListView顯示其完整的項目列表,以填補ScrollView提供的無限容器。還會有滑動沖突等等問題

* Layout container for a view hierarchy that can be scrolled by the user,
* allowing it to be larger than the physical display. A ScrollView
* is a {@link FrameLayout}, meaning you should place one child in it
* containing the entire contents to scroll; this child may itself be a layout
* manager with a complex hierarchy of objects. A child that is often used
* is a {@link LinearLayout} in a vertical orientation, presenting a vertical
* array of top-level items that the user can scroll through.

* You should never use a ScrollView with a {@link ListView}, because
* ListView takes care of its own vertical scrolling. Most importantly, doing this
* defeats all of the important optimizations in ListView for dealing with
* large lists, since it effectively forces the ListView to display its entire
* list of items to fill up the infinite container supplied by ScrollView.
  
* The {@link TextView} class also
* takes care of its own scrolling, so does not require a ScrollView, but
* using the two together is possible to achieve the effect of a text view
* within a larger container.

我們來找找罪魁禍首ScrollView的onMeasure方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (!mFillViewport) {
            return;
        }

        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            return;
        }

        if (getChildCount() > 0) {
            final View child = getChildAt(0);
            int height = getMeasuredHeight();
            if (child.getMeasuredHeight() < height) {
                final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();

                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        mPaddingLeft + mPaddingRight, lp.width);
                height -= mPaddingTop;
                height -= mPaddingBottom;
                int childHeightMeasureSpec =
                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

從源碼可以發(fā)現(xiàn),ScrollView的高度是由它的子View決定的,而且它的子View的測量模式為MeasureSpec.EXACTLY模式。

從Android開發(fā)藝術(shù)探索一書由講到View的測量模式,SepcMode有三種,每一種都有其特殊意義

  • UNSPECIFIED 父容器不對View有任何限制,要多大給多大,這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量的狀態(tài)
  • EXACTLY 父容器已經(jīng)檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對于LayoutParams中的match_parant和具體數(shù)值兩種情況
  • AT_MOST 父容器指定了一個可用大小即SpecSize,View的值不能大于這個值,具體是什么值要看不同View的具體實現(xiàn)。他對應(yīng)于LayoutParams中的wrap_content

在這個模式下父布局需要知道子布局的精確高度,這樣的話才能正常顯示出來,想想這種做法也是挺合理的,畢竟這是一個滾動布局,整個布局的高度就像畫布一樣肯定是精確的,不然怎么滾動顯示呢。但是也正是由于這種做法導(dǎo)致了如果子布局的高度不是什么精確值會導(dǎo)致各種顯示異常。

我們在解法一的方法中也用到了AT_MOST,解法一的原理就是給定了ListView的最大值為INTEGER.MAX_VALUE>>2的大小,讓它在這個范圍內(nèi)隨便折騰。

你以為有了這兩種解決方法就萬事大吉了嗎?

在上周我們項目中還是出現(xiàn)了ListView的高度顯示異常,這次是最后兩項的高度顯示異常,我們的ListView的基類已經(jīng)指定了AT_MOST模式還是出現(xiàn)了問題,可見問題并不像我們想象的那么簡單。

//項目中的布局
<ScrollView>
  <LinearLayout>
    <LinearLayout>
     ...
     <ListView/>
     ...
    </LinearLayout>
  </LinearLayout>
</ScrollView>  

問題分析:既然大部分的Item都已經(jīng)顯示出來了,只差最后最后兩項的高度,那么極有可能是在測量高度的過程中出現(xiàn)了測量高度有偏差,所有的偏差值加起來正好等于最后兩項的高度值。最后將Item的高度設(shè)置為精確值,發(fā)現(xiàn)就不會出現(xiàn)問題了。正好證明我的猜想是正確的。

總結(jié)

在實際開發(fā)中,我們還是要盡量去避免使用這種嵌套,畢竟官方都不建議我們?nèi)ミ@樣使用,而且還是有很多坑等著我們?nèi)ゲ?。所以我們可以完全使用一個ListView配合多種布局,或者使用RecycleView去實現(xiàn)我們想要的功能.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容