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)我們想要的功能.