天天看點

Android開發之ScrollView中嵌套ListView的解決方案

在工作中,曾多次碰到scrollview嵌套listview的問題,網上的解決方法有很多種,但是雜而不全。我試過很多種方法,它們各有利弊。

在這裡我将會從使用scrollview嵌套listview結構的原因、這個結構碰到的問題、幾種解決方案和優缺點比較,這4個方面來為大家闡述、分析、總結。

實際上不光是listview,其他繼承自abslistview的類也适用,包括expandablelistview、gridview等等,為了友善說明,以下均用listview來代表。

一、 為什麼要使用scrollview嵌套listview的奇怪的結構        

scrollview和listview都是滾動結構,按理說,這兩個控件在ui上的功能是一樣的,但是看看下面這個設計:

Android開發之ScrollView中嵌套ListView的解決方案

    這是天貓商城的确認訂單的頁面,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)。粗略一看,應該沒有什麼問題。但是看下面的實際效果圖:  

Android開發之ScrollView中嵌套ListView的解決方案

    圖中黑框的部分就是listview,裡面放了20條資料,但是卻隻顯示了1條。

    控件的屬性設定上沒有問題,但是為什麼沒有按照我的想法走呢?

    看看下面這個圖:

Android開發之ScrollView中嵌套ListView的解決方案

    是否有點明白了呢?原因就是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中所有内容

    這個方法是我在試了幾個方法都失敗的情況下自己琢磨出來的。

    用一張圖來解釋這個方法的思想:

Android開發之ScrollView中嵌套ListView的解決方案

    就是說,把整個需要放在scrollview中的内容,統統放在listview中,原listview上方的資料和下方資料,都作為現listview的一個itemview,和原listview中的單條資料是平級的關系。

    xml布局方面十分簡單:

<listview 

    android:id="@+id/act_solution_2_lv"

    android:layout_height="wrap_content">

</listview>

   一個單獨的listview就可以了。

    原listview上方資料和下方資料,都寫進兩個xml布局檔案中:

Android開發之ScrollView中嵌套ListView的解決方案

    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滾動至頂端。如果你還在猶豫不決的話就選這個方法吧,我想我以後是隻會用這個方法了…

繼續閱讀