在工作中,曾多次碰到scrollview嵌套listview的問題,網上的解決方法有很多種,但是雜而不全。我試過很多種方法,它們各有利弊。
在這裡我将會從使用scrollview嵌套listview結構的原因、這個結構碰到的問題、幾種解決方案和優缺點比較,這4個方面來為大家闡述、分析、總結。
實際上不光是listview,其他繼承自abslistview的類也适用,包括expandablelistview、gridview等等,為了友善說明,以下均用listview來代表。
一、 為什麼要使用scrollview嵌套listview的奇怪的結構
scrollview和listview都是滾動結構,按理說,這兩個控件在ui上的功能是一樣的,但是看看下面這個設計:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLrR3brNjYrNjc0MnZxQHdwZTN1QzMy8CX2AzLcJDM0EDMy8CXtVncvZ2LcRnbl1GajFGd0F2LcFGdhR2Lc12bj5yc1J2awFmL3d3dvw1LcpDc0RHaiojIsJye.png)
這是天貓商城的确認訂單的頁面,scrollview中嵌套了expandablelistview,expandablelistview上面有固定的一些控件,下面也有固定的一些控件,整體又要能夠滾動。
清單資料要嵌在固定資料中間,并且作為整體一起滾動,有了這樣的設計需求,于是就有了scrollview嵌套listview的奇怪結構。
二、 scrollview、listview嵌套結構碰到的問題
不多說,直接看失敗例子:
<scrollview
android:id="@+id/act_solution_1_sv"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<linearlayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<textview
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="\nlistview上方資料\n" />
<listview
android:id="@+id/act_solution_1_lv"
android:layout_height="wrap_content">
</listview>
android:text="\nlistview下方資料\n" />
</linearlayout>
</scrollview>
複制代碼
scrollview中隻能放一個控件,一般都放linearlayout,orientation屬性值為vertical。在linearlayout中放需要呈現的内容。listview也在其中,listview的高度設為适應自身内容(wrap_content)。粗略一看,應該沒有什麼問題。但是看下面的實際效果圖:
圖中黑框的部分就是listview,裡面放了20條資料,但是卻隻顯示了1條。
控件的屬性設定上沒有問題,但是為什麼沒有按照我的想法走呢?
看看下面這個圖:
是否有點明白了呢?原因就是scroll事件的消費處理以及listview控件的高度設定問題。
雖然我看源碼也看了不少,但是要說出來卻不知到該怎麼下手,我是大概知道原因,但是不知道怎麼整理完全。求高手賜教…
三、問題解決方案
1、手動設定listview高度
經過測試發現,在xml中直接指定listview的高度,是可以解決這個問題的,但是listview中的資料是可變的,實際高度還需要實際測量。于是手動代碼設定listview高度的方法就誕生了。
/**
* 動态設定listview的高度
* @param listview
*/
public static void setlistviewheightbasedonchildren(listview listview) {
if(listview == null) return;
listadapter listadapter = listview.getadapter();
if (listadapter == null) {
// pre-condition
return;
}
int totalheight = 0;
for (int i = 0; i < listadapter.getcount(); i++) {
view listitem = listadapter.getview(i, null, listview);
listitem.measure(0, 0);
totalheight += listitem.getmeasuredheight();
viewgroup.layoutparams params = listview.getlayoutparams();
params.height = totalheight + (listview.getdividerheight() * (listadapter.getcount() - 1));
listview.setlayoutparams(params);
}
上面這個方法就是設定listview的高度了,在為listview設定了adapter之後使用,就可以解決問題了。
但是這個方法有個兩個細節需要注意:
一是adapter中getview方法傳回的view的必須由linearlayout組成,因為隻有linearlayout才有measure()方法,如果使用其他的布局如relativelayout,在調用listitem.measure(0, 0);時就會抛異常,因為除linearlayout外的其他布局的這個方法就是直接抛異常的,沒理由…。我最初使用的就是這個方法,但是因為子控件的頂層布局是relativelayout,是以一直報錯,不得不放棄這個方法。
二是需要手動把scrollview滾動至最頂端,因為使用這個方法的話,預設在scrollview頂端的項是listview,具體原因不了解,求大神解答…可以在activity中設定:
sv = (scrollview) findviewbyid(r.id.act_solution_1_sv);
2、使用單個listview取代scrollview中所有内容
這個方法是我在試了幾個方法都失敗的情況下自己琢磨出來的。
用一張圖來解釋這個方法的思想:
就是說,把整個需要放在scrollview中的内容,統統放在listview中,原listview上方的資料和下方資料,都作為現listview的一個itemview,和原listview中的單條資料是平級的關系。
xml布局方面十分簡單:
<listview
android:id="@+id/act_solution_2_lv"
android:layout_height="wrap_content">
</listview>
一個單獨的listview就可以了。
原listview上方資料和下方資料,都寫進兩個xml布局檔案中:
java代碼方面,需要自定義一個adapter,在adapter中的getview方法中進行position值的判斷,根據position值來決定inflate哪個布局:
public view getview(int position, view convertview, viewgroup parent) {
//清單第一項
if(position == 0){
convertview = inflater.inflate(r.layout.item_solution2_top, null);
return convertview;
}
//清單最後一項
else if(position == 21){
convertview = inflater.inflate(r.layout.item_solution2_bottom, null);
//普通清單項
viewholder h = null;
if(convertview == null || convertview.gettag() == null){
convertview = inflater.inflate(r.layout.item_listview_data, null);
h = new viewholder();
h.tv = (textview) convertview.findviewbyid(r.id.item_listview_data_tv);
convertview.settag(h);
}else{
h = (viewholder) convertview.gettag();
h.tv.settext("第"+ position + "條資料");
return convertview;
在activty中,隻需要直接為listview設定自定義的adapter就行了。
lv = (listview) findviewbyid(r.id.act_solution_2_lv);
adapter = new adapterforlistview2(this);
lv.setadapter(adapter);
3、使用linearlayout取代listview
既然listview不能适應scrollview,那就換一個可以适應scrollview的控件,幹嘛非要吊死在listview這一棵樹上呢?而linearlayout是最好的選擇。但如果我仍想繼續使用已經定義好的adater呢?我們隻需要自定義一個類繼承自linearlayout,為其加上對baseadapter的适配。
import android.content.context;
import android.util.attributeset;
import android.util.log;
import android.view.view;
import android.widget.baseadapter;
import android.widget.linearlayout;
* 取代listview的linearlayout,使之能夠成功嵌套在scrollview中
* @author terry_龍
public class linearlayoutforlistview extends linearlayout {
private baseadapter adapter;
private onclicklistener onclicklistener = null;
/**
* 綁定布局
*/
public void bindlinearlayout() {
int count = adapter.getcount();
this.removeallviews();
for (int i = 0; i < count; i++) {
view v = adapter.getview(i, null, null);
v.setonclicklistener(this.onclicklistener);
addview(v, i);
}
log.v("counttag", "" + count);
public linearlayoutforlistview(context context) {
super(context);
上面的代碼拷貝儲存為linearlayoutforlistview.class,或者直接拷貝demo中的這個類在自己的工程裡。我們隻需要把原來xml布局檔案中的listview替換為這個類就行了:
<pm.nestificationbetweenscrollviewandabslistview.mywidgets.linearlayoutforlistview
android:id="@+id/act_solution_3_mylinearlayout"
android:layout_height="wrap_content"
android:orientation="vertical" >
</pm.nestificationbetweenscrollviewandabslistview.mywidgets.linearlayoutforlistview>
在activity中也把listview改成linearlayoutforlistview,就能成功運作了。
mylinearlayout = (linearlayoutforlistview) findviewbyid(r.id.act_solution_3_mylinearlayout);
adapter = new adapterforlistview(this);
mylinearlayout.setadapter(adapter);
4、自定義可适應scrollview的listview
這個方法和上面的方法是異曲同工,方法3是自定義了linearlayout以取代listview的功能,但如果我脾氣就是倔,就是要用listview怎麼辦?那就隻好自定義一個類繼承自listview,通過重寫其onmeasure方法,達到對scrollview适配的效果。
下面是繼承了listview的自定義類:
import android.widget.listview;
public class listviewforscrollview extends listview {
public listviewforscrollview(context context) {
public listviewforscrollview(context context, attributeset attrs) {
super(context, attrs);
public listviewforscrollview(context context, attributeset attrs,
int defstyle) {
super(context, attrs, defstyle);
@override
* 重寫該方法,達到使listview适應scrollview的效果
protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
int expandspec = measurespec.makemeasurespec(integer.max_value >> 2,
measurespec.at_most);
super.onmeasure(widthmeasurespec, expandspec);
三個構造方法完全不用動,隻要重寫onmeasure方法,需要改動的地方比起方法3少了不是一點半點…
在xml布局中和activty中使用的listview改成這個自定義listview就行了。代碼就省了吧…
這個方法和方法1有一個同樣的毛病,就是預設顯示的首項是listview,需要手動把scrollview滾動至最頂端。
sv = (scrollview) findviewbyid(r.id.act_solution_4_sv);
sv.smoothscrollto(0, 0);
5、設定scrollview的屬性,使listview能夠成功嵌套(無法達到預定效果)
這個方法是我在寫demo的時候找到的,第一反應是有這個方法我還寫這個demo幹嘛,隻要在布局檔案中添加一個屬性就搞定了。不過結果确實是listview的大小把scrollview的剩餘部分填滿了,但卻不能滾動,真是個緻命的問題…
不廢話了,布局檔案中:
<scrollview
android:id="@+id/act_solution_5_sv"
android:layout_height="fill_parent"
android:layout_below="@+id/act_solution_5_vg_top"
android:fillviewport="true">
設定fillviewport的屬性為true即可。簡單吧?
但是不能滾動這個緻命的問題我卻不知道該怎麼解決了,繼續求大神解答…
四、幾種種方法的優缺點比較
上面一共給出了4中親測可用的方法,各自有使用條件,複雜程度也各不相同。
下面我來從幾個方面來分析幾種方法的優勢和劣勢。
方法1的優點是不用對使用的控件做任何修改,隻需要使用一個現成的方法就好了,而最大的限制是listview的item隻能由linearlayout這一個布局組成,對于一些複雜的布局就不适用了。如果你的工程急需解決這個問題,而且滿足方法的使用條件,即listview的item布局簡單,完全有linearlayout組成,你就隻需要把setlistviewheightbasedonchildren方法拿過去就行了。
方法2的優點是布局檔案設計簡單、activity中的代碼也很少,而缺點卻是自定義adapter變得十分複雜,而且執行效率會變低,因為findviewbyid是十分費時的操作,而使用viewholder結構可以解決費時的問題(有興趣的童鞋可以去搜一艘viewholder結構),然而使用了方法2的話,會破壞這種結構。如果你的工程設計上偏簡單,listview子項相對少、listview上下方資料少、子項間互動少的話,可以嘗試一下。
方法3的優點是完全解決了scrollview嵌套listview的問題,同時代碼較少,你甚至可以直接使用linearlayout,而在activity中手動為linearlayout添加子項控件,不過需要注意的是,在添加前需要調用其removeallviews的方法,否則可能會出現預想不到的事情,那時你會想念天國的listview的。缺點不是很明顯,但還是有兩個:一是使用的不是系統控件,不能在xml布局的graphical layout視圖中直接看到效果;二是不能向listview那樣可以使用viewholder結構,在加載大量子項時會費很多時間在findviewbyid中。如果你的清單資料比較少的話,不妨試試這個方法,除了不能使用viewholder結構,使用方法幾乎和listview一樣。
方法4…比方法3更簡單,代碼更少,同時保留了listview原有的所有方法,包括notifydatasetchanged方法,相比其他方法是最趨近于完美的方法,隻是需要在activity中設定scrollview滾動至頂端。如果你還在猶豫不決的話就選這個方法吧,我想我以後是隻會用這個方法了…